# Directory Structure
```
├── .gitignore
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README_zh.md
├── README.md
├── src
│ └── index.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
node_modules/
build/
*.log
.env*
.idea/
.vscode/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# duckduckgo-search MCP Server
English | [中文](README_zh.md)
A Model Context Protocol server for DuckDuckGo Search
This is a TypeScript-based MCP server that provides DuckDuckGo search functionality. It demonstrates core MCP concepts through:
- Integration with DuckDuckGo Search
- Easy-to-use search tool interface
- Rate limiting and error handling support
<a href="https://glama.ai/mcp/servers/34fhy9xb9w">
<img width="380" height="200" src="https://glama.ai/mcp/servers/34fhy9xb9w/badge" alt="DuckDuckGo Server MCP server" />
</a>
## Features
### Search Tool
- `duckduckgo_search` - Perform web searches using DuckDuckGo API
- Required parameter: `query` (search query, max 400 characters)
- Optional parameter: `count` (number of results, 1-20, default 10)
- Optional parameter: `safeSearch` (safety level: strict/moderate/off, default moderate)
- Returns formatted Markdown search results
### Rate Limits
- Maximum 1 request per second
- Maximum 15000 requests per month
## Development
### Prerequisites
- Node.js >= 18
- pnpm >= 8.0.0
### Installation
```bash
# Install pnpm if not already installed
npm install -g pnpm
# Install project dependencies
pnpm install
```
### Build and Run
Build the server:
```bash
pnpm run build
```
For development with auto-rebuild:
```bash
pnpm run watch
```
## Setup in Claude Desktop
To use with Claude Desktop, add the server config:
On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
```json
# online
{
"mcpServers": {
"duckduckgo-search": {
"command": "npx",
"args": [
"-y",
"duckduckgo-mcp-server"
]
}
}
}
# local
{
"mcpServers": {
"duckduckgo-search": {
"command": "node",
"args": [
"/path/to/duckduckgo-search/build/index.js"
]
}
}
}
```


### Debugging
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:
```bash
pnpm run inspector
```
The Inspector will provide a URL to access debugging tools in your browser.
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "duckduckgo-mcp-server",
"version": "0.1.2",
"description": "A TypeScript-based MCP server that provides DuckDuckGo search functionality.",
"type": "module",
"author": {
"name": "zhsama",
"email": "[email protected]",
"url": "https://github.com/zhsama/duckduckgo-mcp-server"
},
"homepage": "https://github.com/zhsama/duckduckgo-mcp-server",
"repository": {
"type": "git",
"url": "git+https://github.com/zhsama/duckduckgo-mcp-server.git"
},
"license": "MIT",
"bin": {
"duckduckgo-mcp-server": "./build/index.js"
},
"files": [
"build"
],
"scripts": {
"build": "tsc && echo '#!/usr/bin/env node\n' | cat - build/index.js > temp && mv temp build/index.js && chmod +x build/index.js",
"prepare": "pnpm run build",
"watch": "tsc --watch",
"inspector": "npx @modelcontextprotocol/inspector build/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "0.6.0",
"duck-duck-scrape": "2.2.7",
"node-fetch": "^3.3.2"
},
"devDependencies": {
"@types/node": "^20.11.24",
"ts-node": "^10.9.1",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=18",
"pnpm": ">=8.0.0"
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
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 * as DDG from "duck-duck-scrape";
interface DuckDuckGoSearchArgs {
query: string;
count?: number;
safeSearch?: "strict" | "moderate" | "off";
}
interface SearchResult {
title: string;
description: string;
url: string;
}
interface RateLimit {
perSecond: number;
perMonth: number;
}
interface RequestCount {
second: number;
month: number;
lastReset: number;
}
const CONFIG = {
server: {
name: "zhsama/duckduckgo-mcp-server",
version: "0.1.2",
},
rateLimit: {
perSecond: 1,
perMonth: 15000,
} as RateLimit,
search: {
maxQueryLength: 400,
maxResults: 20,
defaultResults: 10,
defaultSafeSearch: "moderate" as const,
},
} as const;
const WEB_SEARCH_TOOL = {
name: "duckduckgo_web_search",
description:
"Performs a web search using the DuckDuckGo, ideal for general queries, news, articles, and online content. " +
"Use this for broad information gathering, recent events, or when you need diverse web sources. " +
"Supports content filtering and region-specific searches. " +
`Maximum ${CONFIG.search.maxResults} results per request.`,
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: `Search query (max ${CONFIG.search.maxQueryLength} chars)`,
maxLength: CONFIG.search.maxQueryLength,
},
count: {
type: "number",
description: `Number of results (1-${CONFIG.search.maxResults}, default ${CONFIG.search.defaultResults})`,
minimum: 1,
maximum: CONFIG.search.maxResults,
default: CONFIG.search.defaultResults,
},
safeSearch: {
type: "string",
description: "SafeSearch level (strict, moderate, off)",
enum: ["strict", "moderate", "off"],
default: CONFIG.search.defaultSafeSearch,
},
},
required: ["query"],
},
};
const server = new Server(CONFIG.server, {
capabilities: {
tools: {},
},
});
// 速率限制状态
let requestCount: RequestCount = {
second: 0,
month: 0,
lastReset: Date.now(),
};
/**
* 检查并更新速率限制
* @throws {Error} 当超过速率限制时抛出错误
*/
function checkRateLimit(): void {
const now = Date.now();
console.error(`[DEBUG] Rate limit check - Current counts:`, requestCount);
// 重置每秒计数器
if (now - requestCount.lastReset > 1000) {
requestCount.second = 0;
requestCount.lastReset = now;
}
// 检查限制
if (
requestCount.second >= CONFIG.rateLimit.perSecond ||
requestCount.month >= CONFIG.rateLimit.perMonth
) {
const error = new Error("Rate limit exceeded");
console.error("[ERROR] Rate limit exceeded:", requestCount);
throw error;
}
// 更新计数器
requestCount.second++;
requestCount.month++;
}
/**
* 类型守卫:检查参数是否符合 DuckDuckGoSearchArgs 接口
*/
function isDuckDuckGoWebSearchArgs(
args: unknown
): args is DuckDuckGoSearchArgs {
if (typeof args !== "object" || args === null) {
return false;
}
const { query } = args as Partial<DuckDuckGoSearchArgs>;
if (typeof query !== "string") {
return false;
}
if (query.length > CONFIG.search.maxQueryLength) {
return false;
}
return true;
}
/**
* 执行网络搜索
* @param query 搜索查询
* @param count 结果数量
* @param safeSearch 安全搜索级别
* @returns 格式化的搜索结果
*/
async function performWebSearch(
query: string,
count: number = CONFIG.search.defaultResults,
safeSearch: "strict" | "moderate" | "off" = CONFIG.search.defaultSafeSearch
): Promise<string> {
console.error(
`[DEBUG] Performing search - Query: "${query}", Count: ${count}, SafeSearch: ${safeSearch}`
);
try {
checkRateLimit();
const safeSearchMap = {
strict: DDG.SafeSearchType.STRICT,
moderate: DDG.SafeSearchType.MODERATE,
off: DDG.SafeSearchType.OFF,
};
const searchResults = await DDG.search(query, {
safeSearch: safeSearchMap[safeSearch],
});
if (searchResults.noResults) {
console.error(`[INFO] No results found for query: "${query}"`);
return `# DuckDuckGo 搜索结果\n没有找到与 "${query}" 相关的结果。`;
}
const results: SearchResult[] = searchResults.results
.slice(0, count)
.map((result: DDG.SearchResult) => ({
title: result.title,
description: result.description || result.title,
url: result.url,
}));
console.error(
`[INFO] Found ${results.length} results for query: "${query}"`
);
// 格式化结果
return formatSearchResults(query, results);
} catch (error) {
console.error(`[ERROR] Search failed - Query: "${query}"`, error);
throw error;
}
}
/**
* 格式化搜索结果为 Markdown
*/
function formatSearchResults(query: string, results: SearchResult[]): string {
const formattedResults = results
.map((r: SearchResult) => {
return `### ${r.title}
${r.description}
🔗 [阅读更多](${r.url})
`;
})
.join("\n\n");
return `# DuckDuckGo 搜索结果
${query} 的搜索结果(${results.length}件)
---
${formattedResults}
`;
}
// 工具处理器
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [WEB_SEARCH_TOOL],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
console.error(
`[DEBUG] Received tool call request:`,
JSON.stringify(request.params, null, 2)
);
const { name, arguments: args } = request.params;
if (!args) {
throw new Error("No arguments provided");
}
switch (name) {
case "duckduckgo_web_search": {
if (!isDuckDuckGoWebSearchArgs(args)) {
throw new Error("Invalid arguments for duckduckgo_web_search");
}
const {
query,
count = CONFIG.search.defaultResults,
safeSearch = CONFIG.search.defaultSafeSearch,
} = args;
const results = await performWebSearch(query, count, safeSearch);
return {
content: [{ type: "text", text: results }],
isError: false,
};
}
default: {
console.error(`[ERROR] Unknown tool requested: ${name}`);
return {
content: [{ type: "text", text: `Unknown tool: ${name}` }],
isError: true,
};
}
}
} catch (error) {
console.error("[ERROR] Request handler error:", error);
return {
content: [
{
type: "text",
text: `Error: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
});
/**
* 启动服务器
*/
async function runServer() {
try {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[INFO] DuckDuckGo Search MCP Server running on stdio");
} catch (error) {
console.error("[FATAL] Failed to start server:", error);
process.exit(1);
}
}
// 启动服务器并处理未捕获的错误
process.on("uncaughtException", (error) => {
console.error("[FATAL] Uncaught exception:", error);
process.exit(1);
});
process.on("unhandledRejection", (reason) => {
console.error("[FATAL] Unhandled rejection:", reason);
process.exit(1);
});
runServer();
```