Gemini Search MCP Server

  • src
#!/usr/bin/env node import 'dotenv/config'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js'; import axios from 'axios'; import { GoogleNewsSearchArgs, isValidGoogleNewsSearchArgs, NewsArticle, NewsCategory, FormattedNewsResponse, MenuLink } from './types.js'; // SerpAPI配置 const SERP_API_KEY = process.env.SERP_API_KEY; const SERP_API_BASE_URL = 'https://serpapi.com/search'; class GoogleNewsServer { private server: Server; private axiosInstance: import('axios').AxiosInstance; constructor() { if (!process.env.SERP_API_KEY) { throw new Error('SERP_API_KEY environment variable is required'); } this.server = new Server( { name: 'google-news-server', version: '1.0.0' }, { capabilities: { tools: {} } } ); this.axiosInstance = axios.create({ baseURL: SERP_API_BASE_URL, }); this.setupHandlers(); this.setupErrorHandling(); } private setupErrorHandling(): void { this.server.onerror = (error) => { console.error('[MCP Error]', error); }; process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private setupHandlers(): void { // 注册可用工具 this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'google_news_search', description: 'Search Google News for articles and news content. Results will be automatically categorized by topic.', inputSchema: { type: 'object', properties: { q: { type: 'string', description: 'Search query', }, gl: { type: 'string', description: 'Country code (e.g., us, uk)', default: 'us' }, hl: { type: 'string', description: 'Language code (e.g., en)', default: 'en' }, topic_token: { type: 'string', description: 'Topic token for specific news topics', }, publication_token: { type: 'string', description: 'Publication token for specific publishers', }, story_token: { type: 'string', description: 'Story token for full coverage of a story', }, section_token: { type: 'string', description: 'Section token for specific sections', }, }, }, }, ], })); // 处理工具调用 this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === 'google_news_search') { if (!isValidGoogleNewsSearchArgs(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for google_news_search' ); } const params = { ...args, engine: 'google_news', api_key: SERP_API_KEY, gl: args.gl || 'us', hl: args.hl === 'zh' ? 'en' : (args.hl || 'en') }; try { const response = await this.axiosInstance.get('', { params }); const formattedResults = this.formatNewsResults(response.data); const resultText = this.formatResponseText(formattedResults); return { content: [{ type: 'text', text: resultText }] }; } catch (error) { return { isError: true, content: [{ type: 'text', text: this.getErrorMessage(error) }] }; } } throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); }); } private formatNewsResults(data: any): FormattedNewsResponse { const newsResults = data.news_results || []; const menuLinks = data.menu_links || []; const categories = new Map<string, NewsArticle[]>(); // 根据内容分类整理新闻 newsResults.forEach((result: any) => { let category = 'General'; // 通过标题和内容关键词判断分类 const titleAndSnippet = (result.title + ' ' + (result.snippet || '')).toLowerCase(); if (titleAndSnippet.match(/ai|artificial intelligence|machine learning|deep learning/)) { category = 'AI & Technology'; } else if (titleAndSnippet.match(/business|economy|market|finance/)) { category = 'Business'; } else if (titleAndSnippet.match(/science|research|study|discovery/)) { category = 'Science & Research'; } else if (titleAndSnippet.match(/health|medical|disease|treatment/)) { category = 'Healthcare'; } if (!categories.has(category)) { categories.set(category, []); } categories.get(category)?.push({ title: result.title, source: result.source.name, link: result.link, date: result.date, snippet: result.snippet, authors: result.source.authors }); }); // 转换为最终格式 const formattedCategories: NewsCategory[] = Array.from(categories.entries()) .map(([name, articles]) => ({ name, articles: articles.sort((a, b) => { // 按日期降序排序 const dateA = a.date ? new Date(a.date) : new Date(0); const dateB = b.date ? new Date(b.date) : new Date(0); return dateB.getTime() - dateA.getTime(); }) })); return { categories: formattedCategories, menu_links: menuLinks, timestamp: new Date().toISOString() }; } private formatResponseText(formattedResults: FormattedNewsResponse): string { const categoryTexts = formattedResults.categories .map(category => { const articlesList = category.articles .map(article => { let text = `- ${article.title}\n`; text += ` Source: ${article.source}\n`; if (article.authors?.length) { text += ` Authors: ${article.authors.join(', ')}\n`; } if (article.snippet) { text += ` Summary: ${article.snippet}\n`; } if (article.date) { text += ` Date: ${article.date}\n`; } text += ` Link: ${article.link}`; return text; }) .join('\n\n'); return `${category.name} (${category.articles.length} articles):\n${articlesList}`; }) .join('\n\n' + '='.repeat(80) + '\n\n'); return categoryTexts; } private getErrorMessage(error: any): string { if (axios.isAxiosError(error)) { if (error.response?.data?.error === "Unsupported `zh` interface language - hl parameter.") { return "抱歉,Google News API 暂不支持中文界面。已自动切换为英文显示。"; } return `API 错误: ${error.response?.data?.error || error.message}`; } return `未知错误: ${error.message || '请稍后重试'}`; } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); } } // 创建并启动服务器 const server = new GoogleNewsServer(); server.run().catch(console.error);