150 lines · 4.2 KB · TypeScript
RawDownload
import z from "zod"import { Tool } from "./tool"import DESCRIPTION from "./websearch.txt" const API_CONFIG = {  BASE_URL: "https://mcp.exa.ai",  ENDPOINTS: {    SEARCH: "/mcp",  },  DEFAULT_NUM_RESULTS: 8,} as const interface McpSearchRequest {  jsonrpc: string  id: number  method: string  params: {    name: string    arguments: {      query: string      numResults?: number      livecrawl?: "fallback" | "preferred"      type?: "auto" | "fast" | "deep"      contextMaxCharacters?: number    }  }} interface McpSearchResponse {  jsonrpc: string  result: {    content: Array<{      type: string      text: string    }>  }} export const WebSearchTool = Tool.define("websearch", async () => {  return {    get description() {      return DESCRIPTION.replace("{{date}}", new Date().toISOString().slice(0, 10))    },    parameters: z.object({      query: z.string().describe("Websearch query"),      numResults: z.number().optional().describe("Number of search results to return (default: 8)"),      livecrawl: z        .enum(["fallback", "preferred"])        .optional()        .describe(          "Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback')",        ),      type: z        .enum(["auto", "fast", "deep"])        .optional()        .describe(          "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search",        ),      contextMaxCharacters: z        .number()        .optional()        .describe("Maximum characters for context string optimized for LLMs (default: 10000)"),    }),    async execute(params, ctx) {      await ctx.ask({        permission: "websearch",        patterns: [params.query],        always: ["*"],        metadata: {          query: params.query,          numResults: params.numResults,          livecrawl: params.livecrawl,          type: params.type,          contextMaxCharacters: params.contextMaxCharacters,        },      })       const searchRequest: McpSearchRequest = {        jsonrpc: "2.0",        id: 1,        method: "tools/call",        params: {          name: "web_search_exa",          arguments: {            query: params.query,            type: params.type || "auto",            numResults: params.numResults || API_CONFIG.DEFAULT_NUM_RESULTS,            livecrawl: params.livecrawl || "fallback",            contextMaxCharacters: params.contextMaxCharacters,          },        },      }       const controller = new AbortController()      const timeoutId = setTimeout(() => controller.abort(), 25000)       try {        const headers: Record<string, string> = {          accept: "application/json, text/event-stream",          "content-type": "application/json",        }         const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SEARCH}`, {          method: "POST",          headers,          body: JSON.stringify(searchRequest),          signal: AbortSignal.any([controller.signal, ctx.abort]),        })         clearTimeout(timeoutId)         if (!response.ok) {          const errorText = await response.text()          throw new Error(`Search error (${response.status}): ${errorText}`)        }         const responseText = await response.text()         // Parse SSE response        const lines = responseText.split("\n")        for (const line of lines) {          if (line.startsWith("data: ")) {            const data: McpSearchResponse = JSON.parse(line.substring(6))            if (data.result && data.result.content && data.result.content.length > 0) {              return {                output: data.result.content[0].text,                title: `Web search: ${params.query}`,                metadata: {},              }            }          }        }         return {          output: "No search results found. Please try a different query.",          title: `Web search: ${params.query}`,          metadata: {},        }      } catch (error) {        clearTimeout(timeoutId)         if (error instanceof Error && error.name === "AbortError") {          throw new Error("Search request timed out")        }         throw error      }    },  }})