UNPKG

mcp-duckduckgo-search

Version:
175 lines (174 loc) 7.42 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { readFileSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')); const { name, version } = pkg; // SerpAPI configuration const SERPAPI_BASE_URL = 'https://serpapi.com/search.json'; const SERPAPI_KEY = process.env.SERPAPI_API_KEY || ''; if (!SERPAPI_KEY) { throw new Error('SERPAPI_API_KEY environment variable is required'); } // Assert SERPAPI_KEY is string since we check for existence const API_KEY = SERPAPI_KEY; // Import cache singleton import { search_cache } from './cache.js'; // Import response formatter import { format_response } from './formatters.js'; // Import configuration import { SAFE_SEARCH_LEVELS } from './config.js'; class DuckDuckGoServer { constructor() { this.server = new Server({ name, version }, { capabilities: { tools: {}, }, }); this.setup_tool_handlers(); } setup_tool_handlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'ddg_search', description: 'Search the web using DuckDuckGo API', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query', }, region: { type: 'string', description: 'Region code (e.g., us-en, uk-en)', default: 'us-en', }, safe_search: { type: 'string', description: 'Safe search level (off, moderate, strict)', enum: ['off', 'moderate', 'strict'], default: 'moderate', }, date_filter: { type: 'string', description: 'Filter results by date (d: day, w: week, m: month, y: year, or custom range like 2023-01-01..2023-12-31)', pattern: '^([dwmy]|\\d{4}-\\d{2}-\\d{2}\\.\\.\\d{4}-\\d{2}-\\d{2})$', }, start: { type: 'number', description: 'Result offset for pagination', minimum: 0, }, no_cache: { type: 'boolean', description: 'Bypass cache and fetch fresh results', default: false, }, }, required: ['query'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== 'ddg_search') { throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } const { query, region = 'us-en', safe_search = 'moderate', date_filter, start, no_cache = false, } = request.params.arguments; try { // Check cache first if not explicitly bypassed if (!no_cache) { const cache_key = JSON.stringify({ query, region, safe_search, date_filter, start, }); const cached_result = search_cache.get(cache_key); if (cached_result) { const formatted = format_response(cached_result); return { ...formatted, _meta: {}, }; } } const params = { engine: 'duckduckgo', q: query, kl: region, safe: SAFE_SEARCH_LEVELS[safe_search], api_key: API_KEY, }; // Add date filter if provided if (date_filter) { params.df = date_filter; } // Add pagination if provided if (start !== undefined) { params.start = start.toString(); } const search_params = new URLSearchParams(params); const controller = new AbortController(); const timeout_id = setTimeout(() => controller.abort(), 30000); // 30 second timeout try { const api_response = await fetch(`${SERPAPI_BASE_URL}?${search_params.toString()}`, { method: 'GET', signal: controller.signal, }); if (!api_response.ok) { throw new McpError(ErrorCode.InternalError, `Search API error: ${api_response.statusText}`); } const data = await api_response.json(); // Cache the response if caching wasn't explicitly disabled if (!no_cache) { const cache_key = JSON.stringify({ query, region, safe_search, date_filter, start, }); search_cache.set(cache_key, data); } const formatted = format_response(data); return { ...formatted, _meta: {}, }; } finally { clearTimeout(timeout_id); } } catch (error) { return { content: [ { type: 'text', text: `Error performing search: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('DuckDuckGo MCP server running on stdio'); } } const server = new DuckDuckGoServer(); server.run().catch(console.error);