DuckDuckGo MCP Server
by misanthropic-ai
Verified
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={},
),
),
)