# Directory Structure ``` ├── .gitignore ├── Dockerfile ├── LICENSE ├── package.json ├── pnpm-lock.yaml ├── README.md ├── smithery.yaml └── src └── index.js ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Build output 8 | dist/ 9 | 10 | # IDE 11 | .idea/ 12 | .vscode/ 13 | *.swp 14 | *.swo 15 | 16 | # OS 17 | .DS_Store 18 | Thumbs.db 19 | 20 | # windsurf rules 21 | .windsurfrules 22 | ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | properties: {} 9 | commandFunction: 10 | # A function that produces the CLI command to start the MCP on stdio. 11 | |- 12 | config => ({command:'node',args:['src/index.js'],env:{}}) 13 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | # Use a Node.js image 3 | FROM node:16-alpine 4 | 5 | # Set the working directory 6 | WORKDIR /app 7 | 8 | # Copy package.json and package-lock.json to the working directory 9 | COPY package.json /app 10 | 11 | # Install dependencies 12 | RUN npm install 13 | 14 | # Copy the rest of the application code to the working directory 15 | COPY src /app/src 16 | 17 | # Expose the port the app runs on 18 | EXPOSE 3000 19 | 20 | # Command to run the application 21 | ENTRYPOINT ["node", "src/index.js"] 22 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "duck-duck-mcp", 3 | "version": "1.0.5", 4 | "description": "DuckDuckGo search implementation for Model Context Protocol (MCP)", 5 | "license": "MIT", 6 | "type": "module", 7 | "main": "src/index.js", 8 | "bin": { 9 | "duck-duck-mcp": "src/index.js" 10 | }, 11 | "files": [ 12 | "src" 13 | ], 14 | "scripts": { 15 | "start": "node src/index.js" 16 | }, 17 | "keywords": [ 18 | "mcp", 19 | "duckduckgo", 20 | "search", 21 | "ai", 22 | "claude", 23 | "model-context-protocol" 24 | ], 25 | "author": { 26 | "name": "qwang07" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/qwang07/duck-duck-mcp.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/qwang07/duck-duck-mcp/issues" 34 | }, 35 | "homepage": "https://github.com/qwang07/duck-duck-mcp#readme", 36 | "dependencies": { 37 | "@modelcontextprotocol/sdk": "^1.0.0", 38 | "duck-duck-scrape": "^2.2.5", 39 | "zod": "^3.22.4", 40 | "zod-to-json-schema": "^3.23.5" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "^20.11.24", 44 | "shx": "^0.3.4", 45 | "typescript": "^5.3.3" 46 | } 47 | } ``` -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { 6 | CallToolRequestSchema, 7 | ListToolsRequestSchema, 8 | } from "@modelcontextprotocol/sdk/types.js"; 9 | import { search, SafeSearchType } from 'duck-duck-scrape'; 10 | import { z } from "zod"; 11 | import { zodToJsonSchema } from "zod-to-json-schema"; 12 | 13 | // 服务器配置 14 | const SERVER_CONFIG = { 15 | name: "search-server", 16 | version: "1.0.0", 17 | }; 18 | 19 | const SearchArgsSchema = z.object({ 20 | query: z.string(), 21 | options: z.object({ 22 | region: z.string().default('zh-cn'), 23 | safeSearch: z.enum(['OFF', 'MODERATE', 'STRICT']).default('MODERATE'), 24 | numResults: z.number().default(50) 25 | }).optional() 26 | }); 27 | 28 | function detectContentType(result) { 29 | const url = result.url.toLowerCase(); 30 | if (url.includes('docs.') || url.includes('/docs/') || url.includes('/documentation/')) { 31 | return 'documentation'; 32 | } 33 | if (url.includes('github.com') || url.includes('stackoverflow.com')) { 34 | return 'documentation'; 35 | } 36 | if (url.includes('twitter.com') || url.includes('facebook.com') || url.includes('linkedin.com')) { 37 | return 'social'; 38 | } 39 | return 'article'; 40 | } 41 | 42 | function detectLanguage(query) { 43 | return /[\u4e00-\u9fa5]/.test(query) ? 'zh-cn' : 'en'; 44 | } 45 | 46 | function detectTopics(results) { 47 | const topics = new Set(); 48 | results.forEach(result => { 49 | if (result.title.toLowerCase().includes('github')) topics.add('technology'); 50 | if (result.title.toLowerCase().includes('docs')) topics.add('documentation'); 51 | }); 52 | return Array.from(topics); 53 | } 54 | 55 | function processSearchResults(results, query, options) { 56 | return { 57 | type: 'search_results', 58 | data: results.map(result => ({ 59 | title: result.title.replace(/'/g, "'").replace(/"/g, '"'), 60 | url: result.url, 61 | description: result.description.trim(), 62 | metadata: { 63 | type: detectContentType(result), 64 | source: new URL(result.url).hostname 65 | } 66 | })), 67 | metadata: { 68 | query, 69 | timestamp: new Date().toISOString(), 70 | resultCount: results.length, 71 | searchContext: { 72 | region: options.region || 'zh-cn', 73 | safeSearch: options.safeSearch?.toString() || 'MODERATE' 74 | }, 75 | queryAnalysis: { 76 | language: detectLanguage(query), 77 | topics: detectTopics(results) 78 | } 79 | } 80 | }; 81 | } 82 | 83 | const server = new Server( 84 | SERVER_CONFIG, 85 | { 86 | capabilities: { 87 | tools: {}, 88 | }, 89 | } 90 | ); 91 | 92 | server.setRequestHandler(ListToolsRequestSchema, async () => { 93 | return { 94 | tools: [ 95 | { 96 | name: "search", 97 | description: "Search the web using DuckDuckGo", 98 | inputSchema: zodToJsonSchema(SearchArgsSchema), 99 | } 100 | ], 101 | }; 102 | }); 103 | 104 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 105 | try { 106 | const { name, arguments: args } = request.params; 107 | 108 | if (name !== "search") { 109 | throw Object.assign( 110 | new Error(`Unknown tool: ${name}`), 111 | { errorType: 'UNKNOWN_TOOL', name } 112 | ); 113 | } 114 | 115 | const parsed = SearchArgsSchema.safeParse(args); 116 | if (!parsed.success) { 117 | throw Object.assign( 118 | new Error(`Invalid arguments: ${parsed.error}`), 119 | { errorType: 'INVALID_ARGS', details: parsed.error } 120 | ); 121 | } 122 | 123 | const searchResults = await search(parsed.data.query, { 124 | region: parsed.data.options?.region || 'zh-cn', 125 | safeSearch: parsed.data.options?.safeSearch ? SafeSearchType[parsed.data.options.safeSearch] : SafeSearchType.MODERATE, 126 | maxResults: parsed.data.options?.numResults || 50 127 | }); 128 | 129 | const response = processSearchResults( 130 | searchResults.results, 131 | parsed.data.query, 132 | { 133 | region: parsed.data.options?.region, 134 | safeSearch: parsed.data.options?.safeSearch ? SafeSearchType[parsed.data.options.safeSearch] : SafeSearchType.MODERATE 135 | } 136 | ); 137 | 138 | return { 139 | content: [{ 140 | type: "text", 141 | text: JSON.stringify(response, null, 2) 142 | }] 143 | }; 144 | } catch (error) { 145 | const errorResponse = { 146 | type: 'search_error', 147 | message: error instanceof Error ? error.message : String(error), 148 | suggestion: '你可以尝试:1. 修改搜索关键词 2. 减少结果数量 3. 更换地区', 149 | context: { 150 | query: request.params.arguments?.query, 151 | options: request.params.arguments?.options 152 | } 153 | }; 154 | 155 | return { 156 | content: [{ 157 | type: "text", 158 | text: JSON.stringify(errorResponse, null, 2) 159 | }], 160 | isError: true 161 | }; 162 | } 163 | }); 164 | 165 | async function runServer() { 166 | const transport = new StdioServerTransport(); 167 | await server.connect(transport); 168 | console.error("MCP Search Server running on stdio"); 169 | } 170 | 171 | runServer().catch((error) => { 172 | console.error("Fatal error running server:", error); 173 | process.exit(1); 174 | }); ```