#
tokens: 8581/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
1 | 3.11
2 | 
```

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

```
 1 | # Python-generated files
 2 | __pycache__/
 3 | *.py[oc]
 4 | build/
 5 | dist/
 6 | wheels/
 7 | *.egg-info
 8 | 
 9 | # Virtual environments
10 | .venv
11 | 
```

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

```markdown
  1 | # ddg-mcp MCP server
  2 | 
  3 | DuckDuckGo search API MCP - A server that provides DuckDuckGo search capabilities through the Model Context Protocol.
  4 | 
  5 | ## Components
  6 | 
  7 | ### Prompts
  8 | 
  9 | The server provides the following prompts:
 10 | - **search-results-summary**: Creates a summary of DuckDuckGo search results
 11 |   - Required "query" argument for the search term
 12 |   - Optional "style" argument to control detail level (brief/detailed)
 13 | 
 14 | ### Tools
 15 | 
 16 | The server implements the following DuckDuckGo search tools:
 17 | 
 18 | - **ddg-text-search**: Search the web for text results using DuckDuckGo
 19 |   - Required: "keywords" - Search query keywords
 20 |   - Optional: "region", "safesearch", "timelimit", "max_results"
 21 |   
 22 | - **ddg-image-search**: Search the web for images using DuckDuckGo
 23 |   - Required: "keywords" - Search query keywords
 24 |   - Optional: "region", "safesearch", "timelimit", "size", "color", "type_image", "layout", "license_image", "max_results"
 25 |   
 26 | - **ddg-news-search**: Search for news articles using DuckDuckGo
 27 |   - Required: "keywords" - Search query keywords
 28 |   - Optional: "region", "safesearch", "timelimit", "max_results"
 29 |   
 30 | - **ddg-video-search**: Search for videos using DuckDuckGo
 31 |   - Required: "keywords" - Search query keywords
 32 |   - Optional: "region", "safesearch", "timelimit", "resolution", "duration", "license_videos", "max_results"
 33 |   
 34 | - **ddg-ai-chat**: Chat with DuckDuckGo AI
 35 |   - Required: "keywords" - Message or question to send to the AI
 36 |   - Optional: "model" - AI model to use (options: "gpt-4o-mini", "llama-3.3-70b", "claude-3-haiku", "o3-mini", "mistral-small-3")
 37 | 
 38 | ## Installation
 39 | 
 40 | ### Prerequisites
 41 | 
 42 | - Python 3.9 or higher
 43 | - [uv](https://github.com/astral-sh/uv) (recommended) or pip
 44 | 
 45 | ### Install from PyPI
 46 | 
 47 | ```bash
 48 | # Using uv
 49 | uv install ddg-mcp
 50 | 
 51 | # Using pip
 52 | pip install ddg-mcp
 53 | ```
 54 | 
 55 | ### Install from Source
 56 | 
 57 | 1. Clone the repository:
 58 | ```bash
 59 | git clone https://github.com/misanthropic-ai/ddg-mcp.git
 60 | cd ddg-mcp
 61 | ```
 62 | 
 63 | 2. Install the package:
 64 | ```bash
 65 | # Using uv
 66 | uv install -e .
 67 | 
 68 | # Using pip
 69 | pip install -e .
 70 | ```
 71 | 
 72 | ## Configuration
 73 | 
 74 | ### Required Dependencies
 75 | 
 76 | The server requires the `duckduckgo-search` package, which will be installed automatically when you install `ddg-mcp`.
 77 | 
 78 | If you need to install it manually:
 79 | ```bash
 80 | uv install duckduckgo-search
 81 | # or
 82 | pip install duckduckgo-search
 83 | ```
 84 | 
 85 | ## DuckDuckGo Search Parameters
 86 | 
 87 | ### Common Parameters
 88 | 
 89 | These parameters are available for most search types:
 90 | 
 91 | - **region**: Region code for localized results (default: "wt-wt")
 92 |   - Examples: "us-en" (US English), "uk-en" (UK English), "ru-ru" (Russian)
 93 |   - See [DuckDuckGo regions](https://duckduckgo.com/params) for more options
 94 | 
 95 | - **safesearch**: Content filtering level (default: "moderate")
 96 |   - "on": Strict filtering
 97 |   - "moderate": Moderate filtering
 98 |   - "off": No filtering
 99 | 
100 | - **timelimit**: Time range for results
101 |   - "d": Last day
102 |   - "w": Last week
103 |   - "m": Last month
104 |   - "y": Last year (not available for news/videos)
105 | 
106 | - **max_results**: Maximum number of results to return (default: 10)
107 | 
108 | ### Search Operators
109 | 
110 | You can use these operators in your search keywords:
111 | 
112 | - `cats dogs`: Results about cats or dogs
113 | - `"cats and dogs"`: Results for exact term "cats and dogs"
114 | - `cats -dogs`: Fewer dogs in results
115 | - `cats +dogs`: More dogs in results
116 | - `cats filetype:pdf`: PDFs about cats (supported: pdf, doc(x), xls(x), ppt(x), html)
117 | - `dogs site:example.com`: Pages about dogs from example.com
118 | - `cats -site:example.com`: Pages about cats, excluding example.com
119 | - `intitle:dogs`: Page title includes the word "dogs"
120 | - `inurl:cats`: Page URL includes the word "cats"
121 | 
122 | ### Image Search Specific Parameters
123 | 
124 | - **size**: "Small", "Medium", "Large", "Wallpaper"
125 | - **color**: "color", "Monochrome", "Red", "Orange", "Yellow", "Green", "Blue", "Purple", "Pink", "Brown", "Black", "Gray", "Teal", "White"
126 | - **type_image**: "photo", "clipart", "gif", "transparent", "line"
127 | - **layout**: "Square", "Tall", "Wide"
128 | - **license_image**: "any", "Public", "Share", "ShareCommercially", "Modify", "ModifyCommercially"
129 | 
130 | ### Video Search Specific Parameters
131 | 
132 | - **resolution**: "high", "standard"
133 | - **duration**: "short", "medium", "long"
134 | - **license_videos**: "creativeCommon", "youtube"
135 | 
136 | ### AI Chat Models
137 | 
138 | - **gpt-4o-mini**: OpenAI's GPT-4o mini model
139 | - **llama-3.3-70b**: Meta's Llama 3.3 70B model
140 | - **claude-3-haiku**: Anthropic's Claude 3 Haiku model
141 | - **o3-mini**: OpenAI's O3 mini model
142 | - **mistral-small-3**: Mistral AI's small model
143 | 
144 | ## Quickstart
145 | 
146 | ### Install
147 | 
148 | #### Claude Desktop
149 | 
150 | On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
151 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
152 | 
153 | <details>
154 |   <summary>Development/Unpublished Servers Configuration</summary>
155 |   ```
156 |   "mcpServers": {
157 |     "ddg-mcp": {
158 |       "command": "uv",
159 |       "args": [
160 |         "--directory",
161 |         "/Users/shannon/Workspace/artivus/ddg-mcp",
162 |         "run",
163 |         "ddg-mcp"
164 |       ]
165 |     }
166 |   }
167 |   ```
168 | </details>
169 | 
170 | <details>
171 |   <summary>Published Servers Configuration</summary>
172 |   ```
173 |   "mcpServers": {
174 |     "ddg-mcp": {
175 |       "command": "uvx",
176 |       "args": [
177 |         "ddg-mcp"
178 |       ]
179 |     }
180 |   }
181 |   ```
182 | </details>
183 | 
184 | ## Usage Examples
185 | 
186 | ### Text Search
187 | 
188 | ```
189 | Use the ddg-text-search tool to search for "climate change solutions"
190 | ```
191 | 
192 | Advanced example:
193 | ```
194 | 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
195 | ```
196 | 
197 | ### Image Search
198 | 
199 | ```
200 | Use the ddg-image-search tool to find images of "renewable energy" with color set to "Green"
201 | ```
202 | 
203 | Advanced example:
204 | ```
205 | 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"
206 | ```
207 | 
208 | ### News Search
209 | 
210 | ```
211 | Use the ddg-news-search tool to find recent news about "artificial intelligence" from the last day
212 | ```
213 | 
214 | Advanced example:
215 | ```
216 | Use the ddg-news-search tool to search for "space exploration" with region "uk-en", timelimit "w", and max_results 15
217 | ```
218 | 
219 | ### Video Search
220 | 
221 | ```
222 | Use the ddg-video-search tool to find videos about "machine learning tutorials" with duration set to "medium"
223 | ```
224 | 
225 | Advanced example:
226 | ```
227 | Use the ddg-video-search tool to search for "cooking recipes" with resolution "high", duration "short", license_videos "creativeCommon", and max_results 10
228 | ```
229 | 
230 | ### AI Chat
231 | 
232 | ```
233 | Use the ddg-ai-chat tool to ask "What are the latest developments in quantum computing?" using the claude-3-haiku model
234 | ```
235 | 
236 | ### Search Results Summary
237 | 
238 | ```
239 | Use the search-results-summary prompt with query "space exploration" and style "detailed"
240 | ```
241 | 
242 | ## Claude config
243 | "ddg-mcp": {
244 |       "command": "uv",
245 |       "args": [
246 |         "--directory",
247 |         "/PATH/TO/YOUR/INSTALLATION/ddg-mcp",
248 |         "run",
249 |         "ddg-mcp"
250 |       ]
251 |   },
252 | 
253 | ## Development
254 | 
255 | ### Building and Publishing
256 | 
257 | To prepare the package for distribution:
258 | 
259 | 1. Sync dependencies and update lockfile:
260 | ```bash
261 | uv sync
262 | ```
263 | 
264 | 2. Build package distributions:
265 | ```bash
266 | uv build
267 | ```
268 | 
269 | This will create source and wheel distributions in the `dist/` directory.
270 | 
271 | 3. Publish to PyPI:
272 | ```bash
273 | uv publish
274 | ```
275 | 
276 | Note: You'll need to set PyPI credentials via environment variables or command flags:
277 | - Token: `--token` or `UV_PUBLISH_TOKEN`
278 | - Or username/password: `--username`/`UV_PUBLISH_USERNAME` and `--password`/`UV_PUBLISH_PASSWORD`
279 | 
280 | ### Automated Publishing with GitHub Actions
281 | 
282 | This repository includes a GitHub Actions workflow for automated publishing to PyPI. The workflow is triggered when:
283 | 
284 | 1. A new GitHub Release is created
285 | 2. The workflow is manually triggered via the GitHub Actions interface
286 | 
287 | To set up automated publishing:
288 | 
289 | 1. Generate a PyPI API token:
290 |    - Go to https://pypi.org/manage/account/token/
291 |    - Create a new token with scope limited to the `ddg-mcp` project
292 |    - Copy the token value (you'll only see it once)
293 | 
294 | 2. Add the token to your GitHub repository secrets:
295 |    - Go to your repository on GitHub
296 |    - Navigate to Settings > Secrets and variables > Actions
297 |    - Click "New repository secret"
298 |    - Name: `PYPI_API_TOKEN`
299 |    - Value: Paste your PyPI token
300 |    - Click "Add secret"
301 | 
302 | 3. To publish a new version:
303 |    - Update the version number in `pyproject.toml`
304 |    - Create a new release on GitHub or manually trigger the workflow
305 | 
306 | ### Debugging
307 | 
308 | Since MCP servers run over stdio, debugging can be challenging. For the best debugging
309 | experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
310 | 
311 | 
312 | You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:
313 | 
314 | ```bash
315 | npx @modelcontextprotocol/inspector uv --directory /path/to/your/ddg-mcp run ddg-mcp
316 | ```
317 | 
318 | 
319 | Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.
320 | 
```

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

```python
1 | from . import server
2 | import asyncio
3 | 
4 | def main():
5 |     """Main entry point for the package."""
6 |     asyncio.run(server.main())
7 | 
8 | # Optionally expose other important items at package level
9 | __all__ = ['main', 'server']
```

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

```toml
 1 | [project]
 2 | name = "ddg-mcp"
 3 | version = "0.1.1"
 4 | description = "DuckDuckGo search API MCP"
 5 | readme = "README.md"
 6 | requires-python = ">=3.11"
 7 | dependencies = [
 8 |  "duckduckgo-search>=7.5.1",
 9 |  "mcp>=1.3.0",
10 | ]
11 | [[project.authors]]
12 | name = "Shannon Sands"
13 | email = "[email protected]"
14 | 
15 | [build-system]
16 | requires = [ "hatchling",]
17 | build-backend = "hatchling.build"
18 | 
19 | [project.scripts]
20 | ddg-mcp = "ddg_mcp:main"
21 | 
```

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

```yaml
 1 | name: Publish to PyPI
 2 | 
 3 | on:
 4 |   release:
 5 |     types: [created]
 6 |   workflow_dispatch:  # Allow manual triggering
 7 | 
 8 | jobs:
 9 |   build-and-publish:
10 |     runs-on: ubuntu-latest
11 |     
12 |     steps:
13 |     - name: Check out repository
14 |       uses: actions/checkout@v4
15 |       
16 |     - name: Set up Python
17 |       uses: actions/setup-python@v5
18 |       with:
19 |         python-version: '3.11'
20 |         
21 |     - name: Install dependencies
22 |       run: |
23 |         python -m pip install --upgrade pip
24 |         pip install build twine
25 |         
26 |     - name: Build package
27 |       run: |
28 |         python -m build
29 |         
30 |     - name: Check distribution
31 |       run: |
32 |         python -m twine check dist/*
33 |         
34 |     - name: Publish to PyPI
35 |       if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
36 |       uses: pypa/gh-action-pypi-publish@release/v1
37 |       with:
38 |         user: __token__
39 |         password: ${{ secrets.PYPI_API_TOKEN }}
40 |         skip-existing: true
41 |         verbose: true 
```

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

```python
  1 | import asyncio
  2 | 
  3 | from mcp.server.models import InitializationOptions
  4 | import mcp.types as types
  5 | from mcp.server import NotificationOptions, Server
  6 | from pydantic import AnyUrl
  7 | import mcp.server.stdio
  8 | from duckduckgo_search import DDGS
  9 | 
 10 | server = Server("ddg-mcp")
 11 | 
 12 | @server.list_resources()
 13 | async def handle_list_resources() -> list[types.Resource]:
 14 |     """
 15 |     List available resources.
 16 |     Currently, no resources are exposed.
 17 |     """
 18 |     return []
 19 | 
 20 | @server.list_prompts()
 21 | async def handle_list_prompts() -> list[types.Prompt]:
 22 |     """
 23 |     List available prompts.
 24 |     Each prompt can have optional arguments to customize its behavior.
 25 |     """
 26 |     return [
 27 |         types.Prompt(
 28 |             name="search-results-summary",
 29 |             description="Creates a summary of search results",
 30 |             arguments=[
 31 |                 types.PromptArgument(
 32 |                     name="query",
 33 |                     description="Search query to summarize results for",
 34 |                     required=True,
 35 |                 ),
 36 |                 types.PromptArgument(
 37 |                     name="style",
 38 |                     description="Style of the summary (brief/detailed)",
 39 |                     required=False,
 40 |                 )
 41 |             ],
 42 |         )
 43 |     ]
 44 | 
 45 | @server.get_prompt()
 46 | async def handle_get_prompt(
 47 |     name: str, arguments: dict[str, str] | None
 48 | ) -> types.GetPromptResult:
 49 |     """
 50 |     Generate a prompt by combining arguments with server state.
 51 |     """
 52 |     if name == "search-results-summary":
 53 |         if not arguments or "query" not in arguments:
 54 |             raise ValueError("Missing required 'query' argument")
 55 |         
 56 |         query = arguments.get("query")
 57 |         style = arguments.get("style", "brief")
 58 |         detail_prompt = " Give extensive details." if style == "detailed" else ""
 59 |         
 60 |         # Perform search and get results
 61 |         ddgs = DDGS()
 62 |         results = ddgs.text(query, max_results=10)
 63 |         
 64 |         results_text = "\n\n".join([
 65 |             f"Title: {result.get('title', 'No title')}\n"
 66 |             f"URL: {result.get('href', 'No URL')}\n"
 67 |             f"Description: {result.get('body', 'No description')}"
 68 |             for result in results
 69 |         ])
 70 |         
 71 |         return types.GetPromptResult(
 72 |             description=f"Summarize search results for '{query}'",
 73 |             messages=[
 74 |                 types.PromptMessage(
 75 |                     role="user",
 76 |                     content=types.TextContent(
 77 |                         type="text",
 78 |                         text=f"Here are the search results for '{query}'. Please summarize them{detail_prompt}:\n\n{results_text}",
 79 |                     ),
 80 |                 )
 81 |             ],
 82 |         )
 83 |     else:
 84 |         raise ValueError(f"Unknown prompt: {name}")
 85 | 
 86 | @server.list_tools()
 87 | async def handle_list_tools() -> list[types.Tool]:
 88 |     """
 89 |     List available tools.
 90 |     Each tool specifies its arguments using JSON Schema validation.
 91 |     """
 92 |     return [
 93 |         types.Tool(
 94 |             name="ddg-text-search",
 95 |             description="Search the web for text results using DuckDuckGo",
 96 |             inputSchema={
 97 |                 "type": "object",
 98 |                 "properties": {
 99 |                     "keywords": {"type": "string", "description": "Search query keywords"},
100 |                     "region": {"type": "string", "description": "Region code (e.g., wt-wt, us-en, uk-en)", "default": "wt-wt"},
101 |                     "safesearch": {"type": "string", "enum": ["on", "moderate", "off"], "description": "Safe search level", "default": "moderate"},
102 |                     "timelimit": {"type": "string", "enum": ["d", "w", "m", "y"], "description": "Time limit (d=day, w=week, m=month, y=year)"},
103 |                     "max_results": {"type": "integer", "description": "Maximum number of results to return", "default": 10},
104 |                 },
105 |                 "required": ["keywords"],
106 |             },
107 |         ),
108 |         types.Tool(
109 |             name="ddg-image-search",
110 |             description="Search the web for images using DuckDuckGo",
111 |             inputSchema={
112 |                 "type": "object",
113 |                 "properties": {
114 |                     "keywords": {"type": "string", "description": "Search query keywords"},
115 |                     "region": {"type": "string", "description": "Region code (e.g., wt-wt, us-en, uk-en)", "default": "wt-wt"},
116 |                     "safesearch": {"type": "string", "enum": ["on", "moderate", "off"], "description": "Safe search level", "default": "moderate"},
117 |                     "timelimit": {"type": "string", "enum": ["d", "w", "m", "y"], "description": "Time limit (d=day, w=week, m=month, y=year)"},
118 |                     "size": {"type": "string", "enum": ["Small", "Medium", "Large", "Wallpaper"], "description": "Image size"},
119 |                     "color": {"type": "string", "enum": ["color", "Monochrome", "Red", "Orange", "Yellow", "Green", "Blue", "Purple", "Pink", "Brown", "Black", "Gray", "Teal", "White"], "description": "Image color"},
120 |                     "type_image": {"type": "string", "enum": ["photo", "clipart", "gif", "transparent", "line"], "description": "Image type"},
121 |                     "layout": {"type": "string", "enum": ["Square", "Tall", "Wide"], "description": "Image layout"},
122 |                     "license_image": {"type": "string", "enum": ["any", "Public", "Share", "ShareCommercially", "Modify", "ModifyCommercially"], "description": "Image license type"},
123 |                     "max_results": {"type": "integer", "description": "Maximum number of results to return", "default": 10},
124 |                 },
125 |                 "required": ["keywords"],
126 |             },
127 |         ),
128 |         types.Tool(
129 |             name="ddg-news-search",
130 |             description="Search for news articles using DuckDuckGo",
131 |             inputSchema={
132 |                 "type": "object",
133 |                 "properties": {
134 |                     "keywords": {"type": "string", "description": "Search query keywords"},
135 |                     "region": {"type": "string", "description": "Region code (e.g., wt-wt, us-en, uk-en)", "default": "wt-wt"},
136 |                     "safesearch": {"type": "string", "enum": ["on", "moderate", "off"], "description": "Safe search level", "default": "moderate"},
137 |                     "timelimit": {"type": "string", "enum": ["d", "w", "m"], "description": "Time limit (d=day, w=week, m=month)"},
138 |                     "max_results": {"type": "integer", "description": "Maximum number of results to return", "default": 10},
139 |                 },
140 |                 "required": ["keywords"],
141 |             },
142 |         ),
143 |         types.Tool(
144 |             name="ddg-video-search",
145 |             description="Search for videos using DuckDuckGo",
146 |             inputSchema={
147 |                 "type": "object",
148 |                 "properties": {
149 |                     "keywords": {"type": "string", "description": "Search query keywords"},
150 |                     "region": {"type": "string", "description": "Region code (e.g., wt-wt, us-en, uk-en)", "default": "wt-wt"},
151 |                     "safesearch": {"type": "string", "enum": ["on", "moderate", "off"], "description": "Safe search level", "default": "moderate"},
152 |                     "timelimit": {"type": "string", "enum": ["d", "w", "m"], "description": "Time limit (d=day, w=week, m=month)"},
153 |                     "resolution": {"type": "string", "enum": ["high", "standard"], "description": "Video resolution"},
154 |                     "duration": {"type": "string", "enum": ["short", "medium", "long"], "description": "Video duration"},
155 |                     "license_videos": {"type": "string", "enum": ["creativeCommon", "youtube"], "description": "Video license type"},
156 |                     "max_results": {"type": "integer", "description": "Maximum number of results to return", "default": 10},
157 |                 },
158 |                 "required": ["keywords"],
159 |             },
160 |         ),
161 |         types.Tool(
162 |             name="ddg-ai-chat",
163 |             description="Chat with DuckDuckGo AI",
164 |             inputSchema={
165 |                 "type": "object",
166 |                 "properties": {
167 |                     "keywords": {"type": "string", "description": "Message or question to send to the AI"},
168 |                     "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"},
169 |                 },
170 |                 "required": ["keywords"],
171 |             },
172 |         ),
173 |     ]
174 | 
175 | @server.call_tool()
176 | async def handle_call_tool(
177 |     name: str, arguments: dict | None
178 | ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
179 |     """
180 |     Handle tool execution requests.
181 |     """
182 |     if not arguments:
183 |         raise ValueError("Missing arguments")
184 | 
185 |     if name == "ddg-text-search":
186 |         keywords = arguments.get("keywords")
187 |         if not keywords:
188 |             raise ValueError("Missing keywords")
189 |         
190 |         region = arguments.get("region", "wt-wt")
191 |         safesearch = arguments.get("safesearch", "moderate")
192 |         timelimit = arguments.get("timelimit")
193 |         max_results = arguments.get("max_results", 10)
194 |         
195 |         # Perform search
196 |         ddgs = DDGS()
197 |         results = ddgs.text(
198 |             keywords=keywords,
199 |             region=region,
200 |             safesearch=safesearch,
201 |             timelimit=timelimit,
202 |             max_results=max_results
203 |         )
204 |         
205 |         # Format results
206 |         formatted_results = f"Search results for '{keywords}':\n\n"
207 |         for i, result in enumerate(results, 1):
208 |             formatted_results += (
209 |                 f"{i}. {result.get('title', 'No title')}\n"
210 |                 f"   URL: {result.get('href', 'No URL')}\n"
211 |                 f"   {result.get('body', 'No description')}\n\n"
212 |             )
213 |         
214 |         return [
215 |             types.TextContent(
216 |                 type="text",
217 |                 text=formatted_results,
218 |             )
219 |         ]
220 |     
221 |     elif name == "ddg-image-search":
222 |         keywords = arguments.get("keywords")
223 |         if not keywords:
224 |             raise ValueError("Missing keywords")
225 |         
226 |         region = arguments.get("region", "wt-wt")
227 |         safesearch = arguments.get("safesearch", "moderate")
228 |         timelimit = arguments.get("timelimit")
229 |         size = arguments.get("size")
230 |         color = arguments.get("color")
231 |         type_image = arguments.get("type_image")
232 |         layout = arguments.get("layout")
233 |         license_image = arguments.get("license_image")
234 |         max_results = arguments.get("max_results", 10)
235 |         
236 |         # Perform search
237 |         ddgs = DDGS()
238 |         results = ddgs.images(
239 |             keywords=keywords,
240 |             region=region,
241 |             safesearch=safesearch,
242 |             timelimit=timelimit,
243 |             size=size,
244 |             color=color,
245 |             type_image=type_image,
246 |             layout=layout,
247 |             license_image=license_image,
248 |             max_results=max_results
249 |         )
250 |         
251 |         # Format results
252 |         formatted_results = f"Image search results for '{keywords}':\n\n"
253 |         
254 |         text_results = []
255 |         image_results = []
256 |         
257 |         for i, result in enumerate(results, 1):
258 |             text_results.append(
259 |                 types.TextContent(
260 |                     type="text",
261 |                     text=f"{i}. {result.get('title', 'No title')}\n"
262 |                          f"   Source: {result.get('source', 'Unknown')}\n"
263 |                          f"   URL: {result.get('url', 'No URL')}\n"
264 |                          f"   Size: {result.get('width', 'N/A')}x{result.get('height', 'N/A')}\n"
265 |                 )
266 |             )
267 |             
268 |             image_url = result.get('image')
269 |             if image_url:
270 |                 image_results.append(
271 |                     types.ImageContent(
272 |                         type="image",
273 |                         url=image_url,
274 |                         alt_text=result.get('title', 'Image search result')
275 |                     )
276 |                 )
277 |         
278 |         # Interleave text and image results
279 |         combined_results = []
280 |         for text, image in zip(text_results, image_results):
281 |             combined_results.extend([text, image])
282 |         
283 |         return combined_results
284 |     
285 |     elif name == "ddg-news-search":
286 |         keywords = arguments.get("keywords")
287 |         if not keywords:
288 |             raise ValueError("Missing keywords")
289 |         
290 |         region = arguments.get("region", "wt-wt")
291 |         safesearch = arguments.get("safesearch", "moderate")
292 |         timelimit = arguments.get("timelimit")
293 |         max_results = arguments.get("max_results", 10)
294 |         
295 |         # Perform search
296 |         ddgs = DDGS()
297 |         results = ddgs.news(
298 |             keywords=keywords,
299 |             region=region,
300 |             safesearch=safesearch,
301 |             timelimit=timelimit,
302 |             max_results=max_results
303 |         )
304 |         
305 |         # Format results
306 |         formatted_results = f"News search results for '{keywords}':\n\n"
307 |         for i, result in enumerate(results, 1):
308 |             formatted_results += (
309 |                 f"{i}. {result.get('title', 'No title')}\n"
310 |                 f"   Source: {result.get('source', 'Unknown')}\n"
311 |                 f"   Date: {result.get('date', 'No date')}\n"
312 |                 f"   URL: {result.get('url', 'No URL')}\n"
313 |                 f"   {result.get('body', 'No description')}\n\n"
314 |             )
315 |         
316 |         return [
317 |             types.TextContent(
318 |                 type="text",
319 |                 text=formatted_results,
320 |             )
321 |         ]
322 |     
323 |     elif name == "ddg-video-search":
324 |         keywords = arguments.get("keywords")
325 |         if not keywords:
326 |             raise ValueError("Missing keywords")
327 |         
328 |         region = arguments.get("region", "wt-wt")
329 |         safesearch = arguments.get("safesearch", "moderate")
330 |         timelimit = arguments.get("timelimit")
331 |         resolution = arguments.get("resolution")
332 |         duration = arguments.get("duration")
333 |         license_videos = arguments.get("license_videos")
334 |         max_results = arguments.get("max_results", 10)
335 |         
336 |         # Perform search
337 |         ddgs = DDGS()
338 |         results = ddgs.videos(
339 |             keywords=keywords,
340 |             region=region,
341 |             safesearch=safesearch,
342 |             timelimit=timelimit,
343 |             resolution=resolution,
344 |             duration=duration,
345 |             license_videos=license_videos,
346 |             max_results=max_results
347 |         )
348 |         
349 |         # Format results
350 |         formatted_results = f"Video search results for '{keywords}':\n\n"
351 |         for i, result in enumerate(results, 1):
352 |             formatted_results += (
353 |                 f"{i}. {result.get('title', 'No title')}\n"
354 |                 f"   Publisher: {result.get('publisher', 'Unknown')}\n"
355 |                 f"   Duration: {result.get('duration', 'Unknown')}\n"
356 |                 f"   URL: {result.get('content', 'No URL')}\n"
357 |                 f"   Published: {result.get('published', 'No date')}\n"
358 |                 f"   {result.get('description', 'No description')}\n\n"
359 |             )
360 |         
361 |         return [
362 |             types.TextContent(
363 |                 type="text",
364 |                 text=formatted_results,
365 |             )
366 |         ]
367 |     
368 |     elif name == "ddg-ai-chat":
369 |         keywords = arguments.get("keywords")
370 |         if not keywords:
371 |             raise ValueError("Missing keywords")
372 |         
373 |         model = arguments.get("model", "gpt-4o-mini")
374 |         
375 |         # Perform AI chat
376 |         ddgs = DDGS()
377 |         result = ddgs.chat(
378 |             keywords=keywords,
379 |             model=model
380 |         )
381 |         
382 |         return [
383 |             types.TextContent(
384 |                 type="text",
385 |                 text=f"DuckDuckGo AI ({model}) response:\n\n{result}",
386 |             )
387 |         ]
388 |     
389 |     else:
390 |         raise ValueError(f"Unknown tool: {name}")
391 | 
392 | async def main():
393 |     # Run the server using stdin/stdout streams
394 |     async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
395 |         await server.run(
396 |             read_stream,
397 |             write_stream,
398 |             InitializationOptions(
399 |                 server_name="ddg-mcp",
400 |                 server_version="0.1.0",
401 |                 capabilities=server.get_capabilities(
402 |                     notification_options=NotificationOptions(),
403 |                     experimental_capabilities={},
404 |                 ),
405 |             ),
406 |         )
```