- Make app/domains/auth/ and app/core/redis.py mypy-clean under strict. - Add mypy-gate.ini and Makefile mypy-gate target; promote typecheck-gate in CI. - Consolidate domains into app/domains/: bulletin, admin, intelligence, markets. - Extract referral domain incl. DeFi partner DEX ref links; keep Telegram bot wired. - Move app/mcp/ package and app/api/v1/mcp/router into app/domains/mcp/. - Archive dead app/mcp_router.py.
164 lines
5.3 KiB
Python
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.domains.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
|