- Aggiunto campo 'title' a tools/list in tutti gli script MCP locali (Gmail, Gcal, Drive, Email, SSH, Weather, Wikipedia, WhatsApp) - Aggiunto tools[]/display_name nel manifest per connector remoti/package esterni (Firecrawl, HTTP Fetch, Exa, Tavily, SerpAPI Flights) - Documentata convenzione e resolution order in SKALD.md - Aggiornata manifest_guide.md per tools[] in connectors.json - Bumped version e sha256 per tutti i 13 connector
355 lines
12 KiB
Python
355 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""Wikipedia MCP server (JSON-RPC 2.0 over stdio) using the free Wikipedia API.
|
|
|
|
Capabilities (callable as `mcp__wikipedia__<tool>`):
|
|
status — self-check: confirms Wikipedia API is reachable
|
|
search — search Wikipedia articles by keyword
|
|
readArticle — get the full text content of a Wikipedia article
|
|
|
|
Data source (free, no API key required):
|
|
- Wikipedia API: https://en.wikipedia.org/w/api.php
|
|
|
|
Run with:
|
|
python3 wikipedia_mcp_server.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
# ── Logging ─────────────────────────────────────────────────────────────────────
|
|
|
|
def log(msg: str) -> None:
|
|
print(f"[wikipedia_mcp] {msg}", file=sys.stderr, flush=True)
|
|
|
|
|
|
# ── Constants ───────────────────────────────────────────────────────────────────
|
|
|
|
WIKI_API = "https://en.wikipedia.org/w/api.php"
|
|
USER_AGENT = "SkaldWikipediaMCP/1.0"
|
|
HTTP_TIMEOUT = 15.0
|
|
|
|
|
|
# ── Tool implementations ────────────────────────────────────────────────────────
|
|
|
|
def _status(args: dict[str, Any]) -> str:
|
|
"""Self-check: confirm Wikipedia API is reachable."""
|
|
try:
|
|
params = {
|
|
"action": "query",
|
|
"meta": "siteinfo",
|
|
"format": "json",
|
|
}
|
|
with httpx.Client(timeout=HTTP_TIMEOUT) as client:
|
|
resp = client.get(WIKI_API, params=params,
|
|
headers={"User-Agent": USER_AGENT})
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
if "query" in data:
|
|
return ("OK: Wikipedia API is reachable. "
|
|
"All tools (search, readArticle) are operational.")
|
|
return "Error: Wikipedia API responded but returned unexpected data."
|
|
except httpx.TimeoutException:
|
|
return "Error: Wikipedia API request timed out."
|
|
except httpx.HTTPStatusError as e:
|
|
return f"Error: Wikipedia API returned HTTP {e.response.status_code}."
|
|
except httpx.HTTPError as e:
|
|
return f"Error: Wikipedia API request failed: {e}"
|
|
|
|
|
|
def _search(args: dict[str, Any]) -> str:
|
|
"""Search Wikipedia articles by keyword, returning titles and summaries."""
|
|
query = args.get("query", "").strip()
|
|
if not query:
|
|
return "Error: Missing required parameter 'query'."
|
|
|
|
limit = args.get("limit", 8)
|
|
try:
|
|
limit = int(limit)
|
|
except (TypeError, ValueError):
|
|
limit = 8
|
|
if limit < 1:
|
|
limit = 1
|
|
if limit > 50:
|
|
limit = 50
|
|
|
|
try:
|
|
params = {
|
|
"action": "opensearch",
|
|
"search": query,
|
|
"limit": limit,
|
|
"namespace": 0,
|
|
"format": "json",
|
|
}
|
|
with httpx.Client(timeout=HTTP_TIMEOUT) as client:
|
|
resp = client.get(WIKI_API, params=params,
|
|
headers={"User-Agent": USER_AGENT})
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
except httpx.TimeoutException:
|
|
return "Error: Wikipedia search request timed out."
|
|
except httpx.HTTPStatusError as e:
|
|
return f"Error: Wikipedia API returned HTTP {e.response.status_code}."
|
|
except httpx.HTTPError as e:
|
|
return f"Error: Wikipedia API request failed: {e}"
|
|
|
|
# opensearch returns [query, [titles], [descriptions], [urls]]
|
|
titles = data[1] if len(data) > 1 else []
|
|
descriptions = data[2] if len(data) > 2 else []
|
|
urls = data[3] if len(data) > 3 else []
|
|
|
|
if not titles:
|
|
return f"No results found for '{query}'."
|
|
|
|
lines = [f"📚 Wikipedia search results for '{query}'"]
|
|
lines.append("")
|
|
for i, title in enumerate(titles):
|
|
desc = descriptions[i] if i < len(descriptions) else ""
|
|
url = urls[i] if i < len(urls) else ""
|
|
lines.append(f" {i+1}. **{title}**")
|
|
if desc:
|
|
lines.append(f" {desc}")
|
|
if url:
|
|
lines.append(f" {url}")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _read_article(args: dict[str, Any]) -> str:
|
|
"""Get the full text content of a Wikipedia article by title."""
|
|
title = args.get("title", "").strip()
|
|
if not title:
|
|
return "Error: Missing required parameter 'title'."
|
|
|
|
try:
|
|
# First, get the page ID / exact title
|
|
params = {
|
|
"action": "query",
|
|
"titles": title,
|
|
"redirects": 1,
|
|
"format": "json",
|
|
}
|
|
with httpx.Client(timeout=HTTP_TIMEOUT) as client:
|
|
resp = client.get(WIKI_API, params=params,
|
|
headers={"User-Agent": USER_AGENT})
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
|
|
pages = data.get("query", {}).get("pages", {})
|
|
if not pages:
|
|
return f"Error: Article '{title}' not found."
|
|
|
|
page_id = next(iter(pages))
|
|
page_info = pages[page_id]
|
|
|
|
if "missing" in page_info:
|
|
return f"Error: Article '{title}' not found on Wikipedia."
|
|
|
|
actual_title = page_info.get("title", title)
|
|
|
|
# Now get the extract (full text)
|
|
params = {
|
|
"action": "query",
|
|
"pageids": page_id,
|
|
"prop": "extracts",
|
|
"explaintext": 1,
|
|
"exlimit": 1,
|
|
"format": "json",
|
|
}
|
|
with httpx.Client(timeout=HTTP_TIMEOUT) as client:
|
|
resp = client.get(WIKI_API, params=params,
|
|
headers={"User-Agent": USER_AGENT})
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
|
|
pages = data.get("query", {}).get("pages", {})
|
|
extract = pages.get(page_id, {}).get("extract", "")
|
|
|
|
if not extract:
|
|
return f"**{actual_title}**\n\n_(No content available.)_"
|
|
|
|
# Get the URL
|
|
article_url = f"https://en.wikipedia.org/wiki/{actual_title.replace(' ', '_')}"
|
|
|
|
# Truncate very long articles to avoid blowing up context
|
|
max_chars = 15000
|
|
if len(extract) > max_chars:
|
|
extract = extract[:max_chars] + "\n\n_...(article truncated, see full version on Wikipedia)_"
|
|
|
|
return (
|
|
f"# {actual_title}\n\n"
|
|
f"_{article_url}_\n\n"
|
|
f"{extract}"
|
|
)
|
|
|
|
except httpx.TimeoutException:
|
|
return "Error: Wikipedia API request timed out."
|
|
except httpx.HTTPStatusError as e:
|
|
return f"Error: Wikipedia API returned HTTP {e.response.status_code}."
|
|
except httpx.HTTPError as e:
|
|
return f"Error: Wikipedia API request failed: {e}"
|
|
|
|
|
|
# ── Tool manifest ────────────────────────────────────────────────────────────────
|
|
|
|
TOOLS = [
|
|
{
|
|
"name": "status",
|
|
"title": "Status",
|
|
"description": (
|
|
"Self-check that the Wikipedia integration is operational: confirms "
|
|
"the Wikipedia API is reachable by issuing a cheap siteinfo query. "
|
|
"Call this first if any Wikipedia tool fails."
|
|
),
|
|
"inputSchema": {"type": "object", "properties": {}},
|
|
},
|
|
{
|
|
"name": "search",
|
|
"title": "Search Wikipedia",
|
|
"description": (
|
|
"Search Wikipedia articles by keyword. Returns a list of matching "
|
|
"article titles with short descriptions and URLs. Use this to find "
|
|
"the correct article title before calling readArticle."
|
|
),
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {
|
|
"type": "string",
|
|
"description": "Search query (keywords or phrase).",
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "Maximum number of results to return (1-50). Default: 8.",
|
|
},
|
|
},
|
|
"required": ["query"],
|
|
},
|
|
},
|
|
{
|
|
"name": "readArticle",
|
|
"description": (
|
|
"Get the full text content of a Wikipedia article by title. "
|
|
"Returns the article body as Markdown-like text, with a link to the "
|
|
"live Wikipedia page. Long articles are truncated at 15,000 characters. "
|
|
"First use search() to find the exact article title."
|
|
),
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Exact Wikipedia article title (e.g. 'Alan Turing', 'Rome').",
|
|
},
|
|
},
|
|
"required": ["title"],
|
|
},
|
|
},
|
|
]
|
|
|
|
TOOL_DISPATCH = {
|
|
"status": _status,
|
|
"search": _search,
|
|
"readArticle": _read_article,
|
|
}
|
|
|
|
|
|
# ── JSON-RPC dispatch ────────────────────────────────────────────────────────────
|
|
|
|
def _ok(req_id: Any, result: Any) -> str:
|
|
return json.dumps({"jsonrpc": "2.0", "id": req_id, "result": result})
|
|
|
|
|
|
def _text_result(req_id: Any, text: str, is_error: bool = False) -> str:
|
|
payload: dict = {
|
|
"jsonrpc": "2.0",
|
|
"id": req_id,
|
|
"result": {"content": [{"type": "text", "text": text}]},
|
|
}
|
|
if is_error:
|
|
payload["result"]["isError"] = True
|
|
return json.dumps(payload)
|
|
|
|
|
|
def _error(req_id: Any, code: int, message: str) -> str:
|
|
return json.dumps({
|
|
"jsonrpc": "2.0",
|
|
"id": req_id,
|
|
"error": {"code": code, "message": message},
|
|
})
|
|
|
|
|
|
def handle_request(msg: dict) -> str | None:
|
|
method = msg.get("method", "")
|
|
req_id = msg.get("id")
|
|
|
|
if method == "initialize":
|
|
return _ok(req_id, {
|
|
"protocolVersion": "2024-11-05",
|
|
"capabilities": {"tools": {}},
|
|
"serverInfo": {
|
|
"name": "wikipedia",
|
|
"version": "1.0.0",
|
|
},
|
|
})
|
|
|
|
if method == "notifications/initialized":
|
|
return None
|
|
|
|
if method == "ping":
|
|
return _ok(req_id, {})
|
|
|
|
if method == "tools/list":
|
|
return _ok(req_id, {"tools": TOOLS})
|
|
|
|
if method == "tools/call":
|
|
params = msg.get("params", {})
|
|
tool_name = params.get("name", "")
|
|
tool_args = params.get("arguments", {})
|
|
|
|
handler = TOOL_DISPATCH.get(tool_name)
|
|
if handler is None:
|
|
return _text_result(req_id, f"Error: Unknown tool: {tool_name}", is_error=True)
|
|
|
|
try:
|
|
result = handler(tool_args)
|
|
is_err = result.startswith("Error:")
|
|
return _text_result(req_id, result, is_error=is_err)
|
|
except Exception as e:
|
|
log(f"Unhandled exception in tool '{tool_name}': {e}")
|
|
return _text_result(req_id, f"Error: Internal error in tool '{tool_name}': {e}", is_error=True)
|
|
|
|
return _error(req_id, -32601, f"Method not found: {method}")
|
|
|
|
|
|
# ── Main loop ────────────────────────────────────────────────────────────────────
|
|
|
|
def main() -> None:
|
|
log("Starting Wikipedia MCP server")
|
|
try:
|
|
for line in sys.stdin:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
msg = json.loads(line)
|
|
except json.JSONDecodeError as e:
|
|
log(f"Invalid JSON input: {e}")
|
|
continue
|
|
|
|
resp = handle_request(msg)
|
|
if resp is not None:
|
|
sys.stdout.write(resp + "\n")
|
|
sys.stdout.flush()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|