- 10 MCP tools via thin proxy on MacBook - Backend REST API + Dashboard on CT 145 Docker - Services: YaCy crawler, OpenSearch index, Qdrant vectors, Ollama LLM - All 4 services healthy and verified
111 lines
3.6 KiB
Python
111 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
AI Research Engine — Thin MCP Proxy
|
|
Runs on MacBook. Forwards all tool calls to the CT 145 backend.
|
|
Minimal resource usage — all heavy lifting on Proxmox.
|
|
"""
|
|
|
|
import json
|
|
import httpx
|
|
from mcp.server import FastMCP
|
|
|
|
BACKEND_URL = "http://10.30.20.249:8000"
|
|
client = httpx.Client(timeout=120.0)
|
|
|
|
mcp = FastMCP(
|
|
"ai-research-engine",
|
|
instructions="""
|
|
AI Research Engine — private knowledge acquisition system.
|
|
|
|
search_web(query) — Full-text search across indexed documents
|
|
semantic_search(query) — Find documents by meaning (vector search)
|
|
crawl_url(url) — Crawl a URL into the index
|
|
crawl_topic(topic) — Discover and crawl sources for a topic
|
|
research_topic(topic) — Full pipeline: discover → crawl → summarize
|
|
retrieve_document(url) — Get full content of an indexed document
|
|
summarize_sources(urls, instruction) — AI summary of multiple sources
|
|
extract_information(url, schema) — Structured data extraction
|
|
create_report(topic, sources) — Generate comprehensive research report
|
|
index_status() — System health and stats
|
|
""",
|
|
)
|
|
|
|
def _get(path: str) -> dict:
|
|
r = client.get(f"{BACKEND_URL}{path}")
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
@mcp.tool()
|
|
def search_web(query: str, category: str = "", limit: int = 10) -> str:
|
|
"""Full-text search across indexed documents. Find by keywords, titles, content."""
|
|
r = _get(f"/api/search?q={query}&category={category}&limit={limit}")
|
|
return json.dumps(r, indent=2)
|
|
|
|
|
|
@mcp.tool()
|
|
def semantic_search(query: str, limit: int = 10) -> str:
|
|
"""Search by meaning using vector embeddings. Finds conceptually related docs."""
|
|
r = _get(f"/api/semantic-search?q={query}&limit={limit}")
|
|
return json.dumps(r, indent=2)
|
|
|
|
|
|
@mcp.tool()
|
|
def crawl_url(url: str, depth: int = 1) -> str:
|
|
"""Crawl a URL. depth: 0=just this page, 1=+linked pages."""
|
|
r = _get(f"/api/crawl?url={url}&depth={depth}")
|
|
return json.dumps(r, indent=2)
|
|
|
|
|
|
@mcp.tool()
|
|
def crawl_topic(topic: str, max_urls: int = 20) -> str:
|
|
"""Discover and crawl sources for a topic using YaCy."""
|
|
r = _get(f"/api/crawl-topic?topic={topic}&max_urls={max_urls}")
|
|
return json.dumps(r, indent=2)
|
|
|
|
|
|
@mcp.tool()
|
|
def research_topic(topic: str) -> str:
|
|
"""Full research pipeline: keyword search → semantic search → crawl new sources → AI summary."""
|
|
r = _get(f"/api/research?topic={topic}")
|
|
return json.dumps(r, indent=2)
|
|
|
|
|
|
@mcp.tool()
|
|
def retrieve_document(url: str) -> str:
|
|
"""Get full indexed content of a document by URL."""
|
|
r = _get(f"/api/document?url={url}")
|
|
return json.dumps(r, indent=2)
|
|
|
|
|
|
@mcp.tool()
|
|
def summarize_sources(urls: str, instruction: str = "Summarize key points") -> str:
|
|
"""Summarize multiple URLs using local LLM. urls: comma-separated."""
|
|
r = _get(f"/api/summarize?urls={urls}&instruction={instruction}")
|
|
return json.dumps(r, indent=2)
|
|
|
|
|
|
@mcp.tool()
|
|
def extract_information(url: str, schema: str = "company names, products, prices, specifications") -> str:
|
|
"""Extract structured information from a document using LLM."""
|
|
r = _get(f"/api/extract?url={url}&schema={schema}")
|
|
return json.dumps(r, indent=2)
|
|
|
|
|
|
@mcp.tool()
|
|
def create_report(topic: str, sources: str = "") -> str:
|
|
"""Generate a comprehensive research report. sources: optional comma-separated URLs."""
|
|
r = _get(f"/api/report?topic={topic}&sources={sources}")
|
|
return json.dumps(r, indent=2)
|
|
|
|
|
|
@mcp.tool()
|
|
def index_status() -> str:
|
|
"""Check health of all backend services: OpenSearch, Qdrant, YaCy, Ollama."""
|
|
r = _get("/api/status")
|
|
return json.dumps(r, indent=2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
mcp.run(transport="stdio")
|