"""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