# 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 | ) ```