ddgs-mcp-server 0.2.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,15 @@
1
+ # --- PyPI Publishing Secrets ---
2
+ # Required to publish the package to PyPI.
3
+ # Get this from: https://pypi.org/manage/account/token/
4
+ # 1. Log in to PyPI.
5
+ # 2. Go to Account Settings -> API Tokens.
6
+ # 3. Create a new token (scope: "Entire account" for new projects).
7
+ # 4. Copy the token (starts with pypi-).
8
+ TWINE_USERNAME=__token__
9
+ TWINE_PASSWORD=pypi-xxxxxxxxxxxxxxxx
10
+
11
+ # --- Application Configuration (Optional) ---
12
+ # If you are running this in a restricted network or need to bypass rate limits.
13
+ # DDGS supports http/https/socks5 proxies.
14
+ # Example: http://user:pass@10.10.1.10:3128
15
+ DDGS_PROXY=
@@ -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,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: ddgs-mcp-server
3
+ Version: 0.2.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
+ - **search_text**: advanced metasearch using `bing`, `brave`, `duckduckgo`, `google`, `mojeek`, `yahoo`, `yandex`, `wikipedia`.
18
+
19
+
20
+ ## Installation & Usage
21
+
22
+ You can run this server directly using `uvx` without installing it globally.
23
+
24
+ ### VS Code (Claude Desktop / Cline)
25
+
26
+ Add this to your MCP settings file (e.g., `cline_mcp_settings.json` or `claude_desktop_config.json`):
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "ddgs-search": {
32
+ "command": "uvx",
33
+ "args": [
34
+ "ddgs-mcp-server"
35
+ ],
36
+ "disabled": false,
37
+ "alwaysAllow": []
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ ### Manual Execution
44
+
45
+ ```bash
46
+ uvx ddgs-mcp-server
47
+ ```
48
+
49
+
50
+ ## Secrets & Configuration
51
+
52
+ This project technically **does not require API keys** to run locally, as it scrapes DuckDuckGo. However, for **publishing** or **proxy usage**, you should configure your environment.
53
+
54
+ ### 1. Set up Secrets
55
+ Copy the example file:
56
+ ```bash
57
+ cp .env.example .env
58
+ ```
59
+
60
+ ### 2. Required Tokens
61
+
62
+ | Token | Purpose | How to Get It |
63
+ | :--- | :--- | :--- |
64
+ | **PyPI API Token** | Publishing to PyPI | 1. Go to [PyPI Account Settings](https://pypi.org/manage/account/token/)<br>2. Select "Add API Token"<br>3. Scope to "Entire account" (for first publish)<br>4. Set as `TWINE_PASSWORD` in `.env` |
65
+ | **Proxy URL** | Bypassing Blocks (Optional) | Use any HTTP/SOCKS5 proxy provider if you encounter rate limits. |
66
+
67
+ ## Development / Publishing
68
+
69
+ To build and publish this package to PyPI (using the secrets from above):
70
+
71
+ 1. **Build**:
72
+ ```bash
73
+ pip install build twine
74
+ python -m build
75
+ ```
76
+
77
+ 2. **Publish** (loads secrets from .env if you export them, or prompts you):
78
+ ```bash
79
+ # If using .env variables (PowerShell)
80
+ # $env:TWINE_USERNAME = "__token__"
81
+ # $env:TWINE_PASSWORD = "pypi-..."
82
+
83
+ python -m twine upload dist/*
84
+ ```
@@ -0,0 +1,74 @@
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
+ - **search_text**: advanced metasearch using `bing`, `brave`, `duckduckgo`, `google`, `mojeek`, `yahoo`, `yandex`, `wikipedia`.
8
+
9
+
10
+ ## Installation & Usage
11
+
12
+ You can run this server directly using `uvx` without installing it globally.
13
+
14
+ ### VS Code (Claude Desktop / Cline)
15
+
16
+ Add this to your MCP settings file (e.g., `cline_mcp_settings.json` or `claude_desktop_config.json`):
17
+
18
+ ```json
19
+ {
20
+ "mcpServers": {
21
+ "ddgs-search": {
22
+ "command": "uvx",
23
+ "args": [
24
+ "ddgs-mcp-server"
25
+ ],
26
+ "disabled": false,
27
+ "alwaysAllow": []
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ ### Manual Execution
34
+
35
+ ```bash
36
+ uvx ddgs-mcp-server
37
+ ```
38
+
39
+
40
+ ## Secrets & Configuration
41
+
42
+ This project technically **does not require API keys** to run locally, as it scrapes DuckDuckGo. However, for **publishing** or **proxy usage**, you should configure your environment.
43
+
44
+ ### 1. Set up Secrets
45
+ Copy the example file:
46
+ ```bash
47
+ cp .env.example .env
48
+ ```
49
+
50
+ ### 2. Required Tokens
51
+
52
+ | Token | Purpose | How to Get It |
53
+ | :--- | :--- | :--- |
54
+ | **PyPI API Token** | Publishing to PyPI | 1. Go to [PyPI Account Settings](https://pypi.org/manage/account/token/)<br>2. Select "Add API Token"<br>3. Scope to "Entire account" (for first publish)<br>4. Set as `TWINE_PASSWORD` in `.env` |
55
+ | **Proxy URL** | Bypassing Blocks (Optional) | Use any HTTP/SOCKS5 proxy provider if you encounter rate limits. |
56
+
57
+ ## Development / Publishing
58
+
59
+ To build and publish this package to PyPI (using the secrets from above):
60
+
61
+ 1. **Build**:
62
+ ```bash
63
+ pip install build twine
64
+ python -m build
65
+ ```
66
+
67
+ 2. **Publish** (loads secrets from .env if you export them, or prompts you):
68
+ ```bash
69
+ # If using .env variables (PowerShell)
70
+ # $env:TWINE_USERNAME = "__token__"
71
+ # $env:TWINE_PASSWORD = "pypi-..."
72
+
73
+ python -m twine upload dist/*
74
+ ```
@@ -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.2.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,70 @@
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 metasearch using various backends (DuckDuckGo, Google, Bing, etc.). Use this to find APIs, libraries, developer tools, and general information.",
22
+ inputSchema={
23
+ "type": "object",
24
+ "properties": {
25
+ "query": {"type": "string", "description": "Search query"},
26
+ "backend": {
27
+ "type": "string",
28
+ "enum": ["auto", "html", "lite", "bing", "brave", "duckduckgo", "google", "grokipedia", "mojeek", "yandex", "yahoo", "wikipedia"],
29
+ "default": "auto",
30
+ "description": "Search engine backend to use."
31
+ },
32
+ "region": {"type": "string", "default": "us-en", "description": "e.g., us-en, uk-en"},
33
+ "safesearch": {"type": "string", "enum": ["on", "moderate", "off"], "default": "moderate"},
34
+ "timelimit": {"type": "string", "enum": ["d", "w", "m", "y"], "default": None},
35
+ "max_results": {"type": "integer", "default": 10}
36
+ },
37
+ "required": ["query"]
38
+ }
39
+ )
40
+ ]
41
+
42
+ @server.call_tool()
43
+ async def call_tool(name: str, arguments: dict) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
44
+ logger.info(f"Calling tool: {name} with args: {arguments}")
45
+
46
+ if name != "search_text":
47
+ raise ValueError(f"Unknown tool: {name}")
48
+
49
+ query = arguments.get("query")
50
+ backend = arguments.get("backend", "auto")
51
+ region = arguments.get("region", "us-en")
52
+ safesearch = arguments.get("safesearch", "moderate")
53
+ timelimit = arguments.get("timelimit")
54
+ max_results = arguments.get("max_results", 10)
55
+
56
+ try:
57
+ with DDGS() as ddgs:
58
+ results = ddgs.text(
59
+ keywords=query,
60
+ region=region,
61
+ safesearch=safesearch,
62
+ timelimit=timelimit,
63
+ max_results=max_results,
64
+ backend=backend
65
+ )
66
+ return [types.TextContent(type="text", text=json.dumps(results, indent=2))]
67
+
68
+ except Exception as e:
69
+ logger.error(f"Error executing {name}: {e}")
70
+ 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