Skip to main content
Glama
index.ts9.85 kB
import dotenv from "dotenv" import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; dotenv.config(); const WEB_SEARCH_TOOL: Tool = { name: "google_web_search", description: "Performs a web search using the Google Custom Search API, ideal for general queries, news, articles, and online content. " + "Use this for broad information gathering, recent events, or when you need diverse web sources. " + "Supports pagination and filtering by site or type. " + "Maximum 10 results per request, with start index for pagination. ", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query" }, count: { type: "number", description: "Number of results (1-10, default 5)", default: 5 }, start: { type: "number", description: "Pagination start index (default 1)", default: 1 }, site: { type: "string", description: "Optional: Limit search to specific site (e.g., 'site:example.com')", default: "" }, }, required: ["query"], }, }; const IMAGE_SEARCH_TOOL: Tool = { name: "google_image_search", description: "Searches for images using Google's Custom Search API. " + "Best for finding images related to specific terms, concepts, or objects. " + "Returns image URLs, titles, and thumbnails. " + "Use this when needing to find relevant images or visual references.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Image search query" }, count: { type: "number", description: "Number of results (1-10, default 5)", default: 5 }, start: { type: "number", description: "Pagination start index (default 1)", default: 1 }, }, required: ["query"] } }; // Server implementation const server = new Server( { name: "example-servers/google-search", version: "0.1.0", }, { capabilities: { tools: {}, }, }, ); // Check for API key and Search Engine ID const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY!; const GOOGLE_CSE_ID = process.env.GOOGLE_CSE_ID!; if (!GOOGLE_API_KEY) { console.error("Error: GOOGLE_API_KEY environment variable is required"); process.exit(1); } if (!GOOGLE_CSE_ID) { console.error("Error: GOOGLE_CSE_ID environment variable is required"); process.exit(1); } // Google API free tier allows 100 search queries per day const RATE_LIMIT = { perDay: 100, perSecond: 5 // To prevent too many requests at once }; let requestCount = { daily: 0, second: 0, lastSecondReset: Date.now(), lastDayReset: new Date().setHours(0, 0, 0, 0) // Start of day }; function checkRateLimit() { const now = Date.now(); // Reset second counter if it's been a second if (now - requestCount.lastSecondReset > 1000) { requestCount.second = 0; requestCount.lastSecondReset = now; } // Reset daily counter if it's a new day const todayStart = new Date().setHours(0, 0, 0, 0); if (todayStart > requestCount.lastDayReset) { requestCount.daily = 0; requestCount.lastDayReset = todayStart; } if (requestCount.second >= RATE_LIMIT.perSecond || requestCount.daily >= RATE_LIMIT.perDay) { throw new Error('Rate limit exceeded'); } requestCount.second++; requestCount.daily++; } interface GoogleSearchResult { kind: string; items?: Array<{ title: string; link: string; snippet: string; pagemap?: { cse_thumbnail?: Array<{ src: string; width: string; height: string; }>; metatags?: Array<Record<string, string>>; }; displayLink?: string; formattedUrl?: string; }>; searchInformation?: { totalResults: string; searchTime: number; }; error?: { code: number; message: string; }; } interface GoogleImageSearchResult { kind: string; items?: Array<{ title: string; link: string; snippet: string; image?: { contextLink: string; height: number; width: number; thumbnailLink: string; thumbnailHeight: number; thumbnailWidth: number; }; }>; error?: { code: number; message: string; }; } function isGoogleWebSearchArgs(args: unknown): args is { query: string; count?: number; start?: number; site?: string } { return ( typeof args === "object" && args !== null && "query" in args && typeof (args as { query: string }).query === "string" ); } function isGoogleImageSearchArgs(args: unknown): args is { query: string; count?: number; start?: number } { return ( typeof args === "object" && args !== null && "query" in args && typeof (args as { query: string }).query === "string" ); } async function performWebSearch(query: string, count: number = 5, start: number = 1, site: string = "") { checkRateLimit(); let searchQuery = query; // If site is provided, append it to the query if (site && !query.includes("site:")) { searchQuery = `${query} site:${site}`; } const url = new URL('https://www.googleapis.com/customsearch/v1'); url.searchParams.set('key', GOOGLE_API_KEY); url.searchParams.set('cx', GOOGLE_CSE_ID); url.searchParams.set('q', searchQuery); url.searchParams.set('num', Math.min(count, 10).toString()); // API limit is 10 url.searchParams.set('start', start.toString()); const response = await fetch(url, { headers: { 'Accept': 'application/json', } }); if (!response.ok) { throw new Error(`Google API error: ${response.status} ${response.statusText}\n${await response.text()}`); } const data = await response.json() as GoogleSearchResult; if (data.error) { throw new Error(`Google API error: ${data.error.code} ${data.error.message}`); } if (!data.items || data.items.length === 0) { return "No results found for your query."; } // Format the results return data.items.map((item, index) => `[${index + 1}] Title: ${item.title}\nDescription: ${item.snippet}\nURL: ${item.link}` ).join('\n\n'); } async function performImageSearch(query: string, count: number = 5, start: number = 1) { checkRateLimit(); const url = new URL('https://www.googleapis.com/customsearch/v1'); url.searchParams.set('key', GOOGLE_API_KEY); url.searchParams.set('cx', GOOGLE_CSE_ID); url.searchParams.set('q', query); url.searchParams.set('num', Math.min(count, 10).toString()); url.searchParams.set('start', start.toString()); url.searchParams.set('searchType', 'image'); const response = await fetch(url, { headers: { 'Accept': 'application/json', } }); if (!response.ok) { throw new Error(`Google API error: ${response.status} ${response.statusText}\n${await response.text()}`); } const data = await response.json() as GoogleImageSearchResult; if (data.error) { throw new Error(`Google API error: ${data.error.code} ${data.error.message}`); } if (!data.items || data.items.length === 0) { return "No image results found for your query."; } // Format the image results return data.items.map((item, index) => `[${index + 1}] Title: ${item.title}\nDescription: ${item.snippet || 'No description'}\nImage URL: ${item.link}\n${item.image?.thumbnailLink ? `Thumbnail: ${item.image.thumbnailLink}` : ''}` ).join('\n\n'); } // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [WEB_SEARCH_TOOL, IMAGE_SEARCH_TOOL], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; if (!args) { throw new Error("No arguments provided"); } switch (name) { case "google_web_search": { if (!isGoogleWebSearchArgs(args)) { throw new Error("Invalid arguments for google_web_search"); } const { query, count = 5, start = 1, site = "" } = args; const results = await performWebSearch(query, count, start, site); return { content: [{ type: "text", text: results }], isError: false, }; } case "google_image_search": { if (!isGoogleImageSearchArgs(args)) { throw new Error("Invalid arguments for google_image_search"); } const { query, count = 5, start = 1 } = args; const results = await performImageSearch(query, count, start); return { content: [{ type: "text", text: results }], isError: false, }; } default: return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true, }; } } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Google Search MCP Server running on stdio"); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/hunter-arton/google_search_mcp_server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server