# Directory Structure ``` ├── .env.example ├── .gitignore ├── examples │ └── claude-config.json ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── config.ts │ ├── index.ts │ └── tools │ └── newsSearch.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` # News API Keys - Add your API keys here # You can get free API keys from the following services: # NewsAPI.org - 100 requests/day # Get your key at: https://newsapi.org/register NEWSAPI_ORG_KEY=your_newsapi_org_key_here # GNews API - 100 requests/day # Get your key at: https://gnews.io/ GNEWS_API_KEY=your_gnews_api_key_here # TheNewsAPI.com - Unlimited (claimed) # Get your key at: https://www.thenewsapi.com/ THE_NEWS_API_KEY=your_the_news_api_key_here # NewsData.io - ~200 requests/day # Get your key at: https://newsdata.io/ NEWSDATA_IO_KEY=your_newsdata_io_key_here # Twingly News API - Trial available # Get your key at: https://www.twingly.com/news-api/ TWINGLY_API_KEY=your_twingly_api_key_here ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # Build output build/ dist/ *.tsbuildinfo # Environment variables .env .env.local .env.development.local .env.test.local .env.production.local # IDE files .vscode/ .idea/ *.swp *.swo *~ # OS generated files .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db # Logs logs *.log # Runtime data pids *.pid *.seed *.pid.lock # Coverage directory used by tools like istanbul coverage/ *.lcov # nyc test coverage .nyc_output # Dependency directories jspm_packages/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next # Nuxt.js build / generate output .nuxt # Gatsby files .cache/ public # Storybook build outputs .out .storybook-out # Temporary folders tmp/ temp/ ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # 📰 News MCP Server [](https://opensource.org/licenses/Apache-2.0) [](https://www.typescriptlang.org/) [](https://modelcontextprotocol.io/) A smart news search MCP (Model Context Protocol) server with **automatic API switching** for reliable news fetching. Never worry about API limits again! <a href="https://glama.ai/mcp/servers/@guangxiangdebizi/news-mcp"> <img width="380" height="200" src="https://glama.ai/mcp/servers/@guangxiangdebizi/news-mcp/badge" alt="News Server MCP server" /> </a> ## ✨ Features - 🔄 **Smart API Switching**: Automatically switches between multiple news APIs when one reaches its limit - 🆓 **Multiple Free APIs**: Supports 5+ free news APIs with generous daily quotas - 📊 **Quota Management**: Intelligent tracking of daily API usage limits - 🌍 **Multi-language Support**: Search news in 20+ languages - ⚡ **Fast & Reliable**: Failover mechanism ensures you always get results - 🛠️ **Easy Setup**: Simple configuration with environment variables ## 🚀 Quick Start ### 1. Installation ```bash # Clone the repository git clone https://github.com/guangxiangdebizi/news-mcp.git cd news-mcp # Install dependencies npm install # Copy environment template cp .env.example .env ``` ### 2. API Configuration Get free API keys from any of these services (you only need one, but more = better reliability): | Service | Daily Limit | Sign Up Link | Priority | |---------|-------------|--------------|----------| | **TheNewsAPI** | Unlimited* | [Get Key](https://www.thenewsapi.com/) | 🥇 Highest | | **NewsData.io** | ~200 requests | [Get Key](https://newsdata.io/) | 🥈 High | | **NewsAPI.org** | 100 requests | [Get Key](https://newsapi.org/register) | 🥉 Medium | | **GNews** | 100 requests | [Get Key](https://gnews.io/) | 🏅 Medium | | **Twingly** | Trial available | [Get Key](https://www.twingly.com/news-api/) | 🎖️ Low | *\*Claimed unlimited for free tier* ### 3. Configure Environment Edit `.env` file and add your API keys: ```env # Add at least one API key (more is better for reliability) THE_NEWS_API_KEY=your_api_key_here NEWSDATA_IO_KEY=your_api_key_here NEWSAPI_ORG_KEY=your_api_key_here GNEWS_API_KEY=your_api_key_here TWINGLY_API_KEY=your_api_key_here ``` ### 4. Build & Run ```bash # Build the project npm run build # Start the MCP server npm start # Or run in development mode npm run dev ``` ## 🔧 Usage ### With Claude Desktop Add to your Claude Desktop configuration: **Stdio Mode** (Local Development): ```json { "mcpServers": { "news-mcp": { "command": "node", "args": ["path/to/news-mcp/build/index.js"] } } } ``` **SSE Mode** (Web Access): ```bash # Start SSE server npm run sse ``` ```json { "mcpServers": { "news-mcp": { "type": "sse", "url": "http://localhost:3100/sse", "timeout": 600 } } } ``` ### Available Tools #### `search_news` Search for news articles with automatic API switching. **Parameters:** - `query` (required): Search keywords or topics - `language` (optional): Language code (default: "en") - `limit` (optional): Number of articles (1-10, default: 5) **Example:** ```typescript // Search for technology news { "query": "artificial intelligence", "language": "en", "limit": 5 } // Search for Chinese news { "query": "科技新闻", "language": "zh", "limit": 3 } ``` ## 🧠 How Smart Switching Works 1. **Priority Order**: APIs are tried in order of reliability and quota limits 2. **Quota Tracking**: System tracks daily usage for each API 3. **Automatic Failover**: When an API fails or reaches limit, automatically tries the next one 4. **Success Guarantee**: Continues until successful response or all APIs exhausted ``` TheNewsAPI (∞) → NewsData.io (200) → NewsAPI.org (100) → GNews (100) → Twingly (50) ``` ## 📊 Supported Languages - **English** (en) - All APIs - **Chinese** (zh) - Most APIs - **Spanish** (es) - Most APIs - **French** (fr) - Most APIs - **German** (de) - Most APIs - **And 15+ more languages** ## 🛠️ Development ### Project Structure ``` src/ ├── index.ts # MCP server entry point ├── config.ts # API configuration management └── tools/ └── newsSearch.ts # Smart news search tool ``` ### Scripts ```bash npm run build # Build TypeScript npm run dev # Development mode with watch npm start # Start built server npm run sse # Start SSE server on port 3100 ``` ### Adding New APIs 1. Add API configuration to `src/config.ts` 2. Implement API-specific request logic in `src/tools/newsSearch.ts` 3. Add environment variable to `.env.example` ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. 1. Fork the repository 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 4. Push to the branch (`git push origin feature/AmazingFeature`) 5. Open a Pull Request ## 📝 License This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. ## 👨💻 Author **Xingyu Chen** - 🌐 Website: [GitHub Profile](https://github.com/guangxiangdebizi/) - 📧 Email: [email protected] - 💼 LinkedIn: [Xingyu Chen](https://www.linkedin.com/in/xingyu-chen-b5b3b0313/) - 📦 NPM: [@xingyuchen](https://www.npmjs.com/~xingyuchen) ## 🙏 Acknowledgments - [Model Context Protocol](https://modelcontextprotocol.io/) for the amazing MCP framework - All the news API providers for their generous free tiers - The open-source community for inspiration and support --- **⭐ If this project helped you, please give it a star!** ``` -------------------------------------------------------------------------------- /examples/claude-config.json: -------------------------------------------------------------------------------- ```json { "mcpServers": { "news-mcp": { "command": "node", "args": ["C:\\Users\\26214\\Desktop\\MyProject\\Financetoolshere\\news-mcp\\build\\index.js"] } } } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "node", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": true, "declarationMap": true, "sourceMap": true, "resolveJsonModule": true, "allowSyntheticDefaultImports": true }, "include": ["src/**/*"], "exclude": ["node_modules", "build"] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "news-mcp", "version": "1.0.0", "description": "A smart news MCP server with automatic API switching for reliable news fetching", "main": "build/index.js", "type": "module", "scripts": { "build": "tsc", "start": "node build/index.js", "dev": "tsc --watch", "sse": "npx supergateway --stdio \"node build/index.js\" --port 3100" }, "keywords": ["mcp", "news", "api", "smart-switching"], "author": { "name": "Xingyu Chen", "email": "[email protected]", "url": "https://github.com/guangxiangdebizi/" }, "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "0.6.0", "dotenv": "^16.3.1", "node-fetch": "^3.3.2" }, "devDependencies": { "@types/node": "^20.11.24", "typescript": "^5.3.3" }, "repository": { "type": "git", "url": "https://github.com/guangxiangdebizi/news-mcp.git" }, "homepage": "https://github.com/guangxiangdebizi/news-mcp#readme", "bugs": { "url": "https://github.com/guangxiangdebizi/news-mcp/issues" } } ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript import dotenv from 'dotenv'; // Load environment variables dotenv.config(); export interface NewsAPIConfig { name: string; apiKey: string; baseUrl: string; dailyLimit: number; priority: number; // Lower number = higher priority isActive: boolean; } export interface NewsAPIResponse { success: boolean; data?: any; error?: string; apiUsed?: string; remainingQuota?: number; } // API configurations with priority order export const NEWS_APIS: NewsAPIConfig[] = [ { name: 'TheNewsAPI', apiKey: process.env.THE_NEWS_API_KEY || '', baseUrl: 'https://api.thenewsapi.com/v1/news', dailyLimit: 999999, // Claimed unlimited priority: 1, isActive: !!process.env.THE_NEWS_API_KEY }, { name: 'NewsData.io', apiKey: process.env.NEWSDATA_IO_KEY || '', baseUrl: 'https://newsdata.io/api/1/news', dailyLimit: 200, // ~200 requests/day priority: 2, isActive: !!process.env.NEWSDATA_IO_KEY }, { name: 'NewsAPI.org', apiKey: process.env.NEWSAPI_ORG_KEY || '', baseUrl: 'https://newsapi.org/v2/everything', dailyLimit: 100, priority: 3, isActive: !!process.env.NEWSAPI_ORG_KEY }, { name: 'GNews', apiKey: process.env.GNEWS_API_KEY || '', baseUrl: 'https://gnews.io/api/v4/search', dailyLimit: 100, priority: 4, isActive: !!process.env.GNEWS_API_KEY }, { name: 'Twingly', apiKey: process.env.TWINGLY_API_KEY || '', baseUrl: 'https://api.twingly.com/blog/search/api/v3/search', dailyLimit: 50, // Estimated for trial priority: 5, isActive: !!process.env.TWINGLY_API_KEY } ]; // Get active APIs sorted by priority export function getActiveAPIs(): NewsAPIConfig[] { return NEWS_APIS .filter(api => api.isActive) .sort((a, b) => a.priority - b.priority); } // Check if any API is configured export function hasConfiguredAPIs(): boolean { return getActiveAPIs().length > 0; } // Get API configuration by name export function getAPIConfig(name: string): NewsAPIConfig | undefined { return NEWS_APIS.find(api => api.name === name); } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; // Import business tools import { newsSearch } from "./tools/newsSearch.js"; import { hasConfiguredAPIs, getActiveAPIs } from "./config.js"; // Create MCP server const server = new Server({ name: "news-mcp", version: "1.0.0", description: "Smart news search MCP server with automatic API switching for reliable news fetching" }, { capabilities: { tools: {} } }); // 🔸 Tool registration server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: newsSearch.name, description: newsSearch.description, inputSchema: newsSearch.parameters } ] }; }); // 🔸 Tool call handling server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case "search_news": return await newsSearch.run(request.params.arguments as { query: string; language?: string; limit?: number }); default: throw new Error(`Unknown tool: ${request.params.name}`); } }); // Server startup async function main() { try { const transport = new StdioServerTransport(); await server.connect(transport); // Log startup information console.error("🚀 News MCP Server started successfully!"); if (hasConfiguredAPIs()) { const activeAPIs = getActiveAPIs(); console.error(`📡 Active APIs: ${activeAPIs.map(api => api.name).join(', ')}`); console.error(`🔄 Smart switching enabled with ${activeAPIs.length} API(s)`); } else { console.error("⚠️ No API keys configured. Please add API keys to .env file."); } } catch (error) { console.error("❌ Failed to start News MCP Server:", error); process.exit(1); } } // Handle graceful shutdown process.on('SIGINT', () => { console.error("\n🛑 News MCP Server shutting down..."); process.exit(0); }); process.on('SIGTERM', () => { console.error("\n🛑 News MCP Server terminated..."); process.exit(0); }); main().catch((error) => { console.error("💥 Unhandled error:", error); process.exit(1); }); ``` -------------------------------------------------------------------------------- /src/tools/newsSearch.ts: -------------------------------------------------------------------------------- ```typescript import fetch from 'node-fetch'; import { getActiveAPIs, NewsAPIConfig, NewsAPIResponse } from '../config.js'; // Rate limiting tracker const apiUsageTracker = new Map<string, { count: number; lastReset: Date }>(); // Reset daily counters function resetDailyCounters() { const now = new Date(); for (const [apiName, usage] of apiUsageTracker.entries()) { const hoursSinceReset = (now.getTime() - usage.lastReset.getTime()) / (1000 * 60 * 60); if (hoursSinceReset >= 24) { apiUsageTracker.set(apiName, { count: 0, lastReset: now }); } } } // Check if API has remaining quota function hasRemainingQuota(apiConfig: NewsAPIConfig): boolean { resetDailyCounters(); const usage = apiUsageTracker.get(apiConfig.name); if (!usage) { apiUsageTracker.set(apiConfig.name, { count: 0, lastReset: new Date() }); return true; } return usage.count < apiConfig.dailyLimit; } // Increment API usage counter function incrementUsage(apiName: string) { const usage = apiUsageTracker.get(apiName); if (usage) { usage.count++; } else { apiUsageTracker.set(apiName, { count: 1, lastReset: new Date() }); } } // Get remaining quota for an API function getRemainingQuota(apiConfig: NewsAPIConfig): number { const usage = apiUsageTracker.get(apiConfig.name); if (!usage) return apiConfig.dailyLimit; return Math.max(0, apiConfig.dailyLimit - usage.count); } // Format news articles for consistent output function formatNewsResponse(data: any, apiName: string): string { let formattedNews = `# 📰 News Search Results (via ${apiName})\n\n`; try { let articles: any[] = []; // Handle different API response formats switch (apiName) { case 'NewsAPI.org': articles = data.articles || []; break; case 'GNews': articles = data.articles || []; break; case 'TheNewsAPI': articles = data.data || []; break; case 'NewsData.io': articles = data.results || []; break; case 'Twingly': articles = data.posts || []; break; default: articles = data.articles || data.data || data.results || []; } if (articles.length === 0) { return formattedNews + "❌ No articles found for your search query.\n"; } articles.slice(0, 10).forEach((article, index) => { const title = article.title || article.headline || 'No title'; const description = article.description || article.snippet || article.summary || 'No description'; const url = article.url || article.link || '#'; const publishedAt = article.publishedAt || article.published_at || article.published || 'Unknown date'; const source = article.source?.name || article.source || 'Unknown source'; formattedNews += `## ${index + 1}. ${title}\n\n`; formattedNews += `**Source:** ${source}\n`; formattedNews += `**Published:** ${publishedAt}\n`; formattedNews += `**Description:** ${description}\n`; formattedNews += `**URL:** [Read more](${url})\n\n`; formattedNews += "---\n\n"; }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; formattedNews += `❌ Error formatting news data: ${errorMessage}\n`; } return formattedNews; } // Search news using a specific API async function searchWithAPI(apiConfig: NewsAPIConfig, query: string, language: string = 'en', pageSize: number = 10): Promise<NewsAPIResponse> { try { let url = ''; let headers: any = {}; // Build API-specific request switch (apiConfig.name) { case 'NewsAPI.org': url = `${apiConfig.baseUrl}?q=${encodeURIComponent(query)}&language=${language}&pageSize=${pageSize}&apiKey=${apiConfig.apiKey}`; break; case 'GNews': url = `${apiConfig.baseUrl}?q=${encodeURIComponent(query)}&lang=${language}&max=${pageSize}&apikey=${apiConfig.apiKey}`; break; case 'TheNewsAPI': url = `${apiConfig.baseUrl}/all?api_token=${apiConfig.apiKey}&search=${encodeURIComponent(query)}&language=${language}&limit=${pageSize}`; break; case 'NewsData.io': url = `${apiConfig.baseUrl}?apikey=${apiConfig.apiKey}&q=${encodeURIComponent(query)}&language=${language}&size=${pageSize}`; break; case 'Twingly': url = `${apiConfig.baseUrl}?q=${encodeURIComponent(query)}&format=json&apikey=${apiConfig.apiKey}`; break; default: throw new Error(`Unsupported API: ${apiConfig.name}`); } const response = await fetch(url, { headers }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const data = await response.json(); // Check for API-specific error responses if ((data as any).status === 'error' || (data as any).error) { throw new Error((data as any).message || (data as any).error || 'API returned an error'); } incrementUsage(apiConfig.name); return { success: true, data, apiUsed: apiConfig.name, remainingQuota: getRemainingQuota(apiConfig) }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { success: false, error: errorMessage, apiUsed: apiConfig.name }; } } // Smart news search with automatic API switching export const newsSearch = { name: "search_news", description: "Search for news articles using multiple news APIs with automatic failover. The system intelligently switches between different news APIs when one reaches its limit or fails.", parameters: { type: "object", properties: { query: { type: "string", description: "Search query for news articles (keywords, topics, etc.)" }, language: { type: "string", description: "Language code for news articles (e.g., 'en', 'zh', 'es')", default: "en" }, limit: { type: "number", description: "Maximum number of articles to return (1-10)", minimum: 1, maximum: 10, default: 5 } }, required: ["query"] }, async run(args: { query: string; language?: string; limit?: number }) { try { // Validate input if (!args.query || args.query.trim().length === 0) { throw new Error("Search query cannot be empty"); } const query = args.query.trim(); const language = args.language || 'en'; const limit = Math.min(Math.max(args.limit || 5, 1), 10); // Get available APIs const activeAPIs = getActiveAPIs(); if (activeAPIs.length === 0) { return { content: [{ type: "text", text: "❌ **No News APIs Configured**\n\nPlease configure at least one news API key in your .env file. Available options:\n\n" + "- **NewsAPI.org**: Get free key at https://newsapi.org/register\n" + "- **GNews API**: Get free key at https://gnews.io/\n" + "- **TheNewsAPI**: Get free key at https://www.thenewsapi.com/\n" + "- **NewsData.io**: Get free key at https://newsdata.io/\n" + "- **Twingly**: Get trial key at https://www.twingly.com/news-api/\n\n" + "Add your API keys to the .env file and restart the service." }], isError: true }; } let lastError = ''; let attemptedAPIs: string[] = []; // Try each API in priority order for (const apiConfig of activeAPIs) { // Skip APIs that have reached their daily limit if (!hasRemainingQuota(apiConfig)) { attemptedAPIs.push(`${apiConfig.name} (quota exceeded)`); continue; } attemptedAPIs.push(apiConfig.name); const result = await searchWithAPI(apiConfig, query, language, limit); if (result.success && result.data) { const formattedResponse = formatNewsResponse(result.data, result.apiUsed!); const quotaInfo = `\n\n---\n\n**API Used:** ${result.apiUsed}\n**Remaining Quota:** ${result.remainingQuota} requests\n**Attempted APIs:** ${attemptedAPIs.join(', ')}`; return { content: [{ type: "text", text: formattedResponse + quotaInfo }] }; } else { lastError = result.error || 'Unknown error'; console.warn(`API ${apiConfig.name} failed: ${lastError}`); } } // All APIs failed return { content: [{ type: "text", text: `❌ **All News APIs Failed**\n\n` + `**Query:** "${query}"\n` + `**Attempted APIs:** ${attemptedAPIs.join(', ')}\n` + `**Last Error:** ${lastError}\n\n` + `**Suggestions:**\n` + `- Check your API keys in the .env file\n` + `- Verify your internet connection\n` + `- Try a different search query\n` + `- Some APIs may have temporary outages` }], isError: true }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: "text", text: `❌ **Search Failed:** ${errorMessage}` }], isError: true }; } } }; ```