Merge pull request #1643 from code-yeongyu/fix/exa-api-key-1627

fix(mcp): append EXA_API_KEY to Exa MCP URL when env var is set (#1627)
This commit is contained in:
YeonGyu-Kim 2026-02-08 16:01:59 +09:00 committed by GitHub
commit 2588f33075
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 67 additions and 25 deletions

View File

@ -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=="],

View File

@ -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()
})
})

View File

@ -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,
}
}