#
tokens: 4499/50000 5/5 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README_zh.md
├── README.md
├── src
│   └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | node_modules/
2 | build/
3 | *.log
4 | .env*
5 | .idea/
6 | .vscode/
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # duckduckgo-search MCP Server
  2 | 
  3 | English | [中文](README_zh.md)
  4 | 
  5 | A Model Context Protocol server for DuckDuckGo Search
  6 | 
  7 | This is a TypeScript-based MCP server that provides DuckDuckGo search functionality. It demonstrates core MCP concepts through:
  8 | 
  9 | - Integration with DuckDuckGo Search
 10 | - Easy-to-use search tool interface
 11 | - Rate limiting and error handling support
 12 | 
 13 | <a href="https://glama.ai/mcp/servers/34fhy9xb9w">
 14 |   <img width="380" height="200" src="https://glama.ai/mcp/servers/34fhy9xb9w/badge" alt="DuckDuckGo Server MCP server" />
 15 | </a>
 16 | 
 17 | ## Features
 18 | 
 19 | ### Search Tool
 20 | 
 21 | - `duckduckgo_search` - Perform web searches using DuckDuckGo API
 22 |   - Required parameter: `query` (search query, max 400 characters)
 23 |   - Optional parameter: `count` (number of results, 1-20, default 10)
 24 |   - Optional parameter: `safeSearch` (safety level: strict/moderate/off, default moderate)
 25 |   - Returns formatted Markdown search results
 26 | 
 27 | ### Rate Limits
 28 | 
 29 | - Maximum 1 request per second
 30 | - Maximum 15000 requests per month
 31 | 
 32 | ## Development
 33 | 
 34 | ### Prerequisites
 35 | 
 36 | - Node.js >= 18
 37 | - pnpm >= 8.0.0
 38 | 
 39 | ### Installation
 40 | 
 41 | ```bash
 42 | # Install pnpm if not already installed
 43 | npm install -g pnpm
 44 | 
 45 | # Install project dependencies
 46 | pnpm install
 47 | ```
 48 | 
 49 | ### Build and Run
 50 | 
 51 | Build the server:
 52 | 
 53 | ```bash
 54 | pnpm run build
 55 | ```
 56 | 
 57 | For development with auto-rebuild:
 58 | 
 59 | ```bash
 60 | pnpm run watch
 61 | ```
 62 | 
 63 | ## Setup in Claude Desktop
 64 | 
 65 | To use with Claude Desktop, add the server config:
 66 | 
 67 | On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
 68 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
 69 | 
 70 | ```json
 71 | # online
 72 | {
 73 |   "mcpServers": {
 74 |     "duckduckgo-search": {
 75 |         "command": "npx",
 76 |         "args": [
 77 |           "-y",
 78 |           "duckduckgo-mcp-server"
 79 |         ]
 80 |     }
 81 |   }
 82 | }
 83 | 
 84 | # local
 85 | {
 86 |   "mcpServers": {
 87 |     "duckduckgo-search": {
 88 |       "command": "node",
 89 |       "args": [
 90 |         "/path/to/duckduckgo-search/build/index.js"
 91 |       ]
 92 |     }
 93 |   }
 94 | }
 95 | ```
 96 | ![image](https://github.com/user-attachments/assets/6906e280-9dbb-4bb5-a537-d9e45e666084)
 97 | ![image](https://github.com/user-attachments/assets/867a70ae-082f-45ab-a623-869bfd6c31eb)
 98 | 
 99 | ### Debugging
100 | 
101 | Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script:
102 | 
103 | ```bash
104 | pnpm run inspector
105 | ```
106 | 
107 | The Inspector will provide a URL to access debugging tools in your browser.
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": ["src/**/*"],
14 |   "exclude": ["node_modules"]
15 | }
16 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "duckduckgo-mcp-server",
 3 |   "version": "0.1.2",
 4 |   "description": "A TypeScript-based MCP server that provides DuckDuckGo search functionality.",
 5 |   "type": "module",
 6 |   "author": {
 7 |     "name": "zhsama",
 8 |     "email": "[email protected]",
 9 |     "url": "https://github.com/zhsama/duckduckgo-mcp-server"
10 |   },
11 |   "homepage": "https://github.com/zhsama/duckduckgo-mcp-server",
12 |   "repository": {
13 |     "type": "git",
14 |     "url": "git+https://github.com/zhsama/duckduckgo-mcp-server.git"
15 |   },
16 |   "license": "MIT",
17 |   "bin": {
18 |     "duckduckgo-mcp-server": "./build/index.js"
19 |   },
20 |   "files": [
21 |     "build"
22 |   ],
23 |   "scripts": {
24 |     "build": "tsc && echo '#!/usr/bin/env node\n' | cat - build/index.js > temp && mv temp build/index.js && chmod +x build/index.js",
25 |     "prepare": "pnpm run build",
26 |     "watch": "tsc --watch",
27 |     "inspector": "npx @modelcontextprotocol/inspector build/index.js"
28 |   },
29 |   "dependencies": {
30 |     "@modelcontextprotocol/sdk": "0.6.0",
31 |     "duck-duck-scrape": "2.2.7",
32 |     "node-fetch": "^3.3.2"
33 |   },
34 |   "devDependencies": {
35 |     "@types/node": "^20.11.24",
36 |     "ts-node": "^10.9.1",
37 |     "typescript": "^5.3.3"
38 |   },
39 |   "engines": {
40 |     "node": ">=18",
41 |     "pnpm": ">=8.0.0"
42 |   }
43 | }
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
  2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  3 | import {
  4 |   CallToolRequestSchema,
  5 |   ListToolsRequestSchema,
  6 | } from "@modelcontextprotocol/sdk/types.js";
  7 | import * as DDG from "duck-duck-scrape";
  8 | 
  9 | interface DuckDuckGoSearchArgs {
 10 |   query: string;
 11 |   count?: number;
 12 |   safeSearch?: "strict" | "moderate" | "off";
 13 | }
 14 | 
 15 | interface SearchResult {
 16 |   title: string;
 17 |   description: string;
 18 |   url: string;
 19 | }
 20 | 
 21 | interface RateLimit {
 22 |   perSecond: number;
 23 |   perMonth: number;
 24 | }
 25 | 
 26 | interface RequestCount {
 27 |   second: number;
 28 |   month: number;
 29 |   lastReset: number;
 30 | }
 31 | 
 32 | const CONFIG = {
 33 |   server: {
 34 |     name: "zhsama/duckduckgo-mcp-server",
 35 |     version: "0.1.2",
 36 |   },
 37 |   rateLimit: {
 38 |     perSecond: 1,
 39 |     perMonth: 15000,
 40 |   } as RateLimit,
 41 |   search: {
 42 |     maxQueryLength: 400,
 43 |     maxResults: 20,
 44 |     defaultResults: 10,
 45 |     defaultSafeSearch: "moderate" as const,
 46 |   },
 47 | } as const;
 48 | 
 49 | const WEB_SEARCH_TOOL = {
 50 |   name: "duckduckgo_web_search",
 51 |   description:
 52 |     "Performs a web search using the DuckDuckGo, ideal for general queries, news, articles, and online content. " +
 53 |     "Use this for broad information gathering, recent events, or when you need diverse web sources. " +
 54 |     "Supports content filtering and region-specific searches. " +
 55 |     `Maximum ${CONFIG.search.maxResults} results per request.`,
 56 |   inputSchema: {
 57 |     type: "object",
 58 |     properties: {
 59 |       query: {
 60 |         type: "string",
 61 |         description: `Search query (max ${CONFIG.search.maxQueryLength} chars)`,
 62 |         maxLength: CONFIG.search.maxQueryLength,
 63 |       },
 64 |       count: {
 65 |         type: "number",
 66 |         description: `Number of results (1-${CONFIG.search.maxResults}, default ${CONFIG.search.defaultResults})`,
 67 |         minimum: 1,
 68 |         maximum: CONFIG.search.maxResults,
 69 |         default: CONFIG.search.defaultResults,
 70 |       },
 71 |       safeSearch: {
 72 |         type: "string",
 73 |         description: "SafeSearch level (strict, moderate, off)",
 74 |         enum: ["strict", "moderate", "off"],
 75 |         default: CONFIG.search.defaultSafeSearch,
 76 |       },
 77 |     },
 78 |     required: ["query"],
 79 |   },
 80 | };
 81 | 
 82 | const server = new Server(CONFIG.server, {
 83 |   capabilities: {
 84 |     tools: {},
 85 |   },
 86 | });
 87 | 
 88 | // 速率限制状态
 89 | let requestCount: RequestCount = {
 90 |   second: 0,
 91 |   month: 0,
 92 |   lastReset: Date.now(),
 93 | };
 94 | 
 95 | /**
 96 |  * 检查并更新速率限制
 97 |  * @throws {Error} 当超过速率限制时抛出错误
 98 |  */
 99 | function checkRateLimit(): void {
100 |   const now = Date.now();
101 |   console.error(`[DEBUG] Rate limit check - Current counts:`, requestCount);
102 | 
103 |   // 重置每秒计数器
104 |   if (now - requestCount.lastReset > 1000) {
105 |     requestCount.second = 0;
106 |     requestCount.lastReset = now;
107 |   }
108 | 
109 |   // 检查限制
110 |   if (
111 |     requestCount.second >= CONFIG.rateLimit.perSecond ||
112 |     requestCount.month >= CONFIG.rateLimit.perMonth
113 |   ) {
114 |     const error = new Error("Rate limit exceeded");
115 |     console.error("[ERROR] Rate limit exceeded:", requestCount);
116 |     throw error;
117 |   }
118 | 
119 |   // 更新计数器
120 |   requestCount.second++;
121 |   requestCount.month++;
122 | }
123 | 
124 | /**
125 |  * 类型守卫:检查参数是否符合 DuckDuckGoSearchArgs 接口
126 |  */
127 | function isDuckDuckGoWebSearchArgs(
128 |   args: unknown
129 | ): args is DuckDuckGoSearchArgs {
130 |   if (typeof args !== "object" || args === null) {
131 |     return false;
132 |   }
133 | 
134 |   const { query } = args as Partial<DuckDuckGoSearchArgs>;
135 | 
136 |   if (typeof query !== "string") {
137 |     return false;
138 |   }
139 | 
140 |   if (query.length > CONFIG.search.maxQueryLength) {
141 |     return false;
142 |   }
143 | 
144 |   return true;
145 | }
146 | 
147 | /**
148 |  * 执行网络搜索
149 |  * @param query 搜索查询
150 |  * @param count 结果数量
151 |  * @param safeSearch 安全搜索级别
152 |  * @returns 格式化的搜索结果
153 |  */
154 | async function performWebSearch(
155 |   query: string,
156 |   count: number = CONFIG.search.defaultResults,
157 |   safeSearch: "strict" | "moderate" | "off" = CONFIG.search.defaultSafeSearch
158 | ): Promise<string> {
159 |   console.error(
160 |     `[DEBUG] Performing search - Query: "${query}", Count: ${count}, SafeSearch: ${safeSearch}`
161 |   );
162 | 
163 |   try {
164 |     checkRateLimit();
165 | 
166 |     const safeSearchMap = {
167 |       strict: DDG.SafeSearchType.STRICT,
168 |       moderate: DDG.SafeSearchType.MODERATE,
169 |       off: DDG.SafeSearchType.OFF,
170 |     };
171 | 
172 |     const searchResults = await DDG.search(query, {
173 |       safeSearch: safeSearchMap[safeSearch],
174 |     });
175 | 
176 |     if (searchResults.noResults) {
177 |       console.error(`[INFO] No results found for query: "${query}"`);
178 |       return `# DuckDuckGo 搜索结果\n没有找到与 "${query}" 相关的结果。`;
179 |     }
180 | 
181 |     const results: SearchResult[] = searchResults.results
182 |       .slice(0, count)
183 |       .map((result: DDG.SearchResult) => ({
184 |         title: result.title,
185 |         description: result.description || result.title,
186 |         url: result.url,
187 |       }));
188 | 
189 |     console.error(
190 |       `[INFO] Found ${results.length} results for query: "${query}"`
191 |     );
192 | 
193 |     // 格式化结果
194 |     return formatSearchResults(query, results);
195 |   } catch (error) {
196 |     console.error(`[ERROR] Search failed - Query: "${query}"`, error);
197 |     throw error;
198 |   }
199 | }
200 | 
201 | /**
202 |  * 格式化搜索结果为 Markdown
203 |  */
204 | function formatSearchResults(query: string, results: SearchResult[]): string {
205 |   const formattedResults = results
206 |     .map((r: SearchResult) => {
207 |       return `### ${r.title}
208 | ${r.description}
209 | 
210 | 🔗 [阅读更多](${r.url})
211 | `;
212 |     })
213 |     .join("\n\n");
214 | 
215 |   return `# DuckDuckGo 搜索结果
216 | ${query} 的搜索结果(${results.length}件)
217 | 
218 | ---
219 | 
220 | ${formattedResults}
221 | `;
222 | }
223 | 
224 | // 工具处理器
225 | server.setRequestHandler(ListToolsRequestSchema, async () => ({
226 |   tools: [WEB_SEARCH_TOOL],
227 | }));
228 | 
229 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
230 |   try {
231 |     console.error(
232 |       `[DEBUG] Received tool call request:`,
233 |       JSON.stringify(request.params, null, 2)
234 |     );
235 | 
236 |     const { name, arguments: args } = request.params;
237 | 
238 |     if (!args) {
239 |       throw new Error("No arguments provided");
240 |     }
241 | 
242 |     switch (name) {
243 |       case "duckduckgo_web_search": {
244 |         if (!isDuckDuckGoWebSearchArgs(args)) {
245 |           throw new Error("Invalid arguments for duckduckgo_web_search");
246 |         }
247 | 
248 |         const {
249 |           query,
250 |           count = CONFIG.search.defaultResults,
251 |           safeSearch = CONFIG.search.defaultSafeSearch,
252 |         } = args;
253 |         const results = await performWebSearch(query, count, safeSearch);
254 | 
255 |         return {
256 |           content: [{ type: "text", text: results }],
257 |           isError: false,
258 |         };
259 |       }
260 |       default: {
261 |         console.error(`[ERROR] Unknown tool requested: ${name}`);
262 |         return {
263 |           content: [{ type: "text", text: `Unknown tool: ${name}` }],
264 |           isError: true,
265 |         };
266 |       }
267 |     }
268 |   } catch (error) {
269 |     console.error("[ERROR] Request handler error:", error);
270 |     return {
271 |       content: [
272 |         {
273 |           type: "text",
274 |           text: `Error: ${
275 |             error instanceof Error ? error.message : String(error)
276 |           }`,
277 |         },
278 |       ],
279 |       isError: true,
280 |     };
281 |   }
282 | });
283 | 
284 | /**
285 |  * 启动服务器
286 |  */
287 | async function runServer() {
288 |   try {
289 |     const transport = new StdioServerTransport();
290 |     await server.connect(transport);
291 |     console.error("[INFO] DuckDuckGo Search MCP Server running on stdio");
292 |   } catch (error) {
293 |     console.error("[FATAL] Failed to start server:", error);
294 |     process.exit(1);
295 |   }
296 | }
297 | 
298 | // 启动服务器并处理未捕获的错误
299 | process.on("uncaughtException", (error) => {
300 |   console.error("[FATAL] Uncaught exception:", error);
301 |   process.exit(1);
302 | });
303 | 
304 | process.on("unhandledRejection", (reason) => {
305 |   console.error("[FATAL] Unhandled rejection:", reason);
306 |   process.exit(1);
307 | });
308 | 
309 | runServer();
310 | 
```