websearch.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import z from "zod"
  2. import { Tool } from "./tool"
  3. import DESCRIPTION from "./websearch.txt"
  4. const API_CONFIG = {
  5. BASE_URL: "https://mcp.exa.ai",
  6. ENDPOINTS: {
  7. SEARCH: "/mcp",
  8. },
  9. DEFAULT_NUM_RESULTS: 8,
  10. } as const
  11. interface McpSearchRequest {
  12. jsonrpc: string
  13. id: number
  14. method: string
  15. params: {
  16. name: string
  17. arguments: {
  18. query: string
  19. numResults?: number
  20. livecrawl?: "fallback" | "preferred"
  21. type?: "auto" | "fast" | "deep"
  22. contextMaxCharacters?: number
  23. }
  24. }
  25. }
  26. interface McpSearchResponse {
  27. jsonrpc: string
  28. result: {
  29. content: Array<{
  30. type: string
  31. text: string
  32. }>
  33. }
  34. }
  35. export const WebSearchTool = Tool.define("websearch", {
  36. description: DESCRIPTION,
  37. parameters: z.object({
  38. query: z.string().describe("Websearch query"),
  39. numResults: z.number().optional().describe("Number of search results to return (default: 8)"),
  40. livecrawl: z
  41. .enum(["fallback", "preferred"])
  42. .optional()
  43. .describe(
  44. "Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback')",
  45. ),
  46. type: z
  47. .enum(["auto", "fast", "deep"])
  48. .optional()
  49. .describe("Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search"),
  50. contextMaxCharacters: z
  51. .number()
  52. .optional()
  53. .describe("Maximum characters for context string optimized for LLMs (default: 10000)"),
  54. }),
  55. async execute(params, ctx) {
  56. await ctx.ask({
  57. permission: "websearch",
  58. patterns: [params.query],
  59. always: ["*"],
  60. metadata: {
  61. query: params.query,
  62. numResults: params.numResults,
  63. livecrawl: params.livecrawl,
  64. type: params.type,
  65. contextMaxCharacters: params.contextMaxCharacters,
  66. },
  67. })
  68. const searchRequest: McpSearchRequest = {
  69. jsonrpc: "2.0",
  70. id: 1,
  71. method: "tools/call",
  72. params: {
  73. name: "web_search_exa",
  74. arguments: {
  75. query: params.query,
  76. type: params.type || "auto",
  77. numResults: params.numResults || API_CONFIG.DEFAULT_NUM_RESULTS,
  78. livecrawl: params.livecrawl || "fallback",
  79. contextMaxCharacters: params.contextMaxCharacters,
  80. },
  81. },
  82. }
  83. const controller = new AbortController()
  84. const timeoutId = setTimeout(() => controller.abort(), 25000)
  85. try {
  86. const headers: Record<string, string> = {
  87. accept: "application/json, text/event-stream",
  88. "content-type": "application/json",
  89. }
  90. const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SEARCH}`, {
  91. method: "POST",
  92. headers,
  93. body: JSON.stringify(searchRequest),
  94. signal: AbortSignal.any([controller.signal, ctx.abort]),
  95. })
  96. clearTimeout(timeoutId)
  97. if (!response.ok) {
  98. const errorText = await response.text()
  99. throw new Error(`Search error (${response.status}): ${errorText}`)
  100. }
  101. const responseText = await response.text()
  102. // Parse SSE response
  103. const lines = responseText.split("\n")
  104. for (const line of lines) {
  105. if (line.startsWith("data: ")) {
  106. const data: McpSearchResponse = JSON.parse(line.substring(6))
  107. if (data.result && data.result.content && data.result.content.length > 0) {
  108. return {
  109. output: data.result.content[0].text,
  110. title: `Web search: ${params.query}`,
  111. metadata: {},
  112. }
  113. }
  114. }
  115. }
  116. return {
  117. output: "No search results found. Please try a different query.",
  118. title: `Web search: ${params.query}`,
  119. metadata: {},
  120. }
  121. } catch (error) {
  122. clearTimeout(timeoutId)
  123. if (error instanceof Error && error.name === "AbortError") {
  124. throw new Error("Search request timed out")
  125. }
  126. throw error
  127. }
  128. },
  129. })