diff --git a/.env.example b/.env.example index 813f7aa..7f1576d 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,13 @@ # Get yours at: https://api.search.brave.com BRAVE_API_KEY= +# Tavily Search API — Free: 1000 credits/month +# Get yours at: https://app.tavily.com +TAVILY_API_KEY= + +# Search provider selection: 'brave' (default) or 'tavily' +# SEARCH_PROVIDER=brave + # GetXAPI — For X/Twitter search, threads, user posts ($0.001/call) # Get yours at: https://getxapi.com GETXAPI_KEY= diff --git a/.gitignore b/.gitignore index d21b1cb..4a1aa1f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ .env dist/ *.log +package-lock.json diff --git a/package.json b/package.json index 1b2eb97..afba7b0 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "@mozilla/readability": "^0.5.0", + "@tavily/core": "^0.6.0", "linkedom": "^0.18.0" } } diff --git a/tools/web-search/index.ts b/tools/web-search/index.ts index e701779..3d0b9a3 100644 --- a/tools/web-search/index.ts +++ b/tools/web-search/index.ts @@ -1,14 +1,22 @@ import { z } from "zod"; +import { tavily } from "@tavily/core"; import type { ToolDefinition } from "../../lib/tool-registry.js"; import { requireEnv } from "../../lib/rapidapi.js"; -interface BraveResult { +interface SearchResult { title: string; url: string; description: string; } -async function execute({ query, count }: { query: string; count: number }): Promise { +function formatResults(results: SearchResult[], query: string): string { + if (results.length === 0) return `No results found for: "${query}"`; + return results + .map((r, i) => `${i + 1}. **${r.title}**\n ${r.url}\n ${r.description}`) + .join("\n\n"); +} + +async function searchBrave(query: string, count: number): Promise { const apiKey = requireEnv("BRAVE_API_KEY"); const url = new URL("https://api.search.brave.com/res/v1/web/search"); @@ -25,22 +33,79 @@ async function execute({ query, count }: { query: string; count: number }): Prom }); if (!response.ok) { - return `Brave Search error: ${response.status} ${response.statusText}`; + throw new Error(`Brave Search error: ${response.status} ${response.statusText}`); } - const data = (await response.json()) as { web?: { results?: BraveResult[] } }; - const results = data.web?.results ?? []; + const data = (await response.json()) as { web?: { results?: SearchResult[] } }; + return (data.web?.results ?? []).map((r) => ({ + title: r.title, + url: r.url, + description: r.description, + })); +} - if (results.length === 0) return `No results found for: "${query}"`; +async function searchTavily(query: string, count: number): Promise { + const apiKey = requireEnv("TAVILY_API_KEY"); + const client = tavily({ apiKey }); - return results - .map((r, i) => `${i + 1}. **${r.title}**\n ${r.url}\n ${r.description}`) - .join("\n\n"); + const response = await client.search(query, { + maxResults: Math.min(Math.max(count, 1), 20), + }); + + return (response.results ?? []).map((r) => ({ + title: r.title, + url: r.url, + description: r.content, + })); +} + +function getSearchProvider(): "brave" | "tavily" { + const provider = process.env.SEARCH_PROVIDER?.toLowerCase(); + if (provider === "tavily") return "tavily"; + return "brave"; +} + +async function execute({ query, count }: { query: string; count: number }): Promise { + const provider = getSearchProvider(); + + if (provider === "tavily") { + try { + const results = await searchTavily(query, count); + return formatResults(results, query); + } catch (err) { + // Fall back to Brave if Tavily fails and BRAVE_API_KEY is available + if (process.env.BRAVE_API_KEY) { + try { + const results = await searchBrave(query, count); + return formatResults(results, query); + } catch { + // Both providers failed; return original Tavily error + } + } + return `Tavily Search error: ${err instanceof Error ? err.message : String(err)}`; + } + } + + try { + const results = await searchBrave(query, count); + return formatResults(results, query); + } catch (err) { + // Fall back to Tavily if Brave fails and TAVILY_API_KEY is available + if (process.env.TAVILY_API_KEY) { + try { + const results = await searchTavily(query, count); + return formatResults(results, query); + } catch { + // Both providers failed; return original Brave error + } + } + return err instanceof Error ? err.message : String(err); + } } export const definition: ToolDefinition = { name: "web_search", - description: "Search the web using Brave Search. Returns titles, URLs, and descriptions.", + description: "Search the web using Brave Search or Tavily. Returns titles, URLs, and descriptions.", params: { query: z.string().describe("Search query"), count: z.number().min(1).max(20).default(5).describe("Number of results (1-20)"),