# 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: -------------------------------------------------------------------------------- ``` 1 | # News API Keys - Add your API keys here 2 | # You can get free API keys from the following services: 3 | 4 | # NewsAPI.org - 100 requests/day 5 | # Get your key at: https://newsapi.org/register 6 | NEWSAPI_ORG_KEY=your_newsapi_org_key_here 7 | 8 | # GNews API - 100 requests/day 9 | # Get your key at: https://gnews.io/ 10 | GNEWS_API_KEY=your_gnews_api_key_here 11 | 12 | # TheNewsAPI.com - Unlimited (claimed) 13 | # Get your key at: https://www.thenewsapi.com/ 14 | THE_NEWS_API_KEY=your_the_news_api_key_here 15 | 16 | # NewsData.io - ~200 requests/day 17 | # Get your key at: https://newsdata.io/ 18 | NEWSDATA_IO_KEY=your_newsdata_io_key_here 19 | 20 | # Twingly News API - Trial available 21 | # Get your key at: https://www.twingly.com/news-api/ 22 | TWINGLY_API_KEY=your_twingly_api_key_here ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | 8 | # Build output 9 | build/ 10 | dist/ 11 | *.tsbuildinfo 12 | 13 | # Environment variables 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | # IDE files 21 | .vscode/ 22 | .idea/ 23 | *.swp 24 | *.swo 25 | *~ 26 | 27 | # OS generated files 28 | .DS_Store 29 | .DS_Store? 30 | ._* 31 | .Spotlight-V100 32 | .Trashes 33 | ehthumbs.db 34 | Thumbs.db 35 | 36 | # Logs 37 | logs 38 | *.log 39 | 40 | # Runtime data 41 | pids 42 | *.pid 43 | *.seed 44 | *.pid.lock 45 | 46 | # Coverage directory used by tools like istanbul 47 | coverage/ 48 | *.lcov 49 | 50 | # nyc test coverage 51 | .nyc_output 52 | 53 | # Dependency directories 54 | jspm_packages/ 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | 87 | # Gatsby files 88 | .cache/ 89 | public 90 | 91 | # Storybook build outputs 92 | .out 93 | .storybook-out 94 | 95 | # Temporary folders 96 | tmp/ 97 | temp/ ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # 📰 News MCP Server 2 | 3 | [](https://opensource.org/licenses/Apache-2.0) 4 | [](https://www.typescriptlang.org/) 5 | [](https://modelcontextprotocol.io/) 6 | 7 | A smart news search MCP (Model Context Protocol) server with **automatic API switching** for reliable news fetching. Never worry about API limits again! 8 | 9 | <a href="https://glama.ai/mcp/servers/@guangxiangdebizi/news-mcp"> 10 | <img width="380" height="200" src="https://glama.ai/mcp/servers/@guangxiangdebizi/news-mcp/badge" alt="News Server MCP server" /> 11 | </a> 12 | 13 | ## ✨ Features 14 | 15 | - 🔄 **Smart API Switching**: Automatically switches between multiple news APIs when one reaches its limit 16 | - 🆓 **Multiple Free APIs**: Supports 5+ free news APIs with generous daily quotas 17 | - 📊 **Quota Management**: Intelligent tracking of daily API usage limits 18 | - 🌍 **Multi-language Support**: Search news in 20+ languages 19 | - ⚡ **Fast & Reliable**: Failover mechanism ensures you always get results 20 | - 🛠️ **Easy Setup**: Simple configuration with environment variables 21 | 22 | ## 🚀 Quick Start 23 | 24 | ### 1. Installation 25 | 26 | ```bash 27 | # Clone the repository 28 | git clone https://github.com/guangxiangdebizi/news-mcp.git 29 | cd news-mcp 30 | 31 | # Install dependencies 32 | npm install 33 | 34 | # Copy environment template 35 | cp .env.example .env 36 | ``` 37 | 38 | ### 2. API Configuration 39 | 40 | Get free API keys from any of these services (you only need one, but more = better reliability): 41 | 42 | | Service | Daily Limit | Sign Up Link | Priority | 43 | |---------|-------------|--------------|----------| 44 | | **TheNewsAPI** | Unlimited* | [Get Key](https://www.thenewsapi.com/) | 🥇 Highest | 45 | | **NewsData.io** | ~200 requests | [Get Key](https://newsdata.io/) | 🥈 High | 46 | | **NewsAPI.org** | 100 requests | [Get Key](https://newsapi.org/register) | 🥉 Medium | 47 | | **GNews** | 100 requests | [Get Key](https://gnews.io/) | 🏅 Medium | 48 | | **Twingly** | Trial available | [Get Key](https://www.twingly.com/news-api/) | 🎖️ Low | 49 | 50 | *\*Claimed unlimited for free tier* 51 | 52 | ### 3. Configure Environment 53 | 54 | Edit `.env` file and add your API keys: 55 | 56 | ```env 57 | # Add at least one API key (more is better for reliability) 58 | THE_NEWS_API_KEY=your_api_key_here 59 | NEWSDATA_IO_KEY=your_api_key_here 60 | NEWSAPI_ORG_KEY=your_api_key_here 61 | GNEWS_API_KEY=your_api_key_here 62 | TWINGLY_API_KEY=your_api_key_here 63 | ``` 64 | 65 | ### 4. Build & Run 66 | 67 | ```bash 68 | # Build the project 69 | npm run build 70 | 71 | # Start the MCP server 72 | npm start 73 | 74 | # Or run in development mode 75 | npm run dev 76 | ``` 77 | 78 | ## 🔧 Usage 79 | 80 | ### With Claude Desktop 81 | 82 | Add to your Claude Desktop configuration: 83 | 84 | **Stdio Mode** (Local Development): 85 | ```json 86 | { 87 | "mcpServers": { 88 | "news-mcp": { 89 | "command": "node", 90 | "args": ["path/to/news-mcp/build/index.js"] 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | **SSE Mode** (Web Access): 97 | ```bash 98 | # Start SSE server 99 | npm run sse 100 | ``` 101 | 102 | ```json 103 | { 104 | "mcpServers": { 105 | "news-mcp": { 106 | "type": "sse", 107 | "url": "http://localhost:3100/sse", 108 | "timeout": 600 109 | } 110 | } 111 | } 112 | ``` 113 | 114 | ### Available Tools 115 | 116 | #### `search_news` 117 | 118 | Search for news articles with automatic API switching. 119 | 120 | **Parameters:** 121 | - `query` (required): Search keywords or topics 122 | - `language` (optional): Language code (default: "en") 123 | - `limit` (optional): Number of articles (1-10, default: 5) 124 | 125 | **Example:** 126 | ```typescript 127 | // Search for technology news 128 | { 129 | "query": "artificial intelligence", 130 | "language": "en", 131 | "limit": 5 132 | } 133 | 134 | // Search for Chinese news 135 | { 136 | "query": "科技新闻", 137 | "language": "zh", 138 | "limit": 3 139 | } 140 | ``` 141 | 142 | ## 🧠 How Smart Switching Works 143 | 144 | 1. **Priority Order**: APIs are tried in order of reliability and quota limits 145 | 2. **Quota Tracking**: System tracks daily usage for each API 146 | 3. **Automatic Failover**: When an API fails or reaches limit, automatically tries the next one 147 | 4. **Success Guarantee**: Continues until successful response or all APIs exhausted 148 | 149 | ``` 150 | TheNewsAPI (∞) → NewsData.io (200) → NewsAPI.org (100) → GNews (100) → Twingly (50) 151 | ``` 152 | 153 | ## 📊 Supported Languages 154 | 155 | - **English** (en) - All APIs 156 | - **Chinese** (zh) - Most APIs 157 | - **Spanish** (es) - Most APIs 158 | - **French** (fr) - Most APIs 159 | - **German** (de) - Most APIs 160 | - **And 15+ more languages** 161 | 162 | ## 🛠️ Development 163 | 164 | ### Project Structure 165 | 166 | ``` 167 | src/ 168 | ├── index.ts # MCP server entry point 169 | ├── config.ts # API configuration management 170 | └── tools/ 171 | └── newsSearch.ts # Smart news search tool 172 | ``` 173 | 174 | ### Scripts 175 | 176 | ```bash 177 | npm run build # Build TypeScript 178 | npm run dev # Development mode with watch 179 | npm start # Start built server 180 | npm run sse # Start SSE server on port 3100 181 | ``` 182 | 183 | ### Adding New APIs 184 | 185 | 1. Add API configuration to `src/config.ts` 186 | 2. Implement API-specific request logic in `src/tools/newsSearch.ts` 187 | 3. Add environment variable to `.env.example` 188 | 189 | ## 🤝 Contributing 190 | 191 | Contributions are welcome! Please feel free to submit a Pull Request. 192 | 193 | 1. Fork the repository 194 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 195 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 196 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 197 | 5. Open a Pull Request 198 | 199 | ## 📝 License 200 | 201 | This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. 202 | 203 | ## 👨💻 Author 204 | 205 | **Xingyu Chen** 206 | - 🌐 Website: [GitHub Profile](https://github.com/guangxiangdebizi/) 207 | - 📧 Email: [email protected] 208 | - 💼 LinkedIn: [Xingyu Chen](https://www.linkedin.com/in/xingyu-chen-b5b3b0313/) 209 | - 📦 NPM: [@xingyuchen](https://www.npmjs.com/~xingyuchen) 210 | 211 | ## 🙏 Acknowledgments 212 | 213 | - [Model Context Protocol](https://modelcontextprotocol.io/) for the amazing MCP framework 214 | - All the news API providers for their generous free tiers 215 | - The open-source community for inspiration and support 216 | 217 | --- 218 | 219 | **⭐ If this project helped you, please give it a star!** ``` -------------------------------------------------------------------------------- /examples/claude-config.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "mcpServers": { 3 | "news-mcp": { 4 | "command": "node", 5 | "args": ["C:\\Users\\26214\\Desktop\\MyProject\\Financetoolshere\\news-mcp\\build\\index.js"] 6 | } 7 | } 8 | } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "declaration": true, 13 | "declarationMap": true, 14 | "sourceMap": true, 15 | "resolveJsonModule": true, 16 | "allowSyntheticDefaultImports": true 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "build"] 20 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "news-mcp", 3 | "version": "1.0.0", 4 | "description": "A smart news MCP server with automatic API switching for reliable news fetching", 5 | "main": "build/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "node build/index.js", 10 | "dev": "tsc --watch", 11 | "sse": "npx supergateway --stdio \"node build/index.js\" --port 3100" 12 | }, 13 | "keywords": ["mcp", "news", "api", "smart-switching"], 14 | "author": { 15 | "name": "Xingyu Chen", 16 | "email": "[email protected]", 17 | "url": "https://github.com/guangxiangdebizi/" 18 | }, 19 | "license": "Apache-2.0", 20 | "dependencies": { 21 | "@modelcontextprotocol/sdk": "0.6.0", 22 | "dotenv": "^16.3.1", 23 | "node-fetch": "^3.3.2" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^20.11.24", 27 | "typescript": "^5.3.3" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/guangxiangdebizi/news-mcp.git" 32 | }, 33 | "homepage": "https://github.com/guangxiangdebizi/news-mcp#readme", 34 | "bugs": { 35 | "url": "https://github.com/guangxiangdebizi/news-mcp/issues" 36 | } 37 | } ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import dotenv from 'dotenv'; 2 | 3 | // Load environment variables 4 | dotenv.config(); 5 | 6 | export interface NewsAPIConfig { 7 | name: string; 8 | apiKey: string; 9 | baseUrl: string; 10 | dailyLimit: number; 11 | priority: number; // Lower number = higher priority 12 | isActive: boolean; 13 | } 14 | 15 | export interface NewsAPIResponse { 16 | success: boolean; 17 | data?: any; 18 | error?: string; 19 | apiUsed?: string; 20 | remainingQuota?: number; 21 | } 22 | 23 | // API configurations with priority order 24 | export const NEWS_APIS: NewsAPIConfig[] = [ 25 | { 26 | name: 'TheNewsAPI', 27 | apiKey: process.env.THE_NEWS_API_KEY || '', 28 | baseUrl: 'https://api.thenewsapi.com/v1/news', 29 | dailyLimit: 999999, // Claimed unlimited 30 | priority: 1, 31 | isActive: !!process.env.THE_NEWS_API_KEY 32 | }, 33 | { 34 | name: 'NewsData.io', 35 | apiKey: process.env.NEWSDATA_IO_KEY || '', 36 | baseUrl: 'https://newsdata.io/api/1/news', 37 | dailyLimit: 200, // ~200 requests/day 38 | priority: 2, 39 | isActive: !!process.env.NEWSDATA_IO_KEY 40 | }, 41 | { 42 | name: 'NewsAPI.org', 43 | apiKey: process.env.NEWSAPI_ORG_KEY || '', 44 | baseUrl: 'https://newsapi.org/v2/everything', 45 | dailyLimit: 100, 46 | priority: 3, 47 | isActive: !!process.env.NEWSAPI_ORG_KEY 48 | }, 49 | { 50 | name: 'GNews', 51 | apiKey: process.env.GNEWS_API_KEY || '', 52 | baseUrl: 'https://gnews.io/api/v4/search', 53 | dailyLimit: 100, 54 | priority: 4, 55 | isActive: !!process.env.GNEWS_API_KEY 56 | }, 57 | { 58 | name: 'Twingly', 59 | apiKey: process.env.TWINGLY_API_KEY || '', 60 | baseUrl: 'https://api.twingly.com/blog/search/api/v3/search', 61 | dailyLimit: 50, // Estimated for trial 62 | priority: 5, 63 | isActive: !!process.env.TWINGLY_API_KEY 64 | } 65 | ]; 66 | 67 | // Get active APIs sorted by priority 68 | export function getActiveAPIs(): NewsAPIConfig[] { 69 | return NEWS_APIS 70 | .filter(api => api.isActive) 71 | .sort((a, b) => a.priority - b.priority); 72 | } 73 | 74 | // Check if any API is configured 75 | export function hasConfiguredAPIs(): boolean { 76 | return getActiveAPIs().length > 0; 77 | } 78 | 79 | // Get API configuration by name 80 | export function getAPIConfig(name: string): NewsAPIConfig | undefined { 81 | return NEWS_APIS.find(api => api.name === name); 82 | } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; 4 | 5 | // Import business tools 6 | import { newsSearch } from "./tools/newsSearch.js"; 7 | import { hasConfiguredAPIs, getActiveAPIs } from "./config.js"; 8 | 9 | // Create MCP server 10 | const server = new Server({ 11 | name: "news-mcp", 12 | version: "1.0.0", 13 | description: "Smart news search MCP server with automatic API switching for reliable news fetching" 14 | }, { 15 | capabilities: { tools: {} } 16 | }); 17 | 18 | // 🔸 Tool registration 19 | server.setRequestHandler(ListToolsRequestSchema, async () => { 20 | return { 21 | tools: [ 22 | { 23 | name: newsSearch.name, 24 | description: newsSearch.description, 25 | inputSchema: newsSearch.parameters 26 | } 27 | ] 28 | }; 29 | }); 30 | 31 | // 🔸 Tool call handling 32 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 33 | switch (request.params.name) { 34 | case "search_news": 35 | return await newsSearch.run(request.params.arguments as { query: string; language?: string; limit?: number }); 36 | default: 37 | throw new Error(`Unknown tool: ${request.params.name}`); 38 | } 39 | }); 40 | 41 | // Server startup 42 | async function main() { 43 | try { 44 | const transport = new StdioServerTransport(); 45 | await server.connect(transport); 46 | 47 | // Log startup information 48 | console.error("🚀 News MCP Server started successfully!"); 49 | 50 | if (hasConfiguredAPIs()) { 51 | const activeAPIs = getActiveAPIs(); 52 | console.error(`📡 Active APIs: ${activeAPIs.map(api => api.name).join(', ')}`); 53 | console.error(`🔄 Smart switching enabled with ${activeAPIs.length} API(s)`); 54 | } else { 55 | console.error("⚠️ No API keys configured. Please add API keys to .env file."); 56 | } 57 | 58 | } catch (error) { 59 | console.error("❌ Failed to start News MCP Server:", error); 60 | process.exit(1); 61 | } 62 | } 63 | 64 | // Handle graceful shutdown 65 | process.on('SIGINT', () => { 66 | console.error("\n🛑 News MCP Server shutting down..."); 67 | process.exit(0); 68 | }); 69 | 70 | process.on('SIGTERM', () => { 71 | console.error("\n🛑 News MCP Server terminated..."); 72 | process.exit(0); 73 | }); 74 | 75 | main().catch((error) => { 76 | console.error("💥 Unhandled error:", error); 77 | process.exit(1); 78 | }); ``` -------------------------------------------------------------------------------- /src/tools/newsSearch.ts: -------------------------------------------------------------------------------- ```typescript 1 | import fetch from 'node-fetch'; 2 | import { getActiveAPIs, NewsAPIConfig, NewsAPIResponse } from '../config.js'; 3 | 4 | // Rate limiting tracker 5 | const apiUsageTracker = new Map<string, { count: number; lastReset: Date }>(); 6 | 7 | // Reset daily counters 8 | function resetDailyCounters() { 9 | const now = new Date(); 10 | for (const [apiName, usage] of apiUsageTracker.entries()) { 11 | const hoursSinceReset = (now.getTime() - usage.lastReset.getTime()) / (1000 * 60 * 60); 12 | if (hoursSinceReset >= 24) { 13 | apiUsageTracker.set(apiName, { count: 0, lastReset: now }); 14 | } 15 | } 16 | } 17 | 18 | // Check if API has remaining quota 19 | function hasRemainingQuota(apiConfig: NewsAPIConfig): boolean { 20 | resetDailyCounters(); 21 | const usage = apiUsageTracker.get(apiConfig.name); 22 | if (!usage) { 23 | apiUsageTracker.set(apiConfig.name, { count: 0, lastReset: new Date() }); 24 | return true; 25 | } 26 | return usage.count < apiConfig.dailyLimit; 27 | } 28 | 29 | // Increment API usage counter 30 | function incrementUsage(apiName: string) { 31 | const usage = apiUsageTracker.get(apiName); 32 | if (usage) { 33 | usage.count++; 34 | } else { 35 | apiUsageTracker.set(apiName, { count: 1, lastReset: new Date() }); 36 | } 37 | } 38 | 39 | // Get remaining quota for an API 40 | function getRemainingQuota(apiConfig: NewsAPIConfig): number { 41 | const usage = apiUsageTracker.get(apiConfig.name); 42 | if (!usage) return apiConfig.dailyLimit; 43 | return Math.max(0, apiConfig.dailyLimit - usage.count); 44 | } 45 | 46 | // Format news articles for consistent output 47 | function formatNewsResponse(data: any, apiName: string): string { 48 | let formattedNews = `# 📰 News Search Results (via ${apiName})\n\n`; 49 | 50 | try { 51 | let articles: any[] = []; 52 | 53 | // Handle different API response formats 54 | switch (apiName) { 55 | case 'NewsAPI.org': 56 | articles = data.articles || []; 57 | break; 58 | case 'GNews': 59 | articles = data.articles || []; 60 | break; 61 | case 'TheNewsAPI': 62 | articles = data.data || []; 63 | break; 64 | case 'NewsData.io': 65 | articles = data.results || []; 66 | break; 67 | case 'Twingly': 68 | articles = data.posts || []; 69 | break; 70 | default: 71 | articles = data.articles || data.data || data.results || []; 72 | } 73 | 74 | if (articles.length === 0) { 75 | return formattedNews + "❌ No articles found for your search query.\n"; 76 | } 77 | 78 | articles.slice(0, 10).forEach((article, index) => { 79 | const title = article.title || article.headline || 'No title'; 80 | const description = article.description || article.snippet || article.summary || 'No description'; 81 | const url = article.url || article.link || '#'; 82 | const publishedAt = article.publishedAt || article.published_at || article.published || 'Unknown date'; 83 | const source = article.source?.name || article.source || 'Unknown source'; 84 | 85 | formattedNews += `## ${index + 1}. ${title}\n\n`; 86 | formattedNews += `**Source:** ${source}\n`; 87 | formattedNews += `**Published:** ${publishedAt}\n`; 88 | formattedNews += `**Description:** ${description}\n`; 89 | formattedNews += `**URL:** [Read more](${url})\n\n`; 90 | formattedNews += "---\n\n"; 91 | }); 92 | 93 | } catch (error) { 94 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 95 | formattedNews += `❌ Error formatting news data: ${errorMessage}\n`; 96 | } 97 | 98 | return formattedNews; 99 | } 100 | 101 | // Search news using a specific API 102 | async function searchWithAPI(apiConfig: NewsAPIConfig, query: string, language: string = 'en', pageSize: number = 10): Promise<NewsAPIResponse> { 103 | try { 104 | let url = ''; 105 | let headers: any = {}; 106 | 107 | // Build API-specific request 108 | switch (apiConfig.name) { 109 | case 'NewsAPI.org': 110 | url = `${apiConfig.baseUrl}?q=${encodeURIComponent(query)}&language=${language}&pageSize=${pageSize}&apiKey=${apiConfig.apiKey}`; 111 | break; 112 | 113 | case 'GNews': 114 | url = `${apiConfig.baseUrl}?q=${encodeURIComponent(query)}&lang=${language}&max=${pageSize}&apikey=${apiConfig.apiKey}`; 115 | break; 116 | 117 | case 'TheNewsAPI': 118 | url = `${apiConfig.baseUrl}/all?api_token=${apiConfig.apiKey}&search=${encodeURIComponent(query)}&language=${language}&limit=${pageSize}`; 119 | break; 120 | 121 | case 'NewsData.io': 122 | url = `${apiConfig.baseUrl}?apikey=${apiConfig.apiKey}&q=${encodeURIComponent(query)}&language=${language}&size=${pageSize}`; 123 | break; 124 | 125 | case 'Twingly': 126 | url = `${apiConfig.baseUrl}?q=${encodeURIComponent(query)}&format=json&apikey=${apiConfig.apiKey}`; 127 | break; 128 | 129 | default: 130 | throw new Error(`Unsupported API: ${apiConfig.name}`); 131 | } 132 | 133 | const response = await fetch(url, { headers }); 134 | 135 | if (!response.ok) { 136 | const errorText = await response.text(); 137 | throw new Error(`HTTP ${response.status}: ${errorText}`); 138 | } 139 | 140 | const data = await response.json(); 141 | 142 | // Check for API-specific error responses 143 | if ((data as any).status === 'error' || (data as any).error) { 144 | throw new Error((data as any).message || (data as any).error || 'API returned an error'); 145 | } 146 | 147 | incrementUsage(apiConfig.name); 148 | 149 | return { 150 | success: true, 151 | data, 152 | apiUsed: apiConfig.name, 153 | remainingQuota: getRemainingQuota(apiConfig) 154 | }; 155 | 156 | } catch (error) { 157 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 158 | return { 159 | success: false, 160 | error: errorMessage, 161 | apiUsed: apiConfig.name 162 | }; 163 | } 164 | } 165 | 166 | // Smart news search with automatic API switching 167 | export const newsSearch = { 168 | name: "search_news", 169 | 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.", 170 | parameters: { 171 | type: "object", 172 | properties: { 173 | query: { 174 | type: "string", 175 | description: "Search query for news articles (keywords, topics, etc.)" 176 | }, 177 | language: { 178 | type: "string", 179 | description: "Language code for news articles (e.g., 'en', 'zh', 'es')", 180 | default: "en" 181 | }, 182 | limit: { 183 | type: "number", 184 | description: "Maximum number of articles to return (1-10)", 185 | minimum: 1, 186 | maximum: 10, 187 | default: 5 188 | } 189 | }, 190 | required: ["query"] 191 | }, 192 | 193 | async run(args: { query: string; language?: string; limit?: number }) { 194 | try { 195 | // Validate input 196 | if (!args.query || args.query.trim().length === 0) { 197 | throw new Error("Search query cannot be empty"); 198 | } 199 | 200 | const query = args.query.trim(); 201 | const language = args.language || 'en'; 202 | const limit = Math.min(Math.max(args.limit || 5, 1), 10); 203 | 204 | // Get available APIs 205 | const activeAPIs = getActiveAPIs(); 206 | 207 | if (activeAPIs.length === 0) { 208 | return { 209 | content: [{ 210 | type: "text", 211 | text: "❌ **No News APIs Configured**\n\nPlease configure at least one news API key in your .env file. Available options:\n\n" + 212 | "- **NewsAPI.org**: Get free key at https://newsapi.org/register\n" + 213 | "- **GNews API**: Get free key at https://gnews.io/\n" + 214 | "- **TheNewsAPI**: Get free key at https://www.thenewsapi.com/\n" + 215 | "- **NewsData.io**: Get free key at https://newsdata.io/\n" + 216 | "- **Twingly**: Get trial key at https://www.twingly.com/news-api/\n\n" + 217 | "Add your API keys to the .env file and restart the service." 218 | }], 219 | isError: true 220 | }; 221 | } 222 | 223 | let lastError = ''; 224 | let attemptedAPIs: string[] = []; 225 | 226 | // Try each API in priority order 227 | for (const apiConfig of activeAPIs) { 228 | // Skip APIs that have reached their daily limit 229 | if (!hasRemainingQuota(apiConfig)) { 230 | attemptedAPIs.push(`${apiConfig.name} (quota exceeded)`); 231 | continue; 232 | } 233 | 234 | attemptedAPIs.push(apiConfig.name); 235 | 236 | const result = await searchWithAPI(apiConfig, query, language, limit); 237 | 238 | if (result.success && result.data) { 239 | const formattedResponse = formatNewsResponse(result.data, result.apiUsed!); 240 | const quotaInfo = `\n\n---\n\n**API Used:** ${result.apiUsed}\n**Remaining Quota:** ${result.remainingQuota} requests\n**Attempted APIs:** ${attemptedAPIs.join(', ')}`; 241 | 242 | return { 243 | content: [{ 244 | type: "text", 245 | text: formattedResponse + quotaInfo 246 | }] 247 | }; 248 | } else { 249 | lastError = result.error || 'Unknown error'; 250 | console.warn(`API ${apiConfig.name} failed: ${lastError}`); 251 | } 252 | } 253 | 254 | // All APIs failed 255 | return { 256 | content: [{ 257 | type: "text", 258 | text: `❌ **All News APIs Failed**\n\n` + 259 | `**Query:** "${query}"\n` + 260 | `**Attempted APIs:** ${attemptedAPIs.join(', ')}\n` + 261 | `**Last Error:** ${lastError}\n\n` + 262 | `**Suggestions:**\n` + 263 | `- Check your API keys in the .env file\n` + 264 | `- Verify your internet connection\n` + 265 | `- Try a different search query\n` + 266 | `- Some APIs may have temporary outages` 267 | }], 268 | isError: true 269 | }; 270 | 271 | } catch (error) { 272 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 273 | return { 274 | content: [{ 275 | type: "text", 276 | text: `❌ **Search Failed:** ${errorMessage}` 277 | }], 278 | isError: true 279 | }; 280 | } 281 | } 282 | }; ```