Initial commit: Javis project

This commit is contained in:
Javis committed 2026-09-21 21:44:00 +02:00
commit 435c645f36
164 files changed
+35279

No files matched your search

+26
View File
@@ -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"
}
```
+23
View File
@@ -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"]
+15
View File
@@ -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()
+199
View File
@@ -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()