ddgs-mcp-server 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ .venv
5
+ venv/
6
+ *.egg-info/
@@ -0,0 +1,20 @@
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ # Copy application code
10
+ COPY server.py .
11
+ COPY start_api.sh .
12
+
13
+ # Make startup script executable
14
+ RUN chmod +x start_api.sh
15
+
16
+ # Expose port
17
+ EXPOSE 8000
18
+
19
+ # Run the server
20
+ CMD ["./start_api.sh"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chirag Singhal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: ddgs-mcp-server
3
+ Version: 0.1.0
4
+ Summary: DuckDuckGo Search MCP Server
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: duckduckgo-search>=6.0.0
8
+ Requires-Dist: mcp>=1.0.0
9
+ Description-Content-Type: text/markdown
10
+
11
+ # DDGS MCP Server
12
+
13
+ A Model Context Protocol (MCP) server that provides DuckDuckGo Search capabilities to AI agents.
14
+
15
+ ## Features
16
+
17
+ - **Text Search**: General web search (`search_text`)
18
+ - **Image Search**: Find images (`search_images`)
19
+ - **Video Search**: Find videos (`search_videos`)
20
+ - **News Search**: Get latest news (`search_news`)
21
+ - **Book Search**: Search for books (`search_books`)
22
+
23
+ ## Installation & Usage
24
+
25
+ You can run this server directly using `uvx` without installing it globally.
26
+
27
+ ### VS Code (Claude Desktop / Cline)
28
+
29
+ Add this to your MCP settings file (e.g., `cline_mcp_settings.json` or `claude_desktop_config.json`):
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "ddgs-search": {
35
+ "command": "uvx",
36
+ "args": [
37
+ "ddgs-mcp-server"
38
+ ],
39
+ "disabled": false,
40
+ "alwaysAllow": []
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ ### Manual Execution
47
+
48
+ ```bash
49
+ uvx ddgs-mcp-server
50
+ ```
51
+
52
+ ## Development / Publishing
53
+
54
+ To build and publish this package to PyPI:
55
+
56
+ 1. **Build**:
57
+ ```bash
58
+ pip install build twine
59
+ python -m build
60
+ ```
61
+
62
+ 2. **Publish**:
63
+ ```bash
64
+ python -m twine upload dist/*
65
+ ```
@@ -0,0 +1,55 @@
1
+ # DDGS MCP Server
2
+
3
+ A Model Context Protocol (MCP) server that provides DuckDuckGo Search capabilities to AI agents.
4
+
5
+ ## Features
6
+
7
+ - **Text Search**: General web search (`search_text`)
8
+ - **Image Search**: Find images (`search_images`)
9
+ - **Video Search**: Find videos (`search_videos`)
10
+ - **News Search**: Get latest news (`search_news`)
11
+ - **Book Search**: Search for books (`search_books`)
12
+
13
+ ## Installation & Usage
14
+
15
+ You can run this server directly using `uvx` without installing it globally.
16
+
17
+ ### VS Code (Claude Desktop / Cline)
18
+
19
+ Add this to your MCP settings file (e.g., `cline_mcp_settings.json` or `claude_desktop_config.json`):
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "ddgs-search": {
25
+ "command": "uvx",
26
+ "args": [
27
+ "ddgs-mcp-server"
28
+ ],
29
+ "disabled": false,
30
+ "alwaysAllow": []
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ ### Manual Execution
37
+
38
+ ```bash
39
+ uvx ddgs-mcp-server
40
+ ```
41
+
42
+ ## Development / Publishing
43
+
44
+ To build and publish this package to PyPI:
45
+
46
+ 1. **Build**:
47
+ ```bash
48
+ pip install build twine
49
+ python -m build
50
+ ```
51
+
52
+ 2. **Publish**:
53
+ ```bash
54
+ python -m twine upload dist/*
55
+ ```
@@ -0,0 +1,12 @@
1
+ version: '3.8'
2
+
3
+ services:
4
+ ddgs-mcp:
5
+ build: .
6
+ ports:
7
+ - "8000:8000"
8
+ volumes:
9
+ - .:/app
10
+ environment:
11
+ - PYTHONUNBUFFERED=1
12
+ restart: always
@@ -0,0 +1,19 @@
1
+ import asyncio
2
+ import sys
3
+ from mcp.server.stdio import stdio_server
4
+ from server import server
5
+
6
+ async def main():
7
+ """
8
+ Main entry point for UVX / Stdio usage.
9
+ """
10
+ async with stdio_server() as (read_stream, write_stream):
11
+ await server.run(read_stream, write_stream, server.create_initialization_options())
12
+
13
+ if __name__ == "__main__":
14
+ try:
15
+ asyncio.run(main())
16
+ except KeyboardInterrupt:
17
+ pass
18
+ except Exception as e:
19
+ print(f"Error: {e}", file=sys.stderr)
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ddgs-mcp-server"
7
+ version = "0.1.0"
8
+ description = "DuckDuckGo Search MCP Server"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "mcp>=1.0.0",
13
+ "duckduckgo-search>=6.0.0"
14
+ ]
15
+
16
+ [project.scripts]
17
+ ddgs-mcp-server = "ddgs_mcp_server.main:main"
18
+
19
+ [tool.hatch.build.targets.wheel]
20
+ packages = ["src/ddgs_mcp_server"]
@@ -0,0 +1,5 @@
1
+ mcp>=1.0.0
2
+ duckduckgo-search>=6.0.0
3
+ fastapi>=0.110.0
4
+ uvicorn[standard]>=0.29.0
5
+ sse-starlette>=2.0.0
@@ -0,0 +1,216 @@
1
+
2
+ import asyncio
3
+ import json
4
+ import logging
5
+ import uuid
6
+ from typing import Optional, Literal
7
+
8
+ import uvicorn
9
+ from fastapi import FastAPI, Request
10
+ from fastapi.responses import JSONResponse
11
+ from sse_starlette.sse import EventSourceResponse
12
+
13
+ # MCP Imports
14
+ from mcp.server import Server
15
+ from mcp.server.sse import SseServerTransport
16
+ import mcp.types as types
17
+
18
+ # DDGS Import
19
+ from duckduckgo_search import DDGS
20
+
21
+ # Logging Configuration
22
+ logging.basicConfig(level=logging.INFO)
23
+ logger = logging.getLogger("ddgs-mcp")
24
+
25
+ app = FastAPI(title="DDGS MCP Server")
26
+
27
+ # MCP Server
28
+ server = Server("ddgs-mcp-server")
29
+
30
+ # --- DDGS Wrappers ---
31
+
32
+ @server.list_tools()
33
+ async def list_tools() -> list[types.Tool]:
34
+ return [
35
+ types.Tool(
36
+ name="search_text",
37
+ description="Perform a text search using DuckDuckGo. Use this for general web queries.",
38
+ inputSchema={
39
+ "type": "object",
40
+ "properties": {
41
+ "query": {"type": "string", "description": "Search query"},
42
+ "region": {"type": "string", "default": "us-en", "description": "e.g., us-en, uk-en"},
43
+ "safesearch": {"type": "string", "enum": ["on", "moderate", "off"], "default": "moderate"},
44
+ "timelimit": {"type": "string", "enum": ["d", "w", "m", "y"], "default": None},
45
+ "max_results": {"type": "integer", "default": 10}
46
+ },
47
+ "required": ["query"]
48
+ }
49
+ ),
50
+ types.Tool(
51
+ name="search_images",
52
+ description="Perform an image search using DuckDuckGo.",
53
+ inputSchema={
54
+ "type": "object",
55
+ "properties": {
56
+ "query": {"type": "string"},
57
+ "region": {"type": "string", "default": "us-en"},
58
+ "safesearch": {"type": "string", "default": "moderate"},
59
+ "timelimit": {"type": "string", "default": None},
60
+ "max_results": {"type": "integer", "default": 10}
61
+ },
62
+ "required": ["query"]
63
+ }
64
+ ),
65
+ types.Tool(
66
+ name="search_videos",
67
+ description="Perform a video search using DuckDuckGo.",
68
+ inputSchema={
69
+ "type": "object",
70
+ "properties": {
71
+ "query": {"type": "string"},
72
+ "region": {"type": "string", "default": "us-en"},
73
+ "safesearch": {"type": "string", "default": "moderate"},
74
+ "timelimit": {"type": "string", "default": None},
75
+ "max_results": {"type": "integer", "default": 10}
76
+ },
77
+ "required": ["query"]
78
+ }
79
+ ),
80
+ types.Tool(
81
+ name="search_news",
82
+ description="Perform a news search using DuckDuckGo.",
83
+ inputSchema={
84
+ "type": "object",
85
+ "properties": {
86
+ "query": {"type": "string"},
87
+ "region": {"type": "string", "default": "us-en"},
88
+ "safesearch": {"type": "string", "default": "moderate"},
89
+ "timelimit": {"type": "string", "default": None},
90
+ "max_results": {"type": "integer", "default": 10}
91
+ },
92
+ "required": ["query"]
93
+ }
94
+ ),
95
+ types.Tool(
96
+ name="search_books",
97
+ description="Perform a book search using DuckDuckGo (Anna's Archive backend).",
98
+ inputSchema={
99
+ "type": "object",
100
+ "properties": {
101
+ "query": {"type": "string"},
102
+ "max_results": {"type": "integer", "default": 10}
103
+ },
104
+ "required": ["query"]
105
+ }
106
+ )
107
+ ]
108
+
109
+ @server.call_tool()
110
+ async def call_tool(name: str, arguments: dict) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
111
+ logger.info(f"Calling tool: {name} with args: {arguments}")
112
+
113
+ query = arguments.get("query")
114
+ region = arguments.get("region", "us-en")
115
+ safesearch = arguments.get("safesearch", "moderate")
116
+ timelimit = arguments.get("timelimit")
117
+ max_results = arguments.get("max_results", 10)
118
+
119
+ try:
120
+ # Using context manager for DDGS
121
+ with DDGS() as ddgs:
122
+ results = []
123
+ if name == "search_text":
124
+ results = ddgs.text(keywords=query, region=region, safesearch=safesearch, timelimit=timelimit, max_results=max_results)
125
+ elif name == "search_images":
126
+ results = ddgs.images(keywords=query, region=region, safesearch=safesearch, timelimit=timelimit, max_results=max_results)
127
+ elif name == "search_videos":
128
+ results = ddgs.videos(keywords=query, region=region, safesearch=safesearch, timelimit=timelimit, max_results=max_results)
129
+ elif name == "search_news":
130
+ results = ddgs.news(keywords=query, region=region, safesearch=safesearch, timelimit=timelimit, max_results=max_results)
131
+ elif name == "search_books":
132
+ # Check for books method availability or fallback
133
+ if hasattr(ddgs, 'books'):
134
+ results = ddgs.books(keywords=query, max_results=max_results)
135
+ else:
136
+ return [types.TextContent(type="text", text="Error: 'books' search backend not available in this version of python-ddgs.")]
137
+ else:
138
+ raise ValueError(f"Unknown tool: {name}")
139
+
140
+ return [types.TextContent(type="text", text=json.dumps(results, indent=2))]
141
+
142
+ except Exception as e:
143
+ logger.error(f"Error executing {name}: {e}")
144
+ return [types.TextContent(type="text", text=f"Error performing search: {str(e)}")]
145
+
146
+
147
+ # --- SSE Transport Integration ---
148
+
149
+ class SessionManager:
150
+ """Simple in-memory session manager for SSE transports."""
151
+ def __init__(self):
152
+ self.sessions = {}
153
+
154
+ def add_session(self, session_id: str, transport: SseServerTransport):
155
+ self.sessions[session_id] = transport
156
+
157
+ def get_session(self, session_id: str) -> Optional[SseServerTransport]:
158
+ return self.sessions.get(session_id)
159
+
160
+ def remove_session(self, session_id: str):
161
+ if session_id in self.sessions:
162
+ del self.sessions[session_id]
163
+
164
+ session_manager = SessionManager()
165
+
166
+ @app.get("/sse")
167
+ async def handle_sse(request: Request):
168
+ import uuid
169
+
170
+ # Create a new transport for this connection
171
+ # The endpoint passed here is where the client should send messages (POST)
172
+ # We append the session ID to it so we can route correctly in the /messages handler
173
+ session_id = str(uuid.uuid4())
174
+ transport = SseServerTransport(f"/messages?session_id={session_id}")
175
+
176
+ async def sse_generator():
177
+ logger.info(f"New SSE connection: {session_id}")
178
+ session_manager.add_session(session_id, transport)
179
+
180
+ try:
181
+ # transport.connect_sse yields (read_stream, write_stream)
182
+ async with transport.connect_sse(request.scope, request.receive, request._send) as streams:
183
+ read_stream, write_stream = streams
184
+ # Run the MCP server for this session
185
+ await server.run(read_stream, write_stream, server.create_initialization_options())
186
+ except Exception as e:
187
+ logger.error(f"SSE session {session_id} error: {e}")
188
+ pass
189
+ finally:
190
+ logger.info(f"Closing SSE connection: {session_id}")
191
+ session_manager.remove_session(session_id)
192
+
193
+ return EventSourceResponse(sse_generator())
194
+
195
+ @app.post("/messages")
196
+ async def handle_messages(request: Request):
197
+ session_id = request.query_params.get("session_id")
198
+ if not session_id:
199
+ return JSONResponse(status_code=400, content={"error": "Missing session_id"})
200
+
201
+ transport = session_manager.get_session(session_id)
202
+ if not transport:
203
+ return JSONResponse(status_code=404, content={"error": "Session not found or expired"})
204
+
205
+ # Forward the request logic to the transport
206
+ # transport.handle_post_message processes the request body and pushes to the read stream
207
+ await transport.handle_post_message(request.scope, request.receive, request._send)
208
+ return JSONResponse(content={"status": "ok"})
209
+
210
+ @app.get("/health")
211
+ async def health():
212
+ return {"status": "ok", "active_sessions": len(session_manager.sessions)}
213
+
214
+ if __name__ == "__main__":
215
+ import uvicorn
216
+ uvicorn.run(app, host="0.0.0.0", port=8000)
File without changes
@@ -0,0 +1,20 @@
1
+
2
+ import asyncio
3
+ import sys
4
+ from mcp.server.stdio import stdio_server
5
+ from .server import server
6
+
7
+ async def run():
8
+ async with stdio_server() as (read_stream, write_stream):
9
+ await server.run(read_stream, write_stream, server.create_initialization_options())
10
+
11
+ def main():
12
+ try:
13
+ asyncio.run(run())
14
+ except KeyboardInterrupt:
15
+ pass
16
+ except Exception as e:
17
+ print(f"Error: {e}", file=sys.stderr)
18
+
19
+ if __name__ == "__main__":
20
+ main()
@@ -0,0 +1,126 @@
1
+
2
+ import json
3
+ import logging
4
+ from typing import Optional, Literal
5
+ from mcp.server import Server
6
+ import mcp.types as types
7
+ from duckduckgo_search import DDGS
8
+
9
+ # Logging Configuration
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger("ddgs-mcp")
12
+
13
+ # MCP Server
14
+ server = Server("ddgs-mcp-server")
15
+
16
+ @server.list_tools()
17
+ async def list_tools() -> list[types.Tool]:
18
+ return [
19
+ types.Tool(
20
+ name="search_text",
21
+ description="Perform a text search using DuckDuckGo. Use this for general web queries.",
22
+ inputSchema={
23
+ "type": "object",
24
+ "properties": {
25
+ "query": {"type": "string", "description": "Search query"},
26
+ "region": {"type": "string", "default": "us-en", "description": "e.g., us-en, uk-en"},
27
+ "safesearch": {"type": "string", "enum": ["on", "moderate", "off"], "default": "moderate"},
28
+ "timelimit": {"type": "string", "enum": ["d", "w", "m", "y"], "default": None},
29
+ "max_results": {"type": "integer", "default": 10}
30
+ },
31
+ "required": ["query"]
32
+ }
33
+ ),
34
+ types.Tool(
35
+ name="search_images",
36
+ description="Perform an image search using DuckDuckGo.",
37
+ inputSchema={
38
+ "type": "object",
39
+ "properties": {
40
+ "query": {"type": "string"},
41
+ "region": {"type": "string", "default": "us-en"},
42
+ "safesearch": {"type": "string", "default": "moderate"},
43
+ "timelimit": {"type": "string", "default": None},
44
+ "max_results": {"type": "integer", "default": 10}
45
+ },
46
+ "required": ["query"]
47
+ }
48
+ ),
49
+ types.Tool(
50
+ name="search_videos",
51
+ description="Perform a video search using DuckDuckGo.",
52
+ inputSchema={
53
+ "type": "object",
54
+ "properties": {
55
+ "query": {"type": "string"},
56
+ "region": {"type": "string", "default": "us-en"},
57
+ "safesearch": {"type": "string", "default": "moderate"},
58
+ "timelimit": {"type": "string", "default": None},
59
+ "max_results": {"type": "integer", "default": 10}
60
+ },
61
+ "required": ["query"]
62
+ }
63
+ ),
64
+ types.Tool(
65
+ name="search_news",
66
+ description="Perform a news search using DuckDuckGo.",
67
+ inputSchema={
68
+ "type": "object",
69
+ "properties": {
70
+ "query": {"type": "string"},
71
+ "region": {"type": "string", "default": "us-en"},
72
+ "safesearch": {"type": "string", "default": "moderate"},
73
+ "timelimit": {"type": "string", "default": None},
74
+ "max_results": {"type": "integer", "default": 10}
75
+ },
76
+ "required": ["query"]
77
+ }
78
+ ),
79
+ types.Tool(
80
+ name="search_books",
81
+ description="Perform a book search using DuckDuckGo (Anna's Archive backend).",
82
+ inputSchema={
83
+ "type": "object",
84
+ "properties": {
85
+ "query": {"type": "string"},
86
+ "max_results": {"type": "integer", "default": 10}
87
+ },
88
+ "required": ["query"]
89
+ }
90
+ )
91
+ ]
92
+
93
+ @server.call_tool()
94
+ async def call_tool(name: str, arguments: dict) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
95
+ logger.info(f"Calling tool: {name} with args: {arguments}")
96
+
97
+ query = arguments.get("query")
98
+ region = arguments.get("region", "us-en")
99
+ safesearch = arguments.get("safesearch", "moderate")
100
+ timelimit = arguments.get("timelimit")
101
+ max_results = arguments.get("max_results", 10)
102
+
103
+ try:
104
+ with DDGS() as ddgs:
105
+ results = []
106
+ if name == "search_text":
107
+ results = ddgs.text(keywords=query, region=region, safesearch=safesearch, timelimit=timelimit, max_results=max_results)
108
+ elif name == "search_images":
109
+ results = ddgs.images(keywords=query, region=region, safesearch=safesearch, timelimit=timelimit, max_results=max_results)
110
+ elif name == "search_videos":
111
+ results = ddgs.videos(keywords=query, region=region, safesearch=safesearch, timelimit=timelimit, max_results=max_results)
112
+ elif name == "search_news":
113
+ results = ddgs.news(keywords=query, region=region, safesearch=safesearch, timelimit=timelimit, max_results=max_results)
114
+ elif name == "search_books":
115
+ if hasattr(ddgs, 'books'):
116
+ results = ddgs.books(keywords=query, max_results=max_results)
117
+ else:
118
+ return [types.TextContent(type="text", text="Error: 'books' search backend not available in this version of python-ddgs.")]
119
+ else:
120
+ raise ValueError(f"Unknown tool: {name}")
121
+
122
+ return [types.TextContent(type="text", text=json.dumps(results, indent=2))]
123
+
124
+ except Exception as e:
125
+ logger.error(f"Error executing {name}: {e}")
126
+ return [types.TextContent(type="text", text=f"Error performing search: {str(e)}")]
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ uvicorn server:app --host 0.0.0.0 --port 8000 --reload