# Directory Structure ``` ├── .gitignore ├── Dockerfile ├── LICENSE ├── package.json ├── pnpm-lock.yaml ├── README.md ├── smithery.yaml └── src └── index.js ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* # Build output dist/ # IDE .idea/ .vscode/ *.swp *.swo # OS .DS_Store Thumbs.db # windsurf rules .windsurfrules ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml startCommand: type: stdio configSchema: # JSON Schema defining the configuration options for the MCP. type: object properties: {} commandFunction: # A function that produces the CLI command to start the MCP on stdio. |- config => ({command:'node',args:['src/index.js'],env:{}}) ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile # Use a Node.js image FROM node:16-alpine # Set the working directory WORKDIR /app # Copy package.json and package-lock.json to the working directory COPY package.json /app # Install dependencies RUN npm install # Copy the rest of the application code to the working directory COPY src /app/src # Expose the port the app runs on EXPOSE 3000 # Command to run the application ENTRYPOINT ["node", "src/index.js"] ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "duck-duck-mcp", "version": "1.0.5", "description": "DuckDuckGo search implementation for Model Context Protocol (MCP)", "license": "MIT", "type": "module", "main": "src/index.js", "bin": { "duck-duck-mcp": "src/index.js" }, "files": [ "src" ], "scripts": { "start": "node src/index.js" }, "keywords": [ "mcp", "duckduckgo", "search", "ai", "claude", "model-context-protocol" ], "author": { "name": "qwang07" }, "repository": { "type": "git", "url": "git+https://github.com/qwang07/duck-duck-mcp.git" }, "bugs": { "url": "https://github.com/qwang07/duck-duck-mcp/issues" }, "homepage": "https://github.com/qwang07/duck-duck-mcp#readme", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", "duck-duck-scrape": "^2.2.5", "zod": "^3.22.4", "zod-to-json-schema": "^3.23.5" }, "devDependencies": { "@types/node": "^20.11.24", "shx": "^0.3.4", "typescript": "^5.3.3" } } ``` -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node 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 { search, SafeSearchType } from 'duck-duck-scrape'; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; // 服务器配置 const SERVER_CONFIG = { name: "search-server", version: "1.0.0", }; const SearchArgsSchema = z.object({ query: z.string(), options: z.object({ region: z.string().default('zh-cn'), safeSearch: z.enum(['OFF', 'MODERATE', 'STRICT']).default('MODERATE'), numResults: z.number().default(50) }).optional() }); function detectContentType(result) { const url = result.url.toLowerCase(); if (url.includes('docs.') || url.includes('/docs/') || url.includes('/documentation/')) { return 'documentation'; } if (url.includes('github.com') || url.includes('stackoverflow.com')) { return 'documentation'; } if (url.includes('twitter.com') || url.includes('facebook.com') || url.includes('linkedin.com')) { return 'social'; } return 'article'; } function detectLanguage(query) { return /[\u4e00-\u9fa5]/.test(query) ? 'zh-cn' : 'en'; } function detectTopics(results) { const topics = new Set(); results.forEach(result => { if (result.title.toLowerCase().includes('github')) topics.add('technology'); if (result.title.toLowerCase().includes('docs')) topics.add('documentation'); }); return Array.from(topics); } function processSearchResults(results, query, options) { return { type: 'search_results', data: results.map(result => ({ title: result.title.replace(/'/g, "'").replace(/"/g, '"'), url: result.url, description: result.description.trim(), metadata: { type: detectContentType(result), source: new URL(result.url).hostname } })), metadata: { query, timestamp: new Date().toISOString(), resultCount: results.length, searchContext: { region: options.region || 'zh-cn', safeSearch: options.safeSearch?.toString() || 'MODERATE' }, queryAnalysis: { language: detectLanguage(query), topics: detectTopics(results) } } }; } const server = new Server( SERVER_CONFIG, { capabilities: { tools: {}, }, } ); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "search", description: "Search the web using DuckDuckGo", inputSchema: zodToJsonSchema(SearchArgsSchema), } ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; if (name !== "search") { throw Object.assign( new Error(`Unknown tool: ${name}`), { errorType: 'UNKNOWN_TOOL', name } ); } const parsed = SearchArgsSchema.safeParse(args); if (!parsed.success) { throw Object.assign( new Error(`Invalid arguments: ${parsed.error}`), { errorType: 'INVALID_ARGS', details: parsed.error } ); } const searchResults = await search(parsed.data.query, { region: parsed.data.options?.region || 'zh-cn', safeSearch: parsed.data.options?.safeSearch ? SafeSearchType[parsed.data.options.safeSearch] : SafeSearchType.MODERATE, maxResults: parsed.data.options?.numResults || 50 }); const response = processSearchResults( searchResults.results, parsed.data.query, { region: parsed.data.options?.region, safeSearch: parsed.data.options?.safeSearch ? SafeSearchType[parsed.data.options.safeSearch] : SafeSearchType.MODERATE } ); return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] }; } catch (error) { const errorResponse = { type: 'search_error', message: error instanceof Error ? error.message : String(error), suggestion: '你可以尝试:1. 修改搜索关键词 2. 减少结果数量 3. 更换地区', context: { query: request.params.arguments?.query, options: request.params.arguments?.options } }; return { content: [{ type: "text", text: JSON.stringify(errorResponse, null, 2) }], isError: true }; } }); async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP Search Server running on stdio"); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); }); ```