#!/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);
});