# -*- coding: utf-8 -*- """MCP JSON-RPC handler (Streamable HTTP compatible).""" import json import logging from typing import Any, Optional from . import tools LOGGER = logging.getLogger("loogle_mcp.server") # Versioni supportate, dalla più recente: si risponde con quella chiesta dal client # se supportata, altrimenti con la più recente (negoziazione MCP lifecycle) SUPPORTED_PROTOCOL_VERSIONS = ("2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05") def negotiate_protocol_version(requested: Optional[str]) -> str: if requested in SUPPORTED_PROTOCOL_VERSIONS: return requested # Client più nuovi: accettiamo la richiesta dichiarando la più recente che implementiamo if requested: LOGGER.info("protocollo sconosciuto %s — negozio %s", requested, SUPPORTED_PROTOCOL_VERSIONS[0]) return SUPPORTED_PROTOCOL_VERSIONS[0] def expects_response(msg: Any) -> bool: """Solo le request JSON-RPC (method + id) hanno risposta; notifiche e response no.""" return isinstance(msg, dict) and "method" in msg and "id" in msg def _error(req_id: Any, code: int, message: str) -> dict: return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}} def _result(req_id: Any, result: dict) -> dict: return {"jsonrpc": "2.0", "id": req_id, "result": result} def handle_message(body: dict, claims: Optional[dict]) -> dict: method = body.get("method") req_id = body.get("id") params = body.get("params") or {} if method == "initialize": requested = params.get("protocolVersion") version = negotiate_protocol_version(requested) LOGGER.info( "initialize: client=%s protocollo richiesto=%s negoziato=%s", (params.get("clientInfo") or {}).get("name"), requested, version, ) return _result( req_id, { "protocolVersion": version, "capabilities": {"tools": {}, "resources": {}}, "serverInfo": {"name": "loogle-mcp", "version": "1.0.0"}, }, ) if method == "ping": return _result(req_id, {}) if not claims: return _error(req_id, -32001, "Autenticazione richiesta (Bearer token OAuth)") if method == "tools/list": return _result(req_id, {"tools": tools.tool_definitions()}) if method == "tools/call": name = params.get("name") arguments = params.get("arguments") or {} try: tool_result = tools.call_tool(name, arguments, claims) return _result(req_id, tool_result) except PermissionError as exc: return _error(req_id, -32003, str(exc)) except FileNotFoundError as exc: return _error(req_id, -32004, str(exc)) except Exception as exc: LOGGER.exception("Tool %s failed", name) return _error(req_id, -32000, str(exc)) if method == "resources/list": return _result(req_id, {"resources": tools.list_resources(claims)}) if method == "resources/read": uri = params.get("uri") try: resource = tools.read_resource(uri, claims) return _result(req_id, {"contents": [resource]}) except FileNotFoundError as exc: return _error(req_id, -32004, str(exc)) return _error(req_id, -32601, f"Metodo non supportato: {method}") def handle_batch(messages: list, claims: Optional[dict]) -> list: return [handle_message(msg, claims) for msg in messages if expects_response(msg)]