# 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={},
),
),
)
```