5ae0db042d
Switch from query-param (?exaApiKey=...) to Authorization: Bearer header for Exa MCP authentication. The MCP SDK's SSE endpoint resolution discards query parameters when the server replies with a relative path, causing EXA_API_KEY to be silently lost and falling back to rate-limited free tier. Bearer auth via header survives URL resolution and is consistent with the Tavily MCP config pattern already in use. Fixes #3763 Relands #4090 (CLA unsigned by original author) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Minsoo Choi <Neo1228@users.noreply.github.com> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
90 lines
2.6 KiB
TypeScript
90 lines
2.6 KiB
TypeScript
/// <reference types="bun-types" />
|
|
|
|
import { describe, test, expect, spyOn, beforeEach, afterEach } from "bun:test"
|
|
import * as logger from "../shared/logger"
|
|
|
|
let logSpy: ReturnType<typeof spyOn>
|
|
let createWebsearchConfig: (typeof import("./websearch"))["createWebsearchConfig"]
|
|
let originalEnv: Record<"EXA_API_KEY" | "TAVILY_API_KEY", string | undefined>
|
|
|
|
async function importFreshWebsearchModule(): Promise<typeof import("./websearch")> {
|
|
return import(`./websearch?test=${Date.now()}-${Math.random()}`)
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
originalEnv = {
|
|
EXA_API_KEY: process.env.EXA_API_KEY,
|
|
TAVILY_API_KEY: process.env.TAVILY_API_KEY,
|
|
}
|
|
delete process.env.EXA_API_KEY
|
|
delete process.env.TAVILY_API_KEY
|
|
logSpy = spyOn(logger, "log").mockImplementation(() => {})
|
|
;({ createWebsearchConfig } = await importFreshWebsearchModule())
|
|
})
|
|
|
|
afterEach(() => {
|
|
logSpy.mockRestore()
|
|
for (const [key, value] of Object.entries(originalEnv)) {
|
|
if (value === undefined) {
|
|
delete process.env[key]
|
|
continue
|
|
}
|
|
|
|
process.env[key] = value
|
|
}
|
|
})
|
|
|
|
describe("createWebsearchConfig Tavily handling", () => {
|
|
test("returns undefined when Tavily API key is missing", () => {
|
|
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")
|
|
})
|
|
|
|
test("returns valid config when Tavily API key is present", () => {
|
|
process.env.TAVILY_API_KEY = "test-key"
|
|
|
|
const config = createWebsearchConfig({ provider: "tavily" })
|
|
|
|
expect(config).toBeDefined()
|
|
expect(config?.type).toBe("remote")
|
|
expect(config?.url).toBe("https://mcp.tavily.com/mcp/")
|
|
})
|
|
})
|
|
|
|
describe("createWebsearchConfig Exa handling", () => {
|
|
test("keeps EXA_API_KEY out of URL query params and sends bearer auth header", () => {
|
|
process.env.EXA_API_KEY = "exa-secret"
|
|
|
|
const config = createWebsearchConfig({ provider: "exa" })
|
|
|
|
expect(config).toEqual({
|
|
type: "remote",
|
|
url: "https://mcp.exa.ai/mcp?tools=web_search_exa",
|
|
enabled: true,
|
|
headers: {
|
|
Authorization: "Bearer exa-secret",
|
|
},
|
|
oauth: false,
|
|
})
|
|
expect(config?.url).not.toContain("exaApiKey")
|
|
expect(config?.headers).not.toHaveProperty("x-api-key")
|
|
})
|
|
|
|
test("uses unauthenticated Exa URL when EXA_API_KEY is missing", () => {
|
|
delete process.env.EXA_API_KEY
|
|
|
|
const config = createWebsearchConfig({ provider: "exa" })
|
|
|
|
expect(config).toEqual({
|
|
type: "remote",
|
|
url: "https://mcp.exa.ai/mcp?tools=web_search_exa",
|
|
enabled: true,
|
|
oauth: false,
|
|
})
|
|
})
|
|
})
|