"""MCP 工具转 ToolSpec(executor 绑定 http_client 调 MCP server tools/call)。 MCP 工具命名 ``mcp_{server_name}_{tool_name}``(下划线分隔,冒号不含 OpenAI 命名规范),``risk_level`` 缺省 L2(外部工具), ``permission_policy`` 由 risk 经 ``default_policy`` 推导。executor 闭包捕获 http_client + server endpoint + auth_token。 协议:标准 MCP Streamable HTTP(JSON-RPC 2.0 over POST /message) """ from app.integrations.mcp.client import call_tool from app.modules.safety.governor import default_policy from app.modules.tool_runtime.definition import ToolSpec def make_mcp_tool_specs( server, tools: list[dict], http_client, meta: dict | None = None, transport=None, ) -> list[ToolSpec]: """把 MCP server 工具清单(list[dict])转为 ToolSpec 列表。 Args: meta: 可选业务上下文 transport: 可选传输层实例(stdio 用,传给 executor 替代 HTTP call_tool) """ specs: list[ToolSpec] = [] for tool in tools: name = tool.get("name") if not name: continue risk_level = tool.get("risk_level", "L2") input_schema = tool.get("inputSchema") or tool.get("input_schema") specs.append( ToolSpec( name=f"mcp_{server.name}_{name}", description=tool.get("description", ""), risk_level=risk_level, permission_policy=default_policy(risk_level), executor=_make_mcp_executor( http_client, server.endpoint_url, name, server.auth_token, meta=meta, transport=transport, ), input_schema=input_schema, ) ) return specs def _make_mcp_executor( http_client, endpoint_url, tool_name, auth_token, meta: dict | None = None, transport=None, ): async def executor(input, context): # noqa: ARG001 call_meta = dict(meta or {}) if context: user_role = context.get("user_role") or "free" call_meta.setdefault("user_role", user_role) call_meta.setdefault("caller", "hunjing") if context.get("trace_id"): call_meta["trace_id"] = context["trace_id"] if transport is not None: return await transport.call_tool(tool_name, input) return await call_tool( http_client, endpoint_url, tool_name, input, auth_token, meta=call_meta or None, ) return executor