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
This commit is contained in:
+4
-1
@@ -17,7 +17,10 @@ export function createBuiltinMcps(disabledMcps: string[] = [], config?: OhMyOpen
|
||||
const mcps: Record<string, RemoteMcpConfig> = {}
|
||||
|
||||
if (!disabledMcps.includes("websearch")) {
|
||||
mcps.websearch = createWebsearchConfig(config?.websearch)
|
||||
const websearchConfig = createWebsearchConfig(config?.websearch)
|
||||
if (websearchConfig) {
|
||||
mcps.websearch = websearchConfig
|
||||
}
|
||||
}
|
||||
|
||||
if (!disabledMcps.includes("context7")) {
|
||||
|
||||
+36
-148
@@ -1,160 +1,48 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
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<typeof spyOn>
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user