#
tokens: 1915/50000 5/5 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── Dockerfile
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── smithery.yaml
└── src
    └── index.js
```

# Files

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

```
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Build output
dist/

# IDE
.idea/
.vscode/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# windsurf rules
.windsurfrules

```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    properties: {}
  commandFunction:
    # A function that produces the CLI command to start the MCP on stdio.
    |-
    config => ({command:'node',args:['src/index.js'],env:{}})

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
# Use a Node.js image
FROM node:16-alpine

# Set the working directory
WORKDIR /app

# Copy package.json and package-lock.json to the working directory
COPY package.json /app

# Install dependencies
RUN npm install

# Copy the rest of the application code to the working directory
COPY src /app/src

# Expose the port the app runs on
EXPOSE 3000

# Command to run the application
ENTRYPOINT ["node", "src/index.js"]

```

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

```json
{
  "name": "duck-duck-mcp",
  "version": "1.0.5",
  "description": "DuckDuckGo search implementation for Model Context Protocol (MCP)",
  "license": "MIT",
  "type": "module",
  "main": "src/index.js",
  "bin": {
    "duck-duck-mcp": "src/index.js"
  },
  "files": [
    "src"
  ],
  "scripts": {
    "start": "node src/index.js"
  },
  "keywords": [
    "mcp",
    "duckduckgo",
    "search",
    "ai",
    "claude",
    "model-context-protocol"
  ],
  "author": {
    "name": "qwang07"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/qwang07/duck-duck-mcp.git"
  },
  "bugs": {
    "url": "https://github.com/qwang07/duck-duck-mcp/issues"
  },
  "homepage": "https://github.com/qwang07/duck-duck-mcp#readme",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "duck-duck-scrape": "^2.2.5",
    "zod": "^3.22.4",
    "zod-to-json-schema": "^3.23.5"
  },
  "devDependencies": {
    "@types/node": "^20.11.24",
    "shx": "^0.3.4",
    "typescript": "^5.3.3"
  }
}
```

--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node

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 { search, SafeSearchType } from 'duck-duck-scrape';
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

// 服务器配置
const SERVER_CONFIG = {
  name: "search-server",
  version: "1.0.0",
};

const SearchArgsSchema = z.object({
  query: z.string(),
  options: z.object({
    region: z.string().default('zh-cn'),
    safeSearch: z.enum(['OFF', 'MODERATE', 'STRICT']).default('MODERATE'),
    numResults: z.number().default(50)
  }).optional()
});

function detectContentType(result) {
  const url = result.url.toLowerCase();
  if (url.includes('docs.') || url.includes('/docs/') || url.includes('/documentation/')) {
    return 'documentation';
  }
  if (url.includes('github.com') || url.includes('stackoverflow.com')) {
    return 'documentation';
  }
  if (url.includes('twitter.com') || url.includes('facebook.com') || url.includes('linkedin.com')) {
    return 'social';
  }
  return 'article';
}

function detectLanguage(query) {
  return /[\u4e00-\u9fa5]/.test(query) ? 'zh-cn' : 'en';
}

function detectTopics(results) {
  const topics = new Set();
  results.forEach(result => {
    if (result.title.toLowerCase().includes('github')) topics.add('technology');
    if (result.title.toLowerCase().includes('docs')) topics.add('documentation');
  });
  return Array.from(topics);
}

function processSearchResults(results, query, options) {
  return {
    type: 'search_results',
    data: results.map(result => ({
      title: result.title.replace(/'/g, "'").replace(/"/g, '"'),
      url: result.url,
      description: result.description.trim(),
      metadata: {
        type: detectContentType(result),
        source: new URL(result.url).hostname
      }
    })),
    metadata: {
      query,
      timestamp: new Date().toISOString(),
      resultCount: results.length,
      searchContext: {
        region: options.region || 'zh-cn',
        safeSearch: options.safeSearch?.toString() || 'MODERATE'
      },
      queryAnalysis: {
        language: detectLanguage(query),
        topics: detectTopics(results)
      }
    }
  };
}

const server = new Server(
  SERVER_CONFIG,
  {
    capabilities: {
      tools: {},
    },
  }
);

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "search",
        description: "Search the web using DuckDuckGo",
        inputSchema: zodToJsonSchema(SearchArgsSchema),
      }
    ],
  };
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  try {
    const { name, arguments: args } = request.params;

    if (name !== "search") {
      throw Object.assign(
        new Error(`Unknown tool: ${name}`),
        { errorType: 'UNKNOWN_TOOL', name }
      );
    }

    const parsed = SearchArgsSchema.safeParse(args);
    if (!parsed.success) {
      throw Object.assign(
        new Error(`Invalid arguments: ${parsed.error}`),
        { errorType: 'INVALID_ARGS', details: parsed.error }
      );
    }

    const searchResults = await search(parsed.data.query, {
      region: parsed.data.options?.region || 'zh-cn',
      safeSearch: parsed.data.options?.safeSearch ? SafeSearchType[parsed.data.options.safeSearch] : SafeSearchType.MODERATE,
      maxResults: parsed.data.options?.numResults || 50
    });

    const response = processSearchResults(
      searchResults.results,
      parsed.data.query,
      {
        region: parsed.data.options?.region,
        safeSearch: parsed.data.options?.safeSearch ? SafeSearchType[parsed.data.options.safeSearch] : SafeSearchType.MODERATE
      }
    );

    return {
      content: [{
        type: "text",
        text: JSON.stringify(response, null, 2)
      }]
    };
  } catch (error) {
    const errorResponse = {
      type: 'search_error',
      message: error instanceof Error ? error.message : String(error),
      suggestion: '你可以尝试:1. 修改搜索关键词 2. 减少结果数量 3. 更换地区',
      context: {
        query: request.params.arguments?.query,
        options: request.params.arguments?.options
      }
    };

    return {
      content: [{ 
        type: "text", 
        text: JSON.stringify(errorResponse, null, 2)
      }],
      isError: true
    };
  }
});

async function runServer() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP Search Server running on stdio");
}

runServer().catch((error) => {
  console.error("Fatal error running server:", error);
  process.exit(1);
}); 
```