"""MCP 客户端 — Streamable HTTP 与 stdio 均用的 JSON-RPC 解析工具。 两种传输层的具体实现在 ``transports/`` 子包中。 """ from __future__ import annotations from typing import Any import httpx from app.core.config import settings from app.modules.tool_runtime.definition import ToolResult def _auth_headers(auth_token: str | None) -> dict[str, str]: return {"Authorization": f"Bearer {auth_token}"} if auth_token else {} def _parse_call_result(data: dict) -> ToolResult: """解析 JSON-RPC tools/call 响应为 ToolResult。 供 StreamableHttpTransport 和 StdioTransport 共用。 """ if data.get("error"): return ToolResult( ok=False, stdout="", stderr=data["error"].get("message", "MCP 错误"), duration_ms=0, exit_code=1, timeout=False, truncated=False, trace_id="", ) result = data.get("result") if isinstance(data, dict) else None if not isinstance(result, dict): return ToolResult( ok=False, stdout=str(result) if result else "", stderr="无效响应格式", duration_ms=0, exit_code=1, timeout=False, truncated=False, trace_id="", ) content = result.get("content", []) stdout_parts = [] for item in content: if item.get("type") in ("text", "json"): stdout_parts.append(item.get("text", "")) truncated = bool((result.get("meta") or {}).get("truncated", False)) return ToolResult( ok=not result.get("isError", False), stdout="".join(stdout_parts), stderr="", duration_ms=0, exit_code=0 if not result.get("isError", False) else 1, timeout=False, truncated=truncated, trace_id="", ) async def call_tool( http_client: httpx.AsyncClient, endpoint_url: str, tool_name: str, arguments: dict, auth_token: str | None = None, meta: dict | None = None, ) -> ToolResult: """通过 Streamable HTTP 调用 MCP 工具(供 _make_mcp_executor 使用)。""" body: dict[str, Any] = { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": tool_name, "arguments": arguments, }, } if meta: body["meta"] = meta try: resp = await http_client.post( endpoint_url, json=body, headers=_auth_headers(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)