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..2b09e395 100644 --- a/src/mcp/websearch.test.ts +++ b/src/mcp/websearch.test.ts @@ -1,16 +1,30 @@ -import { describe, expect, test, beforeEach, afterEach } from "bun:test" +import { afterEach, beforeEach, describe, expect, test } from "bun:test" import { createWebsearchConfig } from "./websearch" describe("websearch MCP provider configuration", () => { - const originalEnv = { ...process.env } + 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(() => { - process.env = { ...originalEnv } + 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", () => { @@ -21,6 +35,7 @@ describe("websearch MCP provider configuration", () => { //#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) }) @@ -34,10 +49,11 @@ describe("websearch MCP provider configuration", () => { //#then expect(result.url).toContain("mcp.exa.ai") + expect(result.url).toContain("tools=web_search_exa") expect(result.type).toBe("remote") }) - test("includes x-api-key header when EXA_API_KEY is set", () => { + test("appends exaApiKey query param when EXA_API_KEY is set", () => { //#given const apiKey = "test-exa-key-12345" process.env.EXA_API_KEY = apiKey @@ -46,7 +62,30 @@ 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 + process.env.EXA_API_KEY = "test-exa-key-12345" + + //#when + const result = createWebsearchConfig() + + //#then + expect(result.headers).toBeUndefined() + }) + + 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", () => { @@ -77,7 +116,8 @@ describe("websearch MCP provider configuration", () => { test("returns Exa when both keys present but no explicit provider", () => { //#given - process.env.EXA_API_KEY = "test-exa-key" + const exaKey = "test-exa-key" + process.env.EXA_API_KEY = exaKey process.env.TAVILY_API_KEY = "test-tavily-key" //#when @@ -85,7 +125,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=${encodeURIComponent(exaKey)}`) + expect(result.headers).toBeUndefined() }) test("Tavily config uses Authorization Bearer header format", () => { @@ -111,6 +152,8 @@ describe("websearch MCP provider configuration", () => { //#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 91eddccc..a306ac49 100644 --- a/src/mcp/websearch.ts +++ b/src/mcp/websearch.ts @@ -31,11 +31,10 @@ 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=${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, } }