Initial commit: Javis project
This commit is contained in:
commit
435c645f36
164 files changed
+35279
No files matched your search
@@ -0,0 +1,26 @@
|
||||
# duckduckgo-mcp
|
||||
|
||||
MCP server that exposes DuckDuckGo search to Javis via three tools:
|
||||
|
||||
- **`ddg_search`** — general web search (titles, URLs, snippets)
|
||||
- **`ddg_news`** — recent news articles
|
||||
- **`ddg_answers`** — instant answers (definitions, calculations, Wikipedia summaries)
|
||||
|
||||
## Setup
|
||||
|
||||
```bat
|
||||
run.bat
|
||||
```
|
||||
|
||||
The batch script creates a `.venv`, installs dependencies, and launches the server over stdio.
|
||||
|
||||
## Registration in `mcp_servers.json`
|
||||
|
||||
```json
|
||||
"duckduckgo": {
|
||||
"command": "d:\\Javis\\duckduckgo-mcp\\.venv\\Scripts\\duckduckgo-mcp.exe",
|
||||
"args": [],
|
||||
"transport": "stdio",
|
||||
"approval": "allow"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "duckduckgo-mcp"
|
||||
version = "0.1.0"
|
||||
description = "MCP server exposing DuckDuckGo web, news, and instant-answer search."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
license = { text = "MIT" }
|
||||
authors = [{ name = "duckduckgo-mcp" }]
|
||||
keywords = ["mcp", "duckduckgo", "search", "llm"]
|
||||
dependencies = [
|
||||
"mcp[cli]>=1.2",
|
||||
"ddgs>=1.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
duckduckgo-mcp = "duckduckgo_mcp.server:main"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/duckduckgo_mcp"]
|
||||
@@ -0,0 +1,15 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
if not exist ".venv\Scripts\python.exe" (
|
||||
echo Creating virtual environment...
|
||||
python -m venv .venv
|
||||
call .venv\Scripts\activate.bat
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e .
|
||||
) else (
|
||||
call .venv\Scripts\activate.bat
|
||||
)
|
||||
|
||||
python -m duckduckgo_mcp %*
|
||||
@@ -0,0 +1 @@
|
||||
"""duckduckgo_mcp package."""
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Allow ``python -m duckduckgo_mcp`` to start the server."""
|
||||
|
||||
from .server import main
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,199 @@
|
||||
"""DuckDuckGo MCP server — web, news, and instant-answer search."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
log = logging.getLogger("duckduckgo_mcp")
|
||||
|
||||
mcp = FastMCP(
|
||||
name="duckduckgo",
|
||||
instructions=(
|
||||
"Use these tools to search the web via DuckDuckGo. "
|
||||
"Prefer `ddg_search` for general research, `ddg_news` for recent events, "
|
||||
"and `ddg_answers` for quick factual lookups (calculations, definitions, etc.)."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ── helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def _get_ddgs():
|
||||
"""Import DDGS lazily so import errors surface at call time.
|
||||
|
||||
Tries the current package name ``ddgs`` first, then falls back to the
|
||||
legacy ``duckduckgo_search`` name for environments that haven't migrated.
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
from ddgs import DDGS # new package name (v1+)
|
||||
except ImportError:
|
||||
from duckduckgo_search import DDGS # legacy name # type: ignore[import-untyped]
|
||||
return DDGS()
|
||||
except ImportError as exc:
|
||||
raise RuntimeError(
|
||||
"Neither 'ddgs' nor 'duckduckgo-search' is installed. "
|
||||
"Run: pip install ddgs"
|
||||
) from exc
|
||||
|
||||
|
||||
def _fmt_web(results: list[dict[str, Any]]) -> str:
|
||||
if not results:
|
||||
return "No results found."
|
||||
lines: list[str] = []
|
||||
for i, r in enumerate(results, 1):
|
||||
title = r.get("title", "").strip()
|
||||
url = r.get("href", "")
|
||||
body = (r.get("body") or "").strip()
|
||||
lines.append(f"{i}. **{title}**\n {url}\n {body}")
|
||||
return "\n\n".join(lines)
|
||||
|
||||
|
||||
def _fmt_news(results: list[dict[str, Any]]) -> str:
|
||||
if not results:
|
||||
return "No news results found."
|
||||
lines: list[str] = []
|
||||
for i, r in enumerate(results, 1):
|
||||
title = r.get("title", "").strip()
|
||||
url = r.get("url", r.get("href", ""))
|
||||
date = r.get("date", "")
|
||||
source = r.get("source", "")
|
||||
body = (r.get("body") or "").strip()
|
||||
meta = " · ".join(filter(None, [source, date]))
|
||||
lines.append(f"{i}. **{title}**{(' — ' + meta) if meta else ''}\n {url}\n {body}")
|
||||
return "\n\n".join(lines)
|
||||
|
||||
|
||||
def _fmt_answers(results: list[dict[str, Any]]) -> str:
|
||||
if not results:
|
||||
return "No instant answers found."
|
||||
lines: list[str] = []
|
||||
for r in results:
|
||||
text = (r.get("text") or r.get("answer") or "").strip()
|
||||
url = r.get("url") or r.get("href") or ""
|
||||
if text:
|
||||
lines.append(text + (f"\n Source: {url}" if url else ""))
|
||||
return "\n\n".join(lines) if lines else "No instant answers found."
|
||||
|
||||
|
||||
# ── tools ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Search the web using DuckDuckGo. "
|
||||
"Returns titles, URLs, and short descriptions for the top results. "
|
||||
"Use this for general research, how-to questions, finding documentation, "
|
||||
"or any open-ended information lookup."
|
||||
)
|
||||
)
|
||||
def ddg_search(
|
||||
query: str,
|
||||
max_results: int = 10,
|
||||
region: str = "wt-wt",
|
||||
safesearch: str = "moderate",
|
||||
) -> str:
|
||||
"""Web search via DuckDuckGo.
|
||||
|
||||
Args:
|
||||
query: The search query.
|
||||
max_results: Number of results to return (1–30).
|
||||
region: DuckDuckGo region code, e.g. 'wt-wt' (global), 'de-de', 'us-en'.
|
||||
safesearch: 'on', 'moderate', or 'off'.
|
||||
"""
|
||||
max_results = max(1, min(max_results, 30))
|
||||
ddgs = _get_ddgs()
|
||||
try:
|
||||
results = ddgs.text(
|
||||
query,
|
||||
max_results=max_results,
|
||||
region=region,
|
||||
safesearch=safesearch,
|
||||
)
|
||||
return _fmt_web(list(results))
|
||||
except Exception as exc:
|
||||
log.error(f"ddg_search error: {exc}")
|
||||
return f"Search failed: {exc}"
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Search DuckDuckGo News for recent articles and breaking stories. "
|
||||
"Use this for current events, news about people or companies, "
|
||||
"or any topic where freshness matters."
|
||||
)
|
||||
)
|
||||
def ddg_news(
|
||||
query: str,
|
||||
max_results: int = 10,
|
||||
region: str = "wt-wt",
|
||||
) -> str:
|
||||
"""News search via DuckDuckGo.
|
||||
|
||||
Args:
|
||||
query: The news search query.
|
||||
max_results: Number of articles to return (1–30).
|
||||
region: DuckDuckGo region code, e.g. 'wt-wt' (global), 'de-de', 'us-en'.
|
||||
"""
|
||||
max_results = max(1, min(max_results, 30))
|
||||
ddgs = _get_ddgs()
|
||||
try:
|
||||
results = ddgs.news(
|
||||
query,
|
||||
max_results=max_results,
|
||||
region=region,
|
||||
)
|
||||
return _fmt_news(list(results))
|
||||
except Exception as exc:
|
||||
log.error(f"ddg_news error: {exc}")
|
||||
return f"News search failed: {exc}"
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Look up a quick, focused answer for short factual questions — "
|
||||
"definitions, calculations, unit conversions, 'who is X', 'what is Y', etc. "
|
||||
"Returns the top 3 most relevant web results. "
|
||||
"Prefer ddg_search for open-ended research."
|
||||
)
|
||||
)
|
||||
def ddg_answers(query: str) -> str:
|
||||
"""Quick factual lookup via DuckDuckGo.
|
||||
|
||||
The DDG instant-answers API was removed upstream (duckduckgo-search >= 7 /
|
||||
ddgs >= 1). This tool now returns the top 3 focused web results as the
|
||||
best available substitute.
|
||||
|
||||
Args:
|
||||
query: A short, direct question or lookup term.
|
||||
"""
|
||||
ddgs = _get_ddgs()
|
||||
try:
|
||||
# Use native answers() if the installed library still provides it
|
||||
# (kept for forward-compatibility if DDG ever restores the endpoint).
|
||||
if hasattr(ddgs, "answers"):
|
||||
try:
|
||||
results = list(ddgs.answers(query))
|
||||
if results:
|
||||
return _fmt_answers(results)
|
||||
except Exception as exc:
|
||||
log.debug(f"ddg native answers() failed ({exc}), falling back to text")
|
||||
# Fallback: top-3 web results as a focused answer proxy
|
||||
results = list(ddgs.text(query, max_results=3))
|
||||
return _fmt_web(results) if results else "No results found."
|
||||
except Exception as exc:
|
||||
log.error(f"ddg_answers error: {exc}")
|
||||
return f"Answer lookup failed: {exc}"
|
||||
|
||||
|
||||
# ── entry point ───────────────────────────────────────────────────────────────
|
||||
|
||||
def main() -> None:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
mcp.run(transport="stdio")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user