rmi-backend/app/api/v1/mcp/router.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

164 lines
5.3 KiB
Python

"""T33 MCP Server - HTTP wrapper for SSE transport.
Per v4.0 §T33. Endpoints:
POST /mcp JSON-RPC 2.0 endpoint
GET /mcp/tools Tool catalog
POST /mcp/call/{tool_id} Direct tool execution (no JSON-RPC)
The server speaks the Model Context Protocol natively. Claude Desktop
and Cursor connect via:
{"mcpServers": {"rugmunch": {"url": "https://mcp.rugmunch.io/mcp", "transport": "sse"}}}
"""
from __future__ import annotations
import json
import logging
from typing import Any
from fastapi import APIRouter, Request
from pydantic import BaseModel
from app.mcp.server import (
MCP_PROTOCOL_VERSION,
MCP_SERVER_VERSION,
TOOL_CATALOG,
TOOL_DEPRECATED,
TOOL_SUCCESSORS,
TOOL_VERSIONS,
call_tool,
)
router = APIRouter(prefix="/mcp", tags=["mcp"])
log = logging.getLogger(__name__)
class JsonRpcRequest(BaseModel):
jsonrpc: str = "2.0"
method: str
params: dict[str, Any] = {}
id: int | str | None = None
class JsonRpcResponse(BaseModel):
jsonrpc: str = "2.0"
result: Any | None = None
error: dict | None = None
id: int | str | None = None
@router.post("")
async def jsonrpc_handler(req: JsonRpcRequest) -> dict:
"""JSON-RPC 2.0 endpoint for MCP clients.
Methods:
- initialize → returns server info
- tools/list → returns tool catalog
- tools/call → dispatches to backend
- resources/list → empty
- prompts/list → empty
"""
if req.jsonrpc != "2.0":
return {"jsonrpc": "2.0", "error": {"code": -32600, "message": "invalid jsonrpc version"}, "id": req.id}
if req.method == "initialize":
return {
"jsonrpc": "2.0",
"result": {
"protocolVersion": "2024-11-05",
"serverInfo": {
"name": "rugmunch-intelligence",
"version": MCP_SERVER_VERSION,
"description": "Crypto intelligence platform - 13+ chains, 8 MCP tools, x402 paid tier",
},
"capabilities": {"tools": {}, "resources": {}, "prompts": {}},
},
"id": req.id,
}
if req.method == "tools/list":
return {
"jsonrpc": "2.0",
"result": {"tools": TOOL_CATALOG},
"id": req.id,
}
if req.method == "tools/call":
name = req.params.get("name", "")
arguments = req.params.get("arguments", {})
if not name:
return {"jsonrpc": "2.0", "error": {"code": -32602, "message": "tool name required"}, "id": req.id}
result = await call_tool(name, arguments)
return {
"jsonrpc": "2.0",
"result": {
"content": [{"type": "text", "text": json.dumps(result, default=str)[:50000]}],
"isError": "error" in result,
},
"id": req.id,
}
if req.method == "resources/list":
return {"jsonrpc": "2.0", "result": {"resources": []}, "id": req.id}
if req.method == "prompts/list":
return {"jsonrpc": "2.0", "result": {"prompts": []}, "id": req.id}
if req.method == "notifications/initialized":
return {"jsonrpc": "2.0", "result": {}, "id": req.id}
return {
"jsonrpc": "2.0",
"error": {"code": -32601, "message": f"method not found: {req.method}"},
"id": req.id,
}
@router.get("/tools")
async def list_tools() -> dict:
"""Plain JSON endpoint (for direct integration, no JSON-RPC)."""
return {"server": "rugmunch-intelligence", "version": MCP_SERVER_VERSION, "tools": TOOL_CATALOG}
@router.get("/info")
async def server_info() -> dict:
"""Server metadata: version, protocol, tool count, capabilities.
Use this to discover the MCP server's capabilities without listing all tools.
Equivalent to MCP initialize handshake.
"""
return {
"server": "rugmunch-intelligence",
"server_version": MCP_SERVER_VERSION,
"protocol_version": MCP_PROTOCOL_VERSION,
"tool_count": len(TOOL_CATALOG),
"tools_versioned": sum(1 for t in TOOL_CATALOG if t["name"] in TOOL_VERSIONS),
"tools_deprecated": list(TOOL_DEPRECATED),
"tools_with_successors": list(TOOL_SUCCESSORS.keys()),
"capabilities": ["tools", "resources", "prompts"],
"endpoints": {
"jsonrpc": "/mcp",
"tools_list": "/mcp/tools",
"server_info": "/mcp/info",
"direct_call": "/mcp/call/{tool_id}",
},
"auth": {
"free_tier_daily": 5,
"pro_tier": "x402 micropayment per call",
"x402_endpoint": "https://x402.rugmunch.io",
},
"links": {
"homepage": "https://rugmunch.io",
"mcp_endpoint": "https://mcp.rugmunch.io/mcp",
"status": "https://status.rugmunch.io",
"docs": "https://github.com/Rug-Munch-Media-LLC/rmi-docs",
},
}
@router.post("/call/{tool_id}")
async def direct_call(tool_id: str, request: Request) -> dict:
"""Direct tool execution (no JSON-RPC). For curl/scripts."""
body = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {}
arguments = body.get("arguments", body) if isinstance(body, dict) else {}
result = await call_tool(tool_id, arguments)
return result