Fetch Browser

by TheSethRose
Verified
  • src
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { fetchUrl } from "./url-fetcher.js"; // Schema for Google search parameters const GoogleSearchSchema = z.object({ query: z.string() .min(1) .describe("The search query to execute"), responseType: z.enum(['text', 'json', 'html', 'markdown']) .default('json') .describe("Expected response type"), maxResults: z.number() .min(1) .max(100) .default(10) .describe("Maximum number of results to return"), topic: z.enum(['web', 'news']) .default('web') .describe("Type of search to perform") }); /** * Build a Google search URL with proper parameters */ function buildGoogleSearchUrl(options: { query: string; maxResults?: number; topic?: 'web' | 'news'; }): string { const searchParams = new URLSearchParams({ q: options.query, num: `${options.maxResults || 10}` }); if (options.topic === 'news') { // News tab searchParams.set("tbm", "nws"); } else { // Web tab searchParams.set("udm", "14"); } return `https://www.google.com/search?${searchParams.toString()}`; } /** * Extract URLs from search results */ async function extractSearchUrls(searchResults: string | object[]): Promise<string[]> { if (typeof searchResults === 'string') { // Parse markdown links const urlMatches = searchResults.matchAll(/\[.*?\]\((.*?)\)/g); return Array.from(urlMatches).map(match => match[1]); } else { // Extract URLs from JSON results return (searchResults as any[]).map(result => result.url); } } /** * Register the Google search tool with the MCP server */ export function registerGoogleSearchTool(server: McpServer) { server.tool( "google_search", "Execute a Google search and return results in various formats", GoogleSearchSchema.shape, async (params) => { try { // First, get the search results const searchUrl = buildGoogleSearchUrl({ query: params.query, maxResults: params.maxResults, topic: params.topic }); // Get search results in JSON format to extract URLs const searchResults = await fetchUrl(searchUrl, 'json'); const urls = await extractSearchUrls(searchResults); // Now fetch the full content of each URL const fullResults = await Promise.all( urls.map(async (url) => { try { const content = await fetchUrl(url, params.responseType); return { url, content, error: null }; } catch (error) { return { url, content: null, error: error instanceof Error ? error.message : 'Unknown error' }; } }) ); // Format the results based on response type let formattedResults; switch (params.responseType) { case 'markdown': formattedResults = fullResults .map(r => r.error ? `## [Failed to fetch: ${r.url}]\nError: ${r.error}` : `## [${r.url}]\n\n${r.content}`) .join('\n\n---\n\n'); break; case 'html': formattedResults = fullResults .map(r => r.error ? `<div class="search-result error"><h2><a href="${r.url}">Failed to fetch</a></h2><p class="error">${r.error}</p></div>` : `<div class="search-result"><h2><a href="${r.url}">${r.url}</a></h2>${r.content}</div>`) .join('\n'); break; case 'text': formattedResults = fullResults .map(r => r.error ? `### ${r.url}\nError: ${r.error}` : `### ${r.url}\n\n${r.content}`) .join('\n\n==========\n\n'); break; case 'json': default: formattedResults = JSON.stringify(fullResults, null, 2); break; } return { content: [{ type: "text", text: formattedResults, mimeType: params.responseType === 'json' ? 'application/json' : params.responseType === 'markdown' ? 'text/markdown' : params.responseType === 'html' ? 'text/html' : 'text/plain' }], metadata: { query: params.query, topic: params.topic, maxResults: params.maxResults, responseType: params.responseType, resultsCount: fullResults.length, successCount: fullResults.filter(r => !r.error).length } }; } catch (error) { return { content: [{ type: "text", text: `Failed to execute Google search: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true }; } } ); }