Browse Source

fix: update websearch MCP to support Tavily provider and fix Exa auth (#233)

- Add WebsearchConfig schema with provider enum (exa | tavily)
- Rewrite websearch.ts with createWebsearchConfig() factory
- Exa: use exaApiKey in URL (reliably validated by Exa MCP endpoint)
  instead of x-api-key header (not reliably validated)
- Exa: support anonymous access when no API key is available
- Tavily: support via TAVILY_API_KEY env var with Bearer auth
- Wire websearch config through createBuiltinMcps() to PluginConfig
- Closes #220
alvinreal 2 weeks ago
parent
commit
766c50a037
4 changed files with 64 additions and 15 deletions
  1. 7 0
      src/config/schema.ts
  2. 1 1
      src/index.ts
  3. 13 4
      src/mcp/index.ts
  4. 43 10
      src/mcp/websearch.ts

+ 7 - 0
src/config/schema.ts

@@ -129,6 +129,12 @@ export const PresetSchema = z.record(z.string(), AgentOverrideConfigSchema);
 
 export type Preset = z.infer<typeof PresetSchema>;
 
+// Websearch provider configuration
+export const WebsearchConfigSchema = z.object({
+  provider: z.enum(['exa', 'tavily']).default('exa'),
+});
+export type WebsearchConfig = z.infer<typeof WebsearchConfigSchema>;
+
 // MCP names
 export const McpNameSchema = z.enum(['websearch', 'context7', 'grep_app']);
 export type McpName = z.infer<typeof McpNameSchema>;
@@ -159,6 +165,7 @@ export const PluginConfigSchema = z.object({
   presets: z.record(z.string(), PresetSchema).optional(),
   agents: z.record(z.string(), AgentOverrideConfigSchema).optional(),
   disabled_mcps: z.array(z.string()).optional(),
+  websearch: WebsearchConfigSchema.optional(),
   tmux: TmuxConfigSchema.optional(),
   background: BackgroundTaskConfigSchema.optional(),
   fallback: FailoverConfigSchema.optional(),

+ 1 - 1
src/index.ts

@@ -111,7 +111,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       )
     : {};
 
-  const mcps = createBuiltinMcps(config.disabled_mcps);
+  const mcps = createBuiltinMcps(config.disabled_mcps, config.websearch);
 
   // Initialize TmuxSessionManager to handle OpenCode's built-in Task tool sessions
   const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig);

+ 13 - 4
src/mcp/index.ts

@@ -1,8 +1,8 @@
-import type { McpName } from '../config';
+import type { McpName, WebsearchConfig } from '../config';
 import { context7 } from './context7';
 import { grep_app } from './grep-app';
 import type { McpConfig } from './types';
-import { websearch } from './websearch';
+import { createWebsearchConfig, websearch } from './websearch';
 
 export type { LocalMcpConfig, McpConfig, RemoteMcpConfig } from './types';
 
@@ -13,14 +13,23 @@ const allBuiltinMcps: Record<McpName, McpConfig> = {
 };
 
 /**
- * Creates MCP configurations, excluding disabled ones
+ * Creates MCP configurations, excluding disabled ones.
+ * Accepts an optional websearchConfig to override the default Exa provider.
  */
 export function createBuiltinMcps(
   disabledMcps: readonly string[] = [],
+  websearchConfig?: WebsearchConfig,
 ): Record<string, McpConfig> {
-  return Object.fromEntries(
+  const mcps = Object.fromEntries(
     Object.entries(allBuiltinMcps).filter(
       ([name]) => !disabledMcps.includes(name),
     ),
   );
+
+  // Override websearch with user-configured provider (default: Exa)
+  if (!disabledMcps.includes('websearch')) {
+    mcps.websearch = createWebsearchConfig(websearchConfig);
+  }
+
+  return mcps;
 }

+ 43 - 10
src/mcp/websearch.ts

@@ -1,14 +1,47 @@
+import type { WebsearchConfig } from '../config';
 import type { RemoteMcpConfig } from './types';
 
 /**
- * Exa AI web search - real-time web search
- * @see https://exa.ai
+ * Creates a websearch MCP config based on the provided configuration.
+ * Supports Exa (default) and Tavily providers.
+ * @see https://exa.ai  @see https://tavily.com
  */
-export const websearch: RemoteMcpConfig = {
-  type: 'remote',
-  url: 'https://mcp.exa.ai/mcp?tools=web_search_exa',
-  headers: process.env.EXA_API_KEY
-    ? { 'x-api-key': process.env.EXA_API_KEY }
-    : undefined,
-  oauth: false,
-};
+export function createWebsearchConfig(
+  config?: WebsearchConfig,
+): RemoteMcpConfig {
+  const provider = config?.provider || 'exa';
+
+  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',
+      );
+    }
+    return {
+      type: 'remote',
+      url: 'https://mcp.tavily.com/mcp/',
+      headers: {
+        Authorization: `Bearer ${tavilyKey}`,
+      },
+      oauth: false,
+    };
+  }
+
+  // Default: Exa provider
+  // Prefer exaApiKey in URL (reliably validated by Exa MCP endpoint)
+  // Fall back to anonymous access when no key is available
+  const exaKey = process.env.EXA_API_KEY;
+  const exaUrl = exaKey
+    ? `https://mcp.exa.ai/mcp?tools=web_search_exa&exaApiKey=${encodeURIComponent(exaKey)}`
+    : 'https://mcp.exa.ai/mcp?tools=web_search_exa';
+
+  return {
+    type: 'remote',
+    url: exaUrl,
+    oauth: false,
+  };
+}
+
+// Backward compatibility: default export using default (Exa) config
+export const websearch: RemoteMcpConfig = createWebsearchConfig();