Skip to main content
Glama
tanamurayuuki

Gemini URL Context & Search MCP Server

index.ts7.65 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { ExtractContentUseCase } from './usecase/ExtractContentUseCase.js'; import { GoogleSearchUseCase } from './usecase/GoogleSearchUseCase.js'; import { GenAIFactory } from './adapter/GenAIFactory.js'; import { GoogleSearchGenAI } from './adapter/GoogleSearchGenAI.js'; import { Environment } from './domain/Environment.js'; import { Url } from './domain/Url.js'; import { ModelName } from './domain/ModelName.js'; class GeminiUrlContextServer { private server: Server; private useCase!: ExtractContentUseCase; private googleSearchUseCase!: GoogleSearchUseCase; constructor() { this.server = new Server( { name: 'gemini-url-context', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); this.setupErrorHandling(); } private setupToolHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'url_context_extract', description: 'Extract content from URLs using Gemini AI and return structured JSON with pages, answer, and metadata', inputSchema: { type: 'object', properties: { urls: { type: 'array', items: { type: 'string' }, description: 'Array of URLs to extract content from', }, query: { type: 'string', description: 'Optional query to guide content extraction and summary', }, model: { type: 'string', description: 'Gemini model name to use (optional, defaults to gemini-2.0-flash-exp)', }, maxCharsPerPage: { type: 'number', description: 'Maximum characters per page (optional, defaults to 8000)', }, }, required: ['urls'], }, }, { name: 'google_search', description: 'Search the web using Google Search grounding via Gemini API. Provides search results with sources and citations.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query to find information on the web', }, instruction: { type: 'string', description: 'Optional instruction for processing search results', }, model: { type: 'string', description: 'Gemini model name to use (optional, defaults to gemini-2.0-flash-exp)', }, }, required: ['query'], }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'url_context_extract') { try { const { urls, query, model, maxCharsPerPage } = request.params.arguments as { urls: string[]; query?: string; model?: string; maxCharsPerPage?: number; }; if (!Array.isArray(urls) || urls.length === 0) { throw new McpError( ErrorCode.InvalidParams, 'urls must be a non-empty array of strings' ); } // Validate and create domain objects const urlObjects = urls.map(url => { try { return Url.create(url); } catch (error) { throw new McpError( ErrorCode.InvalidParams, `Invalid URL: ${url}. ${(error as Error).message}` ); } }); const modelName = model ? ModelName.create(model) : ModelName.create(); const maxChars = maxCharsPerPage || 8000; // Execute use case const result = await this.useCase.execute(urlObjects, query, modelName, maxChars); return { content: [ { type: 'text', text: JSON.stringify(result.toJSON(), null, 2), }, ], }; } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Failed to extract URL content: ${(error as Error).message}` ); } } if (request.params.name === 'google_search') { try { const { query, instruction, model } = request.params.arguments as { query?: string; instruction?: string; model?: string; }; if (!query || typeof query !== 'string' || query.trim() === '') { throw new McpError( ErrorCode.InvalidParams, 'query must be a non-empty string' ); } const modelName = model ? ModelName.create(model) : ModelName.create(); const result = await this.googleSearchUseCase.execute(query.trim(), instruction, modelName); // Format response similar to other implementation let responseText = result.result; if (result.searchQueries.length > 0) { responseText += "\n\nSearch Queries:\n" + result.searchQueries.map(q => `- ${q}`).join("\n"); } if (result.sources.length > 0) { responseText += "\n\nSources (Google Search):\n" + result.sources.map(source => `- ${source.title}: ${source.url}`).join("\n"); } return { content: [ { type: 'text', text: responseText, }, ], }; } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Failed to perform Google search: ${(error as Error).message}` ); } } throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); }); } private setupErrorHandling(): void { this.server.onerror = (error) => { console.error('[MCP Error]', error); }; process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } async run(): Promise<void> { // Load environment and initialize use cases const env = Environment.load(); const genAI = GenAIFactory.create(env, false); const googleSearchGenAI = new GoogleSearchGenAI(env.geminiApiKey); this.useCase = new ExtractContentUseCase(genAI); this.googleSearchUseCase = new GoogleSearchUseCase(googleSearchGenAI); const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Gemini URL Context MCP server running on stdio'); } } const server = new GeminiUrlContextServer(); server.run().catch((error) => { console.error('Failed to start server:', (error as Error).message); process.exit(1); });

Implementation Reference

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/tanamurayuuki/MCP-URLcontext'

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