#
tokens: 8368/50000 9/9 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
  4 | [![TypeScript](https://img.shields.io/badge/TypeScript-5.3+-blue.svg)](https://www.typescriptlang.org/)
  5 | [![MCP](https://img.shields.io/badge/MCP-0.6.0-green.svg)](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 | };
```