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 } }, }})
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
}
},
}
})