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

```
├── .github
│   └── workflows
│       └── publish.yaml
├── .gitignore
├── .python-version
├── pyproject.toml
├── README.md
├── src
│   └── ddg_mcp
│       ├── __init__.py
│       └── server.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
3.11

```

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

```
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# Virtual environments
.venv

```

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

```markdown
# ddg-mcp MCP server

DuckDuckGo search API MCP - A server that provides DuckDuckGo search capabilities through the Model Context Protocol.

## Components

### Prompts

The server provides the following prompts:
- **search-results-summary**: Creates a summary of DuckDuckGo search results
  - Required "query" argument for the search term
  - Optional "style" argument to control detail level (brief/detailed)

### Tools

The server implements the following DuckDuckGo search tools:

- **ddg-text-search**: Search the web for text results using DuckDuckGo
  - Required: "keywords" - Search query keywords
  - Optional: "region", "safesearch", "timelimit", "max_results"
  
- **ddg-image-search**: Search the web for images using DuckDuckGo
  - Required: "keywords" - Search query keywords
  - Optional: "region", "safesearch", "timelimit", "size", "color", "type_image", "layout", "license_image", "max_results"
  
- **ddg-news-search**: Search for news articles using DuckDuckGo
  - Required: "keywords" - Search query keywords
  - Optional: "region", "safesearch", "timelimit", "max_results"
  
- **ddg-video-search**: Search for videos using DuckDuckGo
  - Required: "keywords" - Search query keywords
  - Optional: "region", "safesearch", "timelimit", "resolution", "duration", "license_videos", "max_results"
  
- **ddg-ai-chat**: Chat with DuckDuckGo AI
  - Required: "keywords" - Message or question to send to the AI
  - Optional: "model" - AI model to use (options: "gpt-4o-mini", "llama-3.3-70b", "claude-3-haiku", "o3-mini", "mistral-small-3")

## Installation

### Prerequisites

- Python 3.9 or higher
- [uv](https://github.com/astral-sh/uv) (recommended) or pip

### Install from PyPI

```bash
# Using uv
uv install ddg-mcp

# Using pip
pip install ddg-mcp
```

### Install from Source

1. Clone the repository:
```bash
git clone https://github.com/misanthropic-ai/ddg-mcp.git
cd ddg-mcp
```

2. Install the package:
```bash
# Using uv
uv install -e .

# Using pip
pip install -e .
```

## Configuration

### Required Dependencies

The server requires the `duckduckgo-search` package, which will be installed automatically when you install `ddg-mcp`.

If you need to install it manually:
```bash
uv install duckduckgo-search
# or
pip install duckduckgo-search
```

## DuckDuckGo Search Parameters

### Common Parameters

These parameters are available for most search types:

- **region**: Region code for localized results (default: "wt-wt")
  - Examples: "us-en" (US English), "uk-en" (UK English), "ru-ru" (Russian)
  - See [DuckDuckGo regions](https://duckduckgo.com/params) for more options

- **safesearch**: Content filtering level (default: "moderate")
  - "on": Strict filtering
  - "moderate": Moderate filtering
  - "off": No filtering

- **timelimit**: Time range for results
  - "d": Last day
  - "w": Last week
  - "m": Last month
  - "y": Last year (not available for news/videos)

- **max_results**: Maximum number of results to return (default: 10)

### Search Operators

You can use these operators in your search keywords:

- `cats dogs`: Results about cats or dogs
- `"cats and dogs"`: Results for exact term "cats and dogs"
- `cats -dogs`: Fewer dogs in results
- `cats +dogs`: More dogs in results
- `cats filetype:pdf`: PDFs about cats (supported: pdf, doc(x), xls(x), ppt(x), html)
- `dogs site:example.com`: Pages about dogs from example.com
- `cats -site:example.com`: Pages about cats, excluding example.com
- `intitle:dogs`: Page title includes the word "dogs"
- `inurl:cats`: Page URL includes the word "cats"

### Image Search Specific Parameters

- **size**: "Small", "Medium", "Large", "Wallpaper"
- **color**: "color", "Monochrome", "Red", "Orange", "Yellow", "Green", "Blue", "Purple", "Pink", "Brown", "Black", "Gray", "Teal", "White"
- **type_image**: "photo", "clipart", "gif", "transparent", "line"
- **layout**: "Square", "Tall", "Wide"
- **license_image**: "any", "Public", "Share", "ShareCommercially", "Modify", "ModifyCommercially"

### Video Search Specific Parameters

- **resolution**: "high", "standard"
- **duration**: "short", "medium", "long"
- **license_videos**: "creativeCommon", "youtube"

### AI Chat Models

- **gpt-4o-mini**: OpenAI's GPT-4o mini model
- **llama-3.3-70b**: Meta's Llama 3.3 70B model
- **claude-3-haiku**: Anthropic's Claude 3 Haiku model
- **o3-mini**: OpenAI's O3 mini model
- **mistral-small-3**: Mistral AI's small model

## Quickstart

### Install

#### Claude Desktop

On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`

<details>
  <summary>Development/Unpublished Servers Configuration</summary>
  ```
  "mcpServers": {
    "ddg-mcp": {
      "command": "uv",
      "args": [
        "--directory",
        "/Users/shannon/Workspace/artivus/ddg-mcp",
        "run",
        "ddg-mcp"
      ]
    }
  }
  ```
</details>

<details>
  <summary>Published Servers Configuration</summary>
  ```
  "mcpServers": {
    "ddg-mcp": {
      "command": "uvx",
      "args": [
        "ddg-mcp"
      ]
    }
  }
  ```
</details>

## Usage Examples

### Text Search

```
Use the ddg-text-search tool to search for "climate change solutions"
```

Advanced example:
```
Use the ddg-text-search tool to search for "renewable energy filetype:pdf site:edu" with region "us-en", safesearch "off", timelimit "y", and max_results 20
```

### Image Search

```
Use the ddg-image-search tool to find images of "renewable energy" with color set to "Green"
```

Advanced example:
```
Use the ddg-image-search tool to find images of "mountain landscape" with size "Large", color "Blue", type_image "photo", layout "Wide", and license_image "Public"
```

### News Search

```
Use the ddg-news-search tool to find recent news about "artificial intelligence" from the last day
```

Advanced example:
```
Use the ddg-news-search tool to search for "space exploration" with region "uk-en", timelimit "w", and max_results 15
```

### Video Search

```
Use the ddg-video-search tool to find videos about "machine learning tutorials" with duration set to "medium"
```

Advanced example:
```
Use the ddg-video-search tool to search for "cooking recipes" with resolution "high", duration "short", license_videos "creativeCommon", and max_results 10
```

### AI Chat

```
Use the ddg-ai-chat tool to ask "What are the latest developments in quantum computing?" using the claude-3-haiku model
```

### Search Results Summary

```
Use the search-results-summary prompt with query "space exploration" and style "detailed"
```

## Claude config
"ddg-mcp": {
      "command": "uv",
      "args": [
        "--directory",
        "/PATH/TO/YOUR/INSTALLATION/ddg-mcp",
        "run",
        "ddg-mcp"
      ]
  },

## Development

### Building and Publishing

To prepare the package for distribution:

1. Sync dependencies and update lockfile:
```bash
uv sync
```

2. Build package distributions:
```bash
uv build
```

This will create source and wheel distributions in the `dist/` directory.

3. Publish to PyPI:
```bash
uv publish
```

Note: You'll need to set PyPI credentials via environment variables or command flags:
- Token: `--token` or `UV_PUBLISH_TOKEN`
- Or username/password: `--username`/`UV_PUBLISH_USERNAME` and `--password`/`UV_PUBLISH_PASSWORD`

### Automated Publishing with GitHub Actions

This repository includes a GitHub Actions workflow for automated publishing to PyPI. The workflow is triggered when:

1. A new GitHub Release is created
2. The workflow is manually triggered via the GitHub Actions interface

To set up automated publishing:

1. Generate a PyPI API token:
   - Go to https://pypi.org/manage/account/token/
   - Create a new token with scope limited to the `ddg-mcp` project
   - Copy the token value (you'll only see it once)

2. Add the token to your GitHub repository secrets:
   - Go to your repository on GitHub
   - Navigate to Settings > Secrets and variables > Actions
   - Click "New repository secret"
   - Name: `PYPI_API_TOKEN`
   - Value: Paste your PyPI token
   - Click "Add secret"

3. To publish a new version:
   - Update the version number in `pyproject.toml`
   - Create a new release on GitHub or manually trigger the workflow

### Debugging

Since MCP servers run over stdio, debugging can be challenging. For the best debugging
experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).


You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:

```bash
npx @modelcontextprotocol/inspector uv --directory /path/to/your/ddg-mcp run ddg-mcp
```


Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.

```

--------------------------------------------------------------------------------
/src/ddg_mcp/__init__.py:
--------------------------------------------------------------------------------

```python
from . import server
import asyncio

def main():
    """Main entry point for the package."""
    asyncio.run(server.main())

# Optionally expose other important items at package level
__all__ = ['main', 'server']
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[project]
name = "ddg-mcp"
version = "0.1.1"
description = "DuckDuckGo search API MCP"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
 "duckduckgo-search>=7.5.1",
 "mcp>=1.3.0",
]
[[project.authors]]
name = "Shannon Sands"
email = "[email protected]"

[build-system]
requires = [ "hatchling",]
build-backend = "hatchling.build"

[project.scripts]
ddg-mcp = "ddg_mcp:main"

```

--------------------------------------------------------------------------------
/.github/workflows/publish.yaml:
--------------------------------------------------------------------------------

```yaml
name: Publish to PyPI

on:
  release:
    types: [created]
  workflow_dispatch:  # Allow manual triggering

jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    
    steps:
    - name: Check out repository
      uses: actions/checkout@v4
      
    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: '3.11'
        
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install build twine
        
    - name: Build package
      run: |
        python -m build
        
    - name: Check distribution
      run: |
        python -m twine check dist/*
        
    - name: Publish to PyPI
      if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
      uses: pypa/gh-action-pypi-publish@release/v1
      with:
        user: __token__
        password: ${{ secrets.PYPI_API_TOKEN }}
        skip-existing: true
        verbose: true 
```

--------------------------------------------------------------------------------
/src/ddg_mcp/server.py:
--------------------------------------------------------------------------------

```python
import asyncio

from mcp.server.models import InitializationOptions
import mcp.types as types
from mcp.server import NotificationOptions, Server
from pydantic import AnyUrl
import mcp.server.stdio
from duckduckgo_search import DDGS

server = Server("ddg-mcp")

@server.list_resources()
async def handle_list_resources() -> list[types.Resource]:
    """
    List available resources.
    Currently, no resources are exposed.
    """
    return []

@server.list_prompts()
async def handle_list_prompts() -> list[types.Prompt]:
    """
    List available prompts.
    Each prompt can have optional arguments to customize its behavior.
    """
    return [
        types.Prompt(
            name="search-results-summary",
            description="Creates a summary of search results",
            arguments=[
                types.PromptArgument(
                    name="query",
                    description="Search query to summarize results for",
                    required=True,
                ),
                types.PromptArgument(
                    name="style",
                    description="Style of the summary (brief/detailed)",
                    required=False,
                )
            ],
        )
    ]

@server.get_prompt()
async def handle_get_prompt(
    name: str, arguments: dict[str, str] | None
) -> types.GetPromptResult:
    """
    Generate a prompt by combining arguments with server state.
    """
    if name == "search-results-summary":
        if not arguments or "query" not in arguments:
            raise ValueError("Missing required 'query' argument")
        
        query = arguments.get("query")
        style = arguments.get("style", "brief")
        detail_prompt = " Give extensive details." if style == "detailed" else ""
        
        # Perform search and get results
        ddgs = DDGS()
        results = ddgs.text(query, max_results=10)
        
        results_text = "\n\n".join([
            f"Title: {result.get('title', 'No title')}\n"
            f"URL: {result.get('href', 'No URL')}\n"
            f"Description: {result.get('body', 'No description')}"
            for result in results
        ])
        
        return types.GetPromptResult(
            description=f"Summarize search results for '{query}'",
            messages=[
                types.PromptMessage(
                    role="user",
                    content=types.TextContent(
                        type="text",
                        text=f"Here are the search results for '{query}'. Please summarize them{detail_prompt}:\n\n{results_text}",
                    ),
                )
            ],
        )
    else:
        raise ValueError(f"Unknown prompt: {name}")

@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
    """
    List available tools.
    Each tool specifies its arguments using JSON Schema validation.
    """
    return [
        types.Tool(
            name="ddg-text-search",
            description="Search the web for text results using DuckDuckGo",
            inputSchema={
                "type": "object",
                "properties": {
                    "keywords": {"type": "string", "description": "Search query keywords"},
                    "region": {"type": "string", "description": "Region code (e.g., wt-wt, us-en, uk-en)", "default": "wt-wt"},
                    "safesearch": {"type": "string", "enum": ["on", "moderate", "off"], "description": "Safe search level", "default": "moderate"},
                    "timelimit": {"type": "string", "enum": ["d", "w", "m", "y"], "description": "Time limit (d=day, w=week, m=month, y=year)"},
                    "max_results": {"type": "integer", "description": "Maximum number of results to return", "default": 10},
                },
                "required": ["keywords"],
            },
        ),
        types.Tool(
            name="ddg-image-search",
            description="Search the web for images using DuckDuckGo",
            inputSchema={
                "type": "object",
                "properties": {
                    "keywords": {"type": "string", "description": "Search query keywords"},
                    "region": {"type": "string", "description": "Region code (e.g., wt-wt, us-en, uk-en)", "default": "wt-wt"},
                    "safesearch": {"type": "string", "enum": ["on", "moderate", "off"], "description": "Safe search level", "default": "moderate"},
                    "timelimit": {"type": "string", "enum": ["d", "w", "m", "y"], "description": "Time limit (d=day, w=week, m=month, y=year)"},
                    "size": {"type": "string", "enum": ["Small", "Medium", "Large", "Wallpaper"], "description": "Image size"},
                    "color": {"type": "string", "enum": ["color", "Monochrome", "Red", "Orange", "Yellow", "Green", "Blue", "Purple", "Pink", "Brown", "Black", "Gray", "Teal", "White"], "description": "Image color"},
                    "type_image": {"type": "string", "enum": ["photo", "clipart", "gif", "transparent", "line"], "description": "Image type"},
                    "layout": {"type": "string", "enum": ["Square", "Tall", "Wide"], "description": "Image layout"},
                    "license_image": {"type": "string", "enum": ["any", "Public", "Share", "ShareCommercially", "Modify", "ModifyCommercially"], "description": "Image license type"},
                    "max_results": {"type": "integer", "description": "Maximum number of results to return", "default": 10},
                },
                "required": ["keywords"],
            },
        ),
        types.Tool(
            name="ddg-news-search",
            description="Search for news articles using DuckDuckGo",
            inputSchema={
                "type": "object",
                "properties": {
                    "keywords": {"type": "string", "description": "Search query keywords"},
                    "region": {"type": "string", "description": "Region code (e.g., wt-wt, us-en, uk-en)", "default": "wt-wt"},
                    "safesearch": {"type": "string", "enum": ["on", "moderate", "off"], "description": "Safe search level", "default": "moderate"},
                    "timelimit": {"type": "string", "enum": ["d", "w", "m"], "description": "Time limit (d=day, w=week, m=month)"},
                    "max_results": {"type": "integer", "description": "Maximum number of results to return", "default": 10},
                },
                "required": ["keywords"],
            },
        ),
        types.Tool(
            name="ddg-video-search",
            description="Search for videos using DuckDuckGo",
            inputSchema={
                "type": "object",
                "properties": {
                    "keywords": {"type": "string", "description": "Search query keywords"},
                    "region": {"type": "string", "description": "Region code (e.g., wt-wt, us-en, uk-en)", "default": "wt-wt"},
                    "safesearch": {"type": "string", "enum": ["on", "moderate", "off"], "description": "Safe search level", "default": "moderate"},
                    "timelimit": {"type": "string", "enum": ["d", "w", "m"], "description": "Time limit (d=day, w=week, m=month)"},
                    "resolution": {"type": "string", "enum": ["high", "standard"], "description": "Video resolution"},
                    "duration": {"type": "string", "enum": ["short", "medium", "long"], "description": "Video duration"},
                    "license_videos": {"type": "string", "enum": ["creativeCommon", "youtube"], "description": "Video license type"},
                    "max_results": {"type": "integer", "description": "Maximum number of results to return", "default": 10},
                },
                "required": ["keywords"],
            },
        ),
        types.Tool(
            name="ddg-ai-chat",
            description="Chat with DuckDuckGo AI",
            inputSchema={
                "type": "object",
                "properties": {
                    "keywords": {"type": "string", "description": "Message or question to send to the AI"},
                    "model": {"type": "string", "enum": ["gpt-4o-mini", "llama-3.3-70b", "claude-3-haiku", "o3-mini", "mistral-small-3"], "description": "AI model to use", "default": "gpt-4o-mini"},
                },
                "required": ["keywords"],
            },
        ),
    ]

@server.call_tool()
async def handle_call_tool(
    name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
    """
    Handle tool execution requests.
    """
    if not arguments:
        raise ValueError("Missing arguments")

    if name == "ddg-text-search":
        keywords = arguments.get("keywords")
        if not keywords:
            raise ValueError("Missing keywords")
        
        region = arguments.get("region", "wt-wt")
        safesearch = arguments.get("safesearch", "moderate")
        timelimit = arguments.get("timelimit")
        max_results = arguments.get("max_results", 10)
        
        # Perform search
        ddgs = DDGS()
        results = ddgs.text(
            keywords=keywords,
            region=region,
            safesearch=safesearch,
            timelimit=timelimit,
            max_results=max_results
        )
        
        # Format results
        formatted_results = f"Search results for '{keywords}':\n\n"
        for i, result in enumerate(results, 1):
            formatted_results += (
                f"{i}. {result.get('title', 'No title')}\n"
                f"   URL: {result.get('href', 'No URL')}\n"
                f"   {result.get('body', 'No description')}\n\n"
            )
        
        return [
            types.TextContent(
                type="text",
                text=formatted_results,
            )
        ]
    
    elif name == "ddg-image-search":
        keywords = arguments.get("keywords")
        if not keywords:
            raise ValueError("Missing keywords")
        
        region = arguments.get("region", "wt-wt")
        safesearch = arguments.get("safesearch", "moderate")
        timelimit = arguments.get("timelimit")
        size = arguments.get("size")
        color = arguments.get("color")
        type_image = arguments.get("type_image")
        layout = arguments.get("layout")
        license_image = arguments.get("license_image")
        max_results = arguments.get("max_results", 10)
        
        # Perform search
        ddgs = DDGS()
        results = ddgs.images(
            keywords=keywords,
            region=region,
            safesearch=safesearch,
            timelimit=timelimit,
            size=size,
            color=color,
            type_image=type_image,
            layout=layout,
            license_image=license_image,
            max_results=max_results
        )
        
        # Format results
        formatted_results = f"Image search results for '{keywords}':\n\n"
        
        text_results = []
        image_results = []
        
        for i, result in enumerate(results, 1):
            text_results.append(
                types.TextContent(
                    type="text",
                    text=f"{i}. {result.get('title', 'No title')}\n"
                         f"   Source: {result.get('source', 'Unknown')}\n"
                         f"   URL: {result.get('url', 'No URL')}\n"
                         f"   Size: {result.get('width', 'N/A')}x{result.get('height', 'N/A')}\n"
                )
            )
            
            image_url = result.get('image')
            if image_url:
                image_results.append(
                    types.ImageContent(
                        type="image",
                        url=image_url,
                        alt_text=result.get('title', 'Image search result')
                    )
                )
        
        # Interleave text and image results
        combined_results = []
        for text, image in zip(text_results, image_results):
            combined_results.extend([text, image])
        
        return combined_results
    
    elif name == "ddg-news-search":
        keywords = arguments.get("keywords")
        if not keywords:
            raise ValueError("Missing keywords")
        
        region = arguments.get("region", "wt-wt")
        safesearch = arguments.get("safesearch", "moderate")
        timelimit = arguments.get("timelimit")
        max_results = arguments.get("max_results", 10)
        
        # Perform search
        ddgs = DDGS()
        results = ddgs.news(
            keywords=keywords,
            region=region,
            safesearch=safesearch,
            timelimit=timelimit,
            max_results=max_results
        )
        
        # Format results
        formatted_results = f"News search results for '{keywords}':\n\n"
        for i, result in enumerate(results, 1):
            formatted_results += (
                f"{i}. {result.get('title', 'No title')}\n"
                f"   Source: {result.get('source', 'Unknown')}\n"
                f"   Date: {result.get('date', 'No date')}\n"
                f"   URL: {result.get('url', 'No URL')}\n"
                f"   {result.get('body', 'No description')}\n\n"
            )
        
        return [
            types.TextContent(
                type="text",
                text=formatted_results,
            )
        ]
    
    elif name == "ddg-video-search":
        keywords = arguments.get("keywords")
        if not keywords:
            raise ValueError("Missing keywords")
        
        region = arguments.get("region", "wt-wt")
        safesearch = arguments.get("safesearch", "moderate")
        timelimit = arguments.get("timelimit")
        resolution = arguments.get("resolution")
        duration = arguments.get("duration")
        license_videos = arguments.get("license_videos")
        max_results = arguments.get("max_results", 10)
        
        # Perform search
        ddgs = DDGS()
        results = ddgs.videos(
            keywords=keywords,
            region=region,
            safesearch=safesearch,
            timelimit=timelimit,
            resolution=resolution,
            duration=duration,
            license_videos=license_videos,
            max_results=max_results
        )
        
        # Format results
        formatted_results = f"Video search results for '{keywords}':\n\n"
        for i, result in enumerate(results, 1):
            formatted_results += (
                f"{i}. {result.get('title', 'No title')}\n"
                f"   Publisher: {result.get('publisher', 'Unknown')}\n"
                f"   Duration: {result.get('duration', 'Unknown')}\n"
                f"   URL: {result.get('content', 'No URL')}\n"
                f"   Published: {result.get('published', 'No date')}\n"
                f"   {result.get('description', 'No description')}\n\n"
            )
        
        return [
            types.TextContent(
                type="text",
                text=formatted_results,
            )
        ]
    
    elif name == "ddg-ai-chat":
        keywords = arguments.get("keywords")
        if not keywords:
            raise ValueError("Missing keywords")
        
        model = arguments.get("model", "gpt-4o-mini")
        
        # Perform AI chat
        ddgs = DDGS()
        result = ddgs.chat(
            keywords=keywords,
            model=model
        )
        
        return [
            types.TextContent(
                type="text",
                text=f"DuckDuckGo AI ({model}) response:\n\n{result}",
            )
        ]
    
    else:
        raise ValueError(f"Unknown tool: {name}")

async def main():
    # Run the server using stdin/stdout streams
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="ddg-mcp",
                server_version="0.1.0",
                capabilities=server.get_capabilities(
                    notification_options=NotificationOptions(),
                    experimental_capabilities={},
                ),
            ),
        )
```