"""Streamable HTTP 传输层实现。 通过 HTTP POST /message 发送 JSON-RPC 2.0 请求的标准 MCP 传输协议。 """ from __future__ import annotations import time from typing import Any import httpx from app.core.config import settings from app.integrations.mcp.client import _auth_headers, _parse_call_result from app.integrations.mcp.transports.base import TransportProtocol from app.modules.tool_runtime.definition import ToolResult class StreamableHttpTransport(TransportProtocol): """通过 HTTP POST /message 通信的 MCP 传输层。""" def __init__( self, server_id: str, server_name: str, endpoint_url: str, auth_token: str | None = None, http_client: httpx.AsyncClient | None = None, ) -> None: self.server_id = server_id self.server_name = server_name self._endpoint_url = endpoint_url.rstrip("/") self._auth_token = auth_token self._http_client = http_client or httpx.AsyncClient(trust_env=False) self._own_client = http_client is None self._started_at: float | None = None async def list_tools(self) -> list[dict[str, Any]]: body = { "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}, } try: resp = await self._http_client.post( self._endpoint_url, json=body, headers=_auth_headers(self._auth_token), timeout=settings.mcp_list_timeout_seconds, ) resp.raise_for_status() data = resp.json() except (httpx.HTTPError, ValueError): return [] result = data.get("result") if isinstance(data, dict) else None if not result: return [] return result.get("tools") if isinstance(result.get("tools"), list) else [] async def call_tool(self, name: str, arguments: dict[str, Any]) -> ToolResult: body = { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": name, "arguments": arguments, }, } try: resp = await self._http_client.post( self._endpoint_url, json=body, headers=_auth_headers(self._auth_token), timeout=settings.mcp_call_timeout_seconds, ) resp.raise_for_status() data = resp.json() except (httpx.HTTPError, ValueError, TypeError) as exc: return ToolResult( ok=False, stdout="", stderr=str(exc), duration_ms=0, exit_code=None, timeout=False, truncated=False, trace_id="", ) return _parse_call_result(data) async def start(self) -> None: self._started_at = time.time() @property def is_running(self) -> bool: return True async def get_status(self) -> dict[str, Any]: uptime = None if self._started_at is not None: uptime = time.time() - self._started_at return { "server_id": self.server_id, "name": self.server_name, "transport": "streamable-http", "status": "running", "pid": None, "uptime_seconds": uptime, }