#
tokens: 2648/50000 5/5 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | }); 
```