dark 14e297a488 feat: 落地 PAM 智能部署 Agent 骨架
- 新增 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,记录当前代码骨架、进度、使用方式和下一步计划
2026-05-29 15:53:47 +08:00

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