#
tokens: 3215/50000 5/5 files
lines: off (toggle) GitHub
raw markdown copy
# 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"
      ]
    }
  }
}
```
![image](https://github.com/user-attachments/assets/6906e280-9dbb-4bb5-a537-d9e45e666084)
![image](https://github.com/user-attachments/assets/867a70ae-082f-45ab-a623-869bfd6c31eb)

### 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();

```