Skip to main content
Glama

Google Search MCP Server Streamable HTTP

by Maimikuru
index.ts8.62 kB
#!/usr/bin/env node import dotenv from 'dotenv' import { Server } from '@modelcontextprotocol/sdk/server/index.js' import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js' import express from 'express' import { Config, responseSchema, SearchItem, } from './type.js' dotenv.config() // Validate required environment variables function validateEnvironment(): Config { const googleSearchApiKey = process.env.GOOGLE_SEARCH_API_KEY const googleCseId = process.env.GOOGLE_CSE_ID if (!googleSearchApiKey || !googleCseId) { throw new Error( 'Required environment variables are missing.', ) } return { googleSearchApiKey, googleCseId, port: parseInt(process.env.PORT ?? '3000'), // Default port } } const config: Config = validateEnvironment() // Google Custom Search API implementation export async function customSearch(query: string, num = 5) { const url = new URL('https://www.googleapis.com/customsearch/v1') url.searchParams.set('key', config.googleSearchApiKey) url.searchParams.set('cx', config.googleCseId) url.searchParams.set('q', query) url.searchParams.set('num', String(Math.min(num, 10))) // Limit to max 10 results try { const res = await fetch(url, { headers: { 'Content-Type': 'application/json', }, }) if (!res.ok) { const errorText = await res.text() throw new Error( `Google Search API error: ${res.status} ${res.statusText} - ${errorText}`, ) } const data = await res.json() const parsedData = responseSchema.safeParse(data) if (!parsedData.success) { console.error('Failed to parse API response:', parsedData.error) return [] } return parsedData.data?.items ?? [] } catch (error) { console.error( 'Search error:', error instanceof Error ? error.message : String(error), ) throw error } } // Create MCP server const server = new Server( { name: 'google-search-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, }, ) // Provide tools list server.setRequestHandler(ListToolsRequestSchema, async () => { const tools: Tool[] = [ { name: 'search', description: ' Performs a web search using the Google 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.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query', }, num: { type: 'number', description: 'Number of results to return (1-10, default: 5)', minimum: 1, maximum: 10, default: 5, }, }, required: ['query'], }, }, ] return { tools } }) // Handle tool execution server.setRequestHandler(CallToolRequestSchema, async request => { if (request.params.name === 'search') { const { query, num = 5 } = request.params.arguments as { query: string num?: number } try { const results = await customSearch(query, num) return { content: [ { type: 'text', text: JSON.stringify( { query, resultsCount: results.length, results: results.map((item: SearchItem) => ({ title: item.title, url: item.link, snippet: item.snippet, metadata: { ogTitle: item.pagemap?.metatags?.[0]?.['og:title'], ogDescription: item.pagemap?.metatags?.[0] ?.['og:description'], ogImage: item.pagemap?.metatags?.[0]?.['og:image'], }, })), }, null, 2, ), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Search error: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, } } } throw new Error(`Unknown tool: ${request.params.name}`) }) // Server startup process async function main() { console.log('Starting google-search-mcp server') const app = express() // Parse JSON request body app.use(express.json({ limit: '10mb' })) // Request logging (development only) if (process.env.NODE_ENV === 'development') { app.use((req, res, next) => { console.log(`${req.method} ${req.path}`) next() }) } // Create StreamableHTTPServerTransport (stateless) const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, // Set to undefined for stateless server enableJsonResponse: true, // Support both HTTP JSON response and SSE streaming }) // Connect MCP server to transport await server.connect(transport) console.log('MCP server connected to transport') // MCP endpoint (StreamableHTTP) // Handle MCP communication via POST requests app.post('/mcp', async (req, res) => { try { await transport.handleRequest(req, res, req.body) } catch (error) { console.error( 'MCP request handling error:', error instanceof Error ? error.message : String(error), ) if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, }) } } }) // GET requests return 405 for SSE endpoint compatibility app.get('/mcp', (req, res) => { res.status(405).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed. Use POST for Streamable HTTP.', }, id: null, }) }) // DELETE requests return 405 for stateless server app.delete('/mcp', (req, res) => { res.status(405).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed. Stateless server does not support session termination.', }, id: null, }) }) // Health check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString(), version: '1.0.0', mode: 'streamable-http', config: { googleApiConfigured: true, // Checked at startup port: config.port, }, }) }) // Root endpoint (minimal info for security) app.get('/', (req, res) => { res.json({ status: 'ok' }) }) // 404 handler app.use('*', (req, res) => { res.status(404).json({ error: 'Not Found', message: 'The requested resource was not found', }) }) // Error handler app.use( ( err: Error, req: express.Request, res: express.Response, _next: express.NextFunction, ) => { console.error('Express error:', err.message) res.status(500).json({ error: 'Internal Server Error', message: process.env.NODE_ENV === 'development' ? err.message : 'An internal server error occurred', }) }, ) // Start HTTP server const httpServer = app.listen(config.port, '0.0.0.0', () => { console.log(`google-search-mcp server started (port: ${config.port})`) }) // Graceful shutdown const shutdown = (signal: string) => { console.log(`Received ${signal} signal. Shutting down server...`) httpServer.close(err => { if (err) { console.error('Server shutdown error:', err.message) process.exit(1) } else { console.log('Server shut down gracefully') process.exit(0) } }) } process.on('SIGTERM', () => shutdown('SIGTERM')) process.on('SIGINT', () => shutdown('SIGINT')) } // Error handling process.on('uncaughtException', error => { console.error('Uncaught exception:', error.message, error.stack) process.exit(1) }) process.on('unhandledRejection', (reason, _promise) => { console.error('Unhandled promise rejection:', String(reason)) process.exit(1) }) // Execute main process main().catch(error => { console.error('Server startup error:', error.message, error.stack) process.exit(1) })

Latest Blog Posts

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/Maimikuru/google-search-mcp'

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