DuckDuckGo MCP Server

  • src
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 * as DDG from "duck-duck-scrape"; interface DuckDuckGoSearchArgs { query: string; count?: number; safeSearch?: "strict" | "moderate" | "off"; } interface SearchResult { title: string; description: string; url: string; } interface RateLimit { perSecond: number; perMonth: number; } interface RequestCount { second: number; month: number; lastReset: number; } const CONFIG = { server: { name: "zhsama/duckduckgo-mpc-server", version: "0.1.2", }, rateLimit: { perSecond: 1, perMonth: 15000, } as RateLimit, search: { maxQueryLength: 400, maxResults: 20, defaultResults: 10, defaultSafeSearch: "moderate" as const, }, } as const; const WEB_SEARCH_TOOL = { name: "duckduckgo_web_search", description: "Performs a web search using the DuckDuckGo, ideal for general queries, news, articles, and online content. " + "Use this for broad information gathering, recent events, or when you need diverse web sources. " + "Supports content filtering and region-specific searches. " + `Maximum ${CONFIG.search.maxResults} results per request.`, inputSchema: { type: "object", properties: { query: { type: "string", description: `Search query (max ${CONFIG.search.maxQueryLength} chars)`, maxLength: CONFIG.search.maxQueryLength, }, count: { type: "number", description: `Number of results (1-${CONFIG.search.maxResults}, default ${CONFIG.search.defaultResults})`, minimum: 1, maximum: CONFIG.search.maxResults, default: CONFIG.search.defaultResults, }, safeSearch: { type: "string", description: "SafeSearch level (strict, moderate, off)", enum: ["strict", "moderate", "off"], default: CONFIG.search.defaultSafeSearch, }, }, required: ["query"], }, }; const server = new Server(CONFIG.server, { capabilities: { tools: {}, }, }); // 速率限制状态 let requestCount: RequestCount = { second: 0, month: 0, lastReset: Date.now(), }; /** * 检查并更新速率限制 * @throws {Error} 当超过速率限制时抛出错误 */ function checkRateLimit(): void { const now = Date.now(); console.error(`[DEBUG] Rate limit check - Current counts:`, requestCount); // 重置每秒计数器 if (now - requestCount.lastReset > 1000) { requestCount.second = 0; requestCount.lastReset = now; } // 检查限制 if ( requestCount.second >= CONFIG.rateLimit.perSecond || requestCount.month >= CONFIG.rateLimit.perMonth ) { const error = new Error("Rate limit exceeded"); console.error("[ERROR] Rate limit exceeded:", requestCount); throw error; } // 更新计数器 requestCount.second++; requestCount.month++; } /** * 类型守卫:检查参数是否符合 DuckDuckGoSearchArgs 接口 */ function isDuckDuckGoWebSearchArgs( args: unknown ): args is DuckDuckGoSearchArgs { if (typeof args !== "object" || args === null) { return false; } const { query } = args as Partial<DuckDuckGoSearchArgs>; if (typeof query !== "string") { return false; } if (query.length > CONFIG.search.maxQueryLength) { return false; } return true; } /** * 执行网络搜索 * @param query 搜索查询 * @param count 结果数量 * @param safeSearch 安全搜索级别 * @returns 格式化的搜索结果 */ async function performWebSearch( query: string, count: number = CONFIG.search.defaultResults, safeSearch: "strict" | "moderate" | "off" = CONFIG.search.defaultSafeSearch ): Promise<string> { console.error( `[DEBUG] Performing search - Query: "${query}", Count: ${count}, SafeSearch: ${safeSearch}` ); try { checkRateLimit(); const safeSearchMap = { strict: DDG.SafeSearchType.STRICT, moderate: DDG.SafeSearchType.MODERATE, off: DDG.SafeSearchType.OFF, }; const searchResults = await DDG.search(query, { safeSearch: safeSearchMap[safeSearch], }); if (searchResults.noResults) { console.error(`[INFO] No results found for query: "${query}"`); return `# DuckDuckGo 搜索结果\n没有找到与 "${query}" 相关的结果。`; } const results: SearchResult[] = searchResults.results .slice(0, count) .map((result: DDG.SearchResult) => ({ title: result.title, description: result.description || result.title, url: result.url, })); console.error( `[INFO] Found ${results.length} results for query: "${query}"` ); // 格式化结果 return formatSearchResults(query, results); } catch (error) { console.error(`[ERROR] Search failed - Query: "${query}"`, error); throw error; } } /** * 格式化搜索结果为 Markdown */ function formatSearchResults(query: string, results: SearchResult[]): string { const formattedResults = results .map((r: SearchResult) => { return `### ${r.title} ${r.description} 🔗 [阅读更多](${r.url}) `; }) .join("\n\n"); return `# DuckDuckGo 搜索结果 ${query} 的搜索结果(${results.length}件) --- ${formattedResults} `; } // 工具处理器 server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [WEB_SEARCH_TOOL], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { console.error( `[DEBUG] Received tool call request:`, JSON.stringify(request.params, null, 2) ); const { name, arguments: args } = request.params; if (!args) { throw new Error("No arguments provided"); } switch (name) { case "duckduckgo_web_search": { if (!isDuckDuckGoWebSearchArgs(args)) { throw new Error("Invalid arguments for duckduckgo_web_search"); } const { query, count = CONFIG.search.defaultResults, safeSearch = CONFIG.search.defaultSafeSearch, } = args; const results = await performWebSearch(query, count, safeSearch); return { content: [{ type: "text", text: results }], isError: false, }; } default: { console.error(`[ERROR] Unknown tool requested: ${name}`); return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true, }; } } } catch (error) { console.error("[ERROR] Request handler error:", error); return { content: [ { type: "text", text: `Error: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } }); /** * 启动服务器 */ async function runServer() { try { const transport = new StdioServerTransport(); await server.connect(transport); console.error("[INFO] DuckDuckGo Search MCP Server running on stdio"); } catch (error) { console.error("[FATAL] Failed to start server:", error); process.exit(1); } } // 启动服务器并处理未捕获的错误 process.on("uncaughtException", (error) => { console.error("[FATAL] Uncaught exception:", error); process.exit(1); }); process.on("unhandledRejection", (reason) => { console.error("[FATAL] Unhandled rejection:", reason); process.exit(1); }); runServer();