# 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 | 
97 | 
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 |
```