#
tokens: 6293/50000 9/9 files
lines: off (toggle) GitHub
raw markdown copy
# 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

[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.3+-blue.svg)](https://www.typescriptlang.org/)
[![MCP](https://img.shields.io/badge/MCP-0.6.0-green.svg)](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
      };
    }
  }
};
```