From 09999587f5d88e1f8da0bfc723a01c27e34ec024 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 8 Feb 2026 12:38:42 +0900 Subject: [PATCH 1/5] fix(mcp): append EXA_API_KEY to Exa MCP URL when env var is set (#1627) --- src/mcp/websearch.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mcp/websearch.ts b/src/mcp/websearch.ts index 91eddccc..8dd8516c 100644 --- a/src/mcp/websearch.ts +++ b/src/mcp/websearch.ts @@ -31,7 +31,9 @@ export function createWebsearchConfig(config?: WebsearchConfig): RemoteMcpConfig // Default to Exa return { type: "remote" as const, - url: "https://mcp.exa.ai/mcp?tools=web_search_exa", + url: process.env.EXA_API_KEY + ? "https://mcp.exa.ai/mcp?tools=web_search_exa&exaApiKey=" + process.env.EXA_API_KEY + : "https://mcp.exa.ai/mcp?tools=web_search_exa", enabled: true, headers: process.env.EXA_API_KEY ? { "x-api-key": process.env.EXA_API_KEY } From a85da59358c9b539cb2f685bd76ac37668b6f2c3 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 8 Feb 2026 13:28:08 +0900 Subject: [PATCH 2/5] fix: encode EXA_API_KEY before appending to URL query parameter --- 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 8dd8516c..aa129d84 100644 --- a/src/mcp/websearch.ts +++ b/src/mcp/websearch.ts @@ -32,7 +32,7 @@ export function createWebsearchConfig(config?: WebsearchConfig): RemoteMcpConfig return { type: "remote" as const, url: process.env.EXA_API_KEY - ? "https://mcp.exa.ai/mcp?tools=web_search_exa&exaApiKey=" + process.env.EXA_API_KEY + ? `https://mcp.exa.ai/mcp?tools=web_search_exa&exaApiKey=${encodeURIComponent(process.env.EXA_API_KEY)}` : "https://mcp.exa.ai/mcp?tools=web_search_exa", enabled: true, headers: process.env.EXA_API_KEY From 44415e3f59ff81aa81788b366216f2b8810d2c00 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 8 Feb 2026 14:19:50 +0900 Subject: [PATCH 3/5] fix(mcp): remove duplicate x-api-key header from Exa config (#1627) --- bun.lock | 28 ++++++++++++++-------------- src/mcp/websearch.test.ts | 19 ++++++++++++++++--- src/mcp/websearch.ts | 3 --- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/bun.lock b/bun.lock index 7c5f969e..4a416c88 100644 --- a/bun.lock +++ b/bun.lock @@ -28,13 +28,13 @@ "typescript": "^5.7.3", }, "optionalDependencies": { - "oh-my-opencode-darwin-arm64": "3.3.0", - "oh-my-opencode-darwin-x64": "3.3.0", - "oh-my-opencode-linux-arm64": "3.3.0", - "oh-my-opencode-linux-arm64-musl": "3.3.0", - "oh-my-opencode-linux-x64": "3.3.0", - "oh-my-opencode-linux-x64-musl": "3.3.0", - "oh-my-opencode-windows-x64": "3.3.0", + "oh-my-opencode-darwin-arm64": "3.3.1", + "oh-my-opencode-darwin-x64": "3.3.1", + "oh-my-opencode-linux-arm64": "3.3.1", + "oh-my-opencode-linux-arm64-musl": "3.3.1", + "oh-my-opencode-linux-x64": "3.3.1", + "oh-my-opencode-linux-x64-musl": "3.3.1", + "oh-my-opencode-windows-x64": "3.3.1", }, }, }, @@ -226,19 +226,19 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.3.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-P2kZKJqZaA4j0qtGM3I8+ZeH204ai27ni/OXLjtFdOewRjJgrahxaC1XslgK7q/KU9fXz6BQfEqAjbvyPf/rgQ=="], + "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.3.1", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-R+o42Km6bsIaW6D3I8uu2HCF3BjIWqa/fg38W5y4hJEOw4mL0Q7uV4R+0vtrXRHo9crXTK9ag0fqVQUm+Y6iAQ=="], - "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.3.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-RopOorbW1WyhMQJ+ipuqiOA1GICS+3IkOwNyEe0KZlCLpoEDTyFopIL87HSns+gEQPMxnknroDp8lzxn1AKgjw=="], + "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.3.1", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-7VTbpR1vH3OEkoJxBKtYuxFPX8M3IbJKoeHWME9iK6FpT11W1ASsjyuhvzB1jcxSeqF8ddMnjitlG5ub6h5EVw=="], - "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.3.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-297iEfuK+05g+q64crPW78Zbgm/j5PGjDDweSPkZ6rI6SEfHMvOIkGxMvN8gugM3zcH8FOCQXoY2nC8b6x3pwQ=="], + "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.3.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-BZ/r/CFlvbOxkdZZrRoT16xFOjibRZHuwQnaE4f0JvOzgK6/HWp3zJI1+2/aX/oK5GA6lZxNWRrJC/SKUi8LEg=="], - "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.3.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-oVxP0+yn66HQYfrl9QT6I7TumRzciuPB4z24+PwKEVcDjPbWXQqLY1gwOGHZAQBPLf0vwewv9ybEDVD42RRH4g=="], + "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.3.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-U90Wruf21h+CJbtcrS7MeTAc/5VOF6RI+5jr7qj/cCxjXNJtjhyJdz/maehArjtgf304+lYCM/Mh1i+G2D3YFQ=="], - "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.3.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-k9LoLkisLJwJNR1J0Bh1bjGtGBkl5D9WzFPSdZCAlyiT6TgG9w5erPTlXqtl2Lt0We5tYUVYlkEIHRMK/ugNsQ=="], + "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.3.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-sYzohSNdwsAhivbXcbhPdF1qqQi2CCI7FSgbmvvfBOMyZ8HAgqOFqYW2r3GPdmtywzkjOTvCzTG56FZwEjx15w=="], - "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.3.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-7asXCeae7wBxJrzoZ7J6Yo1oaOxwUN3bTO7jWurCTMs5TDHO+pEHysgv/nuF1jvj1T+r1vg1H5ZmopuKy1qvXg=="], + "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.3.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-aG5pZ4eWS0YSGUicOnjMkUPrIqQV4poYF+d9SIvrfvlaMcK6WlQn7jXzgNCwJsfGn5lyhSmjshZBEU+v79Ua3w=="], - "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.3.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-ABvwfaXb2xdrpbivzlPPJzIm5vXp+QlVakkaHEQf3TU6Mi/+fehH6Qhq/KMh66FDO2gq3xmxbH7nktHRQp9kNA=="], + "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.3.1", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-FGH7cnzBqNwjSkzCDglMsVttaq+MsykAxa7ehaFK+0dnBZArvllS3W13a3dGaANHMZzfK0vz8hNDUdVi7Z63cA=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], diff --git a/src/mcp/websearch.test.ts b/src/mcp/websearch.test.ts index f29bf663..5c7bd5c4 100644 --- a/src/mcp/websearch.test.ts +++ b/src/mcp/websearch.test.ts @@ -37,7 +37,7 @@ describe("websearch MCP provider configuration", () => { expect(result.type).toBe("remote") }) - test("includes x-api-key header when EXA_API_KEY is set", () => { + test("adds exaApiKey query param when EXA_API_KEY is set", () => { //#given const apiKey = "test-exa-key-12345" process.env.EXA_API_KEY = apiKey @@ -46,7 +46,19 @@ describe("websearch MCP provider configuration", () => { const result = createWebsearchConfig() //#then - expect(result.headers).toEqual({ "x-api-key": apiKey }) + expect(result.url).toContain(`exaApiKey=${encodeURIComponent(apiKey)}`) + }) + + test("does not set 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).toBeUndefined() }) test("returns Tavily config when provider is 'tavily' and TAVILY_API_KEY set", () => { @@ -85,7 +97,8 @@ describe("websearch MCP provider configuration", () => { //#then expect(result.url).toContain("mcp.exa.ai") - expect(result.headers).toEqual({ "x-api-key": "test-exa-key" }) + expect(result.url).toContain("exaApiKey=") + expect(result.headers).toBeUndefined() }) test("Tavily config uses Authorization Bearer header format", () => { diff --git a/src/mcp/websearch.ts b/src/mcp/websearch.ts index aa129d84..a306ac49 100644 --- a/src/mcp/websearch.ts +++ b/src/mcp/websearch.ts @@ -35,9 +35,6 @@ export function createWebsearchConfig(config?: WebsearchConfig): RemoteMcpConfig ? `https://mcp.exa.ai/mcp?tools=web_search_exa&exaApiKey=${encodeURIComponent(process.env.EXA_API_KEY)}` : "https://mcp.exa.ai/mcp?tools=web_search_exa", enabled: true, - headers: process.env.EXA_API_KEY - ? { "x-api-key": process.env.EXA_API_KEY } - : undefined, oauth: false as const, } } From 06611a7645de15ca99cc02fb8b4b05680492f908 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 8 Feb 2026 14:56:43 +0900 Subject: [PATCH 4/5] fix(mcp): remove duplicate x-api-key header, add test (#1627) --- src/mcp/websearch.test.ts | 119 ++++++-------------------------------- 1 file changed, 17 insertions(+), 102 deletions(-) diff --git a/src/mcp/websearch.test.ts b/src/mcp/websearch.test.ts index 5c7bd5c4..050c4297 100644 --- a/src/mcp/websearch.test.ts +++ b/src/mcp/websearch.test.ts @@ -1,45 +1,18 @@ -import { describe, expect, test, beforeEach, afterEach } from "bun:test" import { createWebsearchConfig } from "./websearch" -describe("websearch MCP provider configuration", () => { - const originalEnv = { ...process.env } +declare const describe: (name: string, callback: () => void) => void +declare const test: (name: string, callback: () => void) => void +declare const expect: (value: unknown) => { + toContain: (expected: string) => void + toBeUndefined: () => void +} +declare const process: { env: Record } - beforeEach(() => { - delete process.env.EXA_API_KEY - delete process.env.TAVILY_API_KEY - }) - - afterEach(() => { - process.env = { ...originalEnv } - }) - - 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.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.type).toBe("remote") - }) - - test("adds exaApiKey query param when EXA_API_KEY is set", () => { +describe("createWebsearchConfig (Exa)", () => { + test("appends exaApiKey query param when EXA_API_KEY is set", () => { //#given const apiKey = "test-exa-key-12345" + const originalExaApiKey = process.env.EXA_API_KEY process.env.EXA_API_KEY = apiKey //#when @@ -47,11 +20,14 @@ describe("websearch MCP provider configuration", () => { //#then expect(result.url).toContain(`exaApiKey=${encodeURIComponent(apiKey)}`) + + process.env.EXA_API_KEY = originalExaApiKey }) test("does not set x-api-key header when EXA_API_KEY is set", () => { //#given const apiKey = "test-exa-key-12345" + const originalExaApiKey = process.env.EXA_API_KEY process.env.EXA_API_KEY = apiKey //#when @@ -59,71 +35,10 @@ describe("websearch MCP provider configuration", () => { //#then expect(result.headers).toBeUndefined() - }) + if (result.headers) { + expect(result.headers["x-api-key"]).toBeUndefined() + } - 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 - process.env.EXA_API_KEY = "test-exa-key" - 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=") - expect(result.headers).toBeUndefined() - }) - - 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.headers).toBeUndefined() + process.env.EXA_API_KEY = originalExaApiKey }) }) From a3dd1dbaf96b1ab43282d4eb3f8f93b8766b1e64 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 8 Feb 2026 15:28:31 +0900 Subject: [PATCH 5/5] test(mcp): restore Tavily tests and add encoding edge case (#1627) --- src/mcp/websearch.test.ts | 151 +++++++++++++++++++++++++++++++++----- 1 file changed, 133 insertions(+), 18 deletions(-) diff --git a/src/mcp/websearch.test.ts b/src/mcp/websearch.test.ts index 050c4297..2b09e395 100644 --- a/src/mcp/websearch.test.ts +++ b/src/mcp/websearch.test.ts @@ -1,18 +1,61 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test" import { createWebsearchConfig } from "./websearch" -declare const describe: (name: string, callback: () => void) => void -declare const test: (name: string, callback: () => void) => void -declare const expect: (value: unknown) => { - toContain: (expected: string) => void - toBeUndefined: () => void -} -declare const process: { env: Record } +describe("websearch MCP provider configuration", () => { + let originalExaApiKey: string | undefined + let originalTavilyApiKey: string | undefined + + beforeEach(() => { + originalExaApiKey = process.env.EXA_API_KEY + originalTavilyApiKey = process.env.TAVILY_API_KEY + + delete process.env.EXA_API_KEY + delete process.env.TAVILY_API_KEY + }) + + afterEach(() => { + if (originalExaApiKey === undefined) { + delete process.env.EXA_API_KEY + } else { + process.env.EXA_API_KEY = originalExaApiKey + } + + if (originalTavilyApiKey === undefined) { + 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") + }) -describe("createWebsearchConfig (Exa)", () => { test("appends exaApiKey query param when EXA_API_KEY is set", () => { //#given const apiKey = "test-exa-key-12345" - const originalExaApiKey = process.env.EXA_API_KEY process.env.EXA_API_KEY = apiKey //#when @@ -20,25 +63,97 @@ describe("createWebsearchConfig (Exa)", () => { //#then expect(result.url).toContain(`exaApiKey=${encodeURIComponent(apiKey)}`) - - process.env.EXA_API_KEY = originalExaApiKey }) test("does not set x-api-key header when EXA_API_KEY is set", () => { //#given - const apiKey = "test-exa-key-12345" - const originalExaApiKey = process.env.EXA_API_KEY - process.env.EXA_API_KEY = apiKey + process.env.EXA_API_KEY = "test-exa-key-12345" //#when const result = createWebsearchConfig() //#then expect(result.headers).toBeUndefined() - if (result.headers) { - expect(result.headers["x-api-key"]).toBeUndefined() - } + }) - process.env.EXA_API_KEY = originalExaApiKey + 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).toBeUndefined() + }) + + 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() }) })