websearch.ts 4.2 KB

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