- 新增 pam_deploy_graph 包,包含 Agent runtime、ActionRouter、脚本/MCP/fake runner - 支持 hybrid_node_mcp 策略:PAM_HOME 走脚本 action,PAM_NODE 走 MCP - 支持 script_only 离线策略,全部 action 走现有脚本 action - 新增 LLM structured output 骨架和规则 fallback,支持意图识别、参数抽取、计划生成 - 新增 LangGraph StateGraph 工厂和 MCP client adapter - 新增 CLI:preview、analyze、run-global、run-deploy - 增加 fake 完整部署流程、单 IP 失败待回滚确认状态和报告输出 - 增加单元测试覆盖路由、parser、runner、Skill 加载、LLM 输出、MCP adapter 和 LangGraph 图 - 更新 README,记录当前代码骨架、进度、使用方式和下一步计划
66 lines
2.0 KiB
Python
66 lines
2.0 KiB
Python
"""MCP client adapters.
|
|
|
|
The Agent only needs a synchronous `call_tool(name, arguments)` surface. This
|
|
module adapts simple callables or SDK-like sessions to that surface without
|
|
forcing the rest of the codebase to import a concrete MCP SDK.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
|
|
class FunctionMcpToolClient:
|
|
"""Wrap a plain Python callable as an MCP tool client."""
|
|
|
|
def __init__(self, caller: Callable[[str, dict[str, Any]], Any]) -> None:
|
|
self.caller = caller
|
|
|
|
def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any:
|
|
return self.caller(tool_name, arguments)
|
|
|
|
|
|
class SessionMcpToolClient:
|
|
"""Adapt SDK-like sessions exposing `call_tool`.
|
|
|
|
The adapter accepts common result shapes:
|
|
|
|
- raw dict/list/string
|
|
- object with `structuredContent`
|
|
- object with `content`, where text content may contain JSON
|
|
"""
|
|
|
|
def __init__(self, session: Any) -> None:
|
|
if not hasattr(session, "call_tool"):
|
|
raise TypeError("MCP session must expose call_tool")
|
|
self.session = session
|
|
|
|
def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any:
|
|
result = self.session.call_tool(tool_name, arguments)
|
|
return normalize_mcp_sdk_result(result)
|
|
|
|
|
|
def normalize_mcp_sdk_result(result: Any) -> Any:
|
|
if hasattr(result, "structuredContent"):
|
|
structured = getattr(result, "structuredContent")
|
|
if structured is not None:
|
|
return structured
|
|
|
|
if hasattr(result, "content"):
|
|
content = getattr(result, "content")
|
|
text_parts: list[str] = []
|
|
for item in content or []:
|
|
text = getattr(item, "text", None)
|
|
if text is not None:
|
|
text_parts.append(text)
|
|
if text_parts:
|
|
joined = "\n".join(text_parts)
|
|
try:
|
|
return json.loads(joined)
|
|
except json.JSONDecodeError:
|
|
return joined
|
|
|
|
return result
|