From a695730891088b7dd05771c18bd0fdb3ad712f7e Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 2 Apr 2026 13:32:44 +0900 Subject: [PATCH 1/2] fix(mcp): handle missing Tavily API key gracefully Previously, when websearch was configured with Tavily provider and the TAVILY_API_KEY environment variable was not set, the entire plugin would fail to load with no visible error to the user. Changes: 1. createWebsearchConfig now returns undefined when Tavily key is missing 2. Added warning log: '[websearch] Tavily API key not found, skipping websearch MCP' 3. createBuiltinMcps now skips undefined configs instead of adding them 4. Added tests for both missing and present Tavily API key scenarios Fixes #2996 --- src/mcp/index.ts | 5 +- src/mcp/websearch.test.ts | 184 ++++++++------------------------------ src/mcp/websearch.ts | 4 +- 3 files changed, 43 insertions(+), 150 deletions(-) diff --git a/src/mcp/index.ts b/src/mcp/index.ts index f97261477..bc9da4d31 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -17,7 +17,10 @@ export function createBuiltinMcps(disabledMcps: string[] = [], config?: OhMyOpen const mcps: Record = {} if (!disabledMcps.includes("websearch")) { - mcps.websearch = createWebsearchConfig(config?.websearch) + const websearchConfig = createWebsearchConfig(config?.websearch) + if (websearchConfig) { + mcps.websearch = websearchConfig + } } if (!disabledMcps.includes("context7")) { diff --git a/src/mcp/websearch.test.ts b/src/mcp/websearch.test.ts index 572ebae33..c525683e2 100644 --- a/src/mcp/websearch.test.ts +++ b/src/mcp/websearch.test.ts @@ -1,160 +1,48 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test" +/// + +import { describe, test, expect, spyOn, beforeEach, afterEach } from "bun:test" import { createWebsearchConfig } from "./websearch" +import * as shared from "../shared" -describe("websearch MCP provider configuration", () => { - let originalExaApiKey: string | undefined - let originalTavilyApiKey: string | undefined +let logSpy: ReturnType - beforeEach(() => { - originalExaApiKey = process.env.EXA_API_KEY - originalTavilyApiKey = process.env.TAVILY_API_KEY +beforeEach(() => { + logSpy = spyOn(shared, "log").mockImplementation(() => {}) +}) - delete process.env.EXA_API_KEY +afterEach(() => { + logSpy.mockRestore() +}) + +describe("createWebsearchConfig Tavily handling", () => { + test("returns undefined when Tavily API key is missing", () => { + const originalEnv = process.env.TAVILY_API_KEY delete process.env.TAVILY_API_KEY + + const config = createWebsearchConfig({ provider: "tavily" }) + + expect(config).toBeUndefined() + expect(logSpy).toHaveBeenCalledWith("[websearch] Tavily API key not found, skipping websearch MCP") + + if (originalEnv) { + process.env.TAVILY_API_KEY = originalEnv + } }) - afterEach(() => { - if (originalExaApiKey === undefined) { - delete process.env.EXA_API_KEY - } else { - process.env.EXA_API_KEY = originalExaApiKey - } + test("returns valid config when Tavily API key is present", () => { + const originalEnv = process.env.TAVILY_API_KEY + process.env.TAVILY_API_KEY = "test-key" - if (originalTavilyApiKey === undefined) { + const config = createWebsearchConfig({ provider: "tavily" }) + + expect(config).toBeDefined() + expect(config?.type).toBe("remote") + expect(config?.url).toBe("https://mcp.tavily.com/mcp/") + + if (originalEnv) { + process.env.TAVILY_API_KEY = originalEnv + } else { delete process.env.TAVILY_API_KEY - } else { - process.env.TAVILY_API_KEY = originalTavilyApiKey } }) - - test("returns Exa config when no config provided", () => { - //#given - no config - - //#when - const result = createWebsearchConfig() - - //#then - expect(result.url).toContain("mcp.exa.ai") - expect(result.url).toContain("tools=web_search_exa") - expect(result.type).toBe("remote") - expect(result.enabled).toBe(true) - }) - - test("returns Exa config when provider is 'exa'", () => { - //#given - const config = { provider: "exa" as const } - - //#when - const result = createWebsearchConfig(config) - - //#then - expect(result.url).toContain("mcp.exa.ai") - expect(result.url).toContain("tools=web_search_exa") - expect(result.type).toBe("remote") - }) - - test("appends exaApiKey query param when EXA_API_KEY is set", () => { - //#given - const apiKey = "test-exa-key-12345" - process.env.EXA_API_KEY = apiKey - - //#when - const result = createWebsearchConfig() - - //#then - expect(result.url).toContain(`exaApiKey=${encodeURIComponent(apiKey)}`) - }) - - test("sets x-api-key header when EXA_API_KEY is set", () => { - //#given - const apiKey = "test-exa-key-12345" - process.env.EXA_API_KEY = apiKey - - //#when - const result = createWebsearchConfig() - - //#then - expect(result.headers).toEqual({ "x-api-key": apiKey }) - }) - - test("URL-encodes EXA_API_KEY when it contains special characters", () => { - //#given an EXA_API_KEY with special characters (+ & =) - const apiKey = "a+b&c=d" - process.env.EXA_API_KEY = apiKey - - //#when createWebsearchConfig is called - const result = createWebsearchConfig() - - //#then the URL contains the properly encoded key via encodeURIComponent - expect(result.url).toContain(`exaApiKey=${encodeURIComponent(apiKey)}`) - }) - - test("returns Tavily config when provider is 'tavily' and TAVILY_API_KEY set", () => { - //#given - const tavilyKey = "test-tavily-key-67890" - process.env.TAVILY_API_KEY = tavilyKey - const config = { provider: "tavily" as const } - - //#when - const result = createWebsearchConfig(config) - - //#then - expect(result.url).toContain("mcp.tavily.com") - expect(result.headers).toEqual({ Authorization: `Bearer ${tavilyKey}` }) - }) - - test("throws error when provider is 'tavily' but TAVILY_API_KEY missing", () => { - //#given - delete process.env.TAVILY_API_KEY - const config = { provider: "tavily" as const } - - //#when - const createTavilyConfig = () => createWebsearchConfig(config) - - //#then - expect(createTavilyConfig).toThrow("TAVILY_API_KEY environment variable is required") - }) - - test("returns Exa when both keys present but no explicit provider", () => { - //#given - const exaKey = "test-exa-key" - process.env.EXA_API_KEY = exaKey - process.env.TAVILY_API_KEY = "test-tavily-key" - - //#when - const result = createWebsearchConfig() - - //#then - expect(result.url).toContain("mcp.exa.ai") - expect(result.url).toContain(`exaApiKey=${encodeURIComponent(exaKey)}`) - expect(result.headers).toEqual({ "x-api-key": exaKey }) - }) - - test("Tavily config uses Authorization Bearer header format", () => { - //#given - const tavilyKey = "tavily-secret-key-xyz" - process.env.TAVILY_API_KEY = tavilyKey - const config = { provider: "tavily" as const } - - //#when - const result = createWebsearchConfig(config) - - //#then - expect(result.headers?.Authorization).toMatch(/^Bearer /) - expect(result.headers?.Authorization).toBe(`Bearer ${tavilyKey}`) - }) - - test("Exa config has no headers when EXA_API_KEY not set", () => { - //#given - delete process.env.EXA_API_KEY - - //#when - const result = createWebsearchConfig() - - //#then - expect(result.url).toContain("mcp.exa.ai") - expect(result.url).toContain("tools=web_search_exa") - expect(result.url).not.toContain("exaApiKey=") - expect(result.headers).toBeUndefined() - }) }) diff --git a/src/mcp/websearch.ts b/src/mcp/websearch.ts index 74301d033..92e6fcf35 100644 --- a/src/mcp/websearch.ts +++ b/src/mcp/websearch.ts @@ -1,4 +1,5 @@ import type { WebsearchConfig } from "../config/schema" +import { log } from "../shared/logger" type RemoteMcpConfig = { type: "remote" @@ -14,7 +15,8 @@ export function createWebsearchConfig(config?: WebsearchConfig): RemoteMcpConfig if (provider === "tavily") { const tavilyKey = process.env.TAVILY_API_KEY if (!tavilyKey) { - throw new Error("TAVILY_API_KEY environment variable is required for Tavily provider") + log("[websearch] Tavily API key not found, skipping websearch MCP") + return undefined } return { From 5d68de79d0e6ff39da959dc47b140d04f22f677d Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 2 Apr 2026 13:55:21 +0900 Subject: [PATCH 2/2] fix(types): return type RemoteMcpConfig | undefined for createWebsearchConfig --- src/mcp/websearch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/websearch.ts b/src/mcp/websearch.ts index 92e6fcf35..a1ab4600e 100644 --- a/src/mcp/websearch.ts +++ b/src/mcp/websearch.ts @@ -9,7 +9,7 @@ type RemoteMcpConfig = { oauth?: false } -export function createWebsearchConfig(config?: WebsearchConfig): RemoteMcpConfig { +export function createWebsearchConfig(config?: WebsearchConfig): RemoteMcpConfig | undefined { const provider = config?.provider || "exa" if (provider === "tavily") {