From 79437e1c1cf5a402b56d7d18309fa185ea59a823 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Tue, 23 Jun 2026 14:34:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(mcp-py,dispatcher):=20=E8=87=AA=E4=B8=BB?= =?UTF-8?q?=20agent=20=E5=B7=A5=E5=85=B7=E5=8A=A8=E6=80=81=E5=8F=91?= =?UTF-8?q?=E7=8E=B0=E8=A6=86=E7=9B=96=20mcp-py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把动态工具发现从 mcp-go 扩到两台 MCP: - mcp-py:TOOL_META 改成与 mcp-go 同形(cn/desc/agent/params/inject), list_tools 上报这些;run_code 标 agent 可用(params: code)→ 自主 agent 可执行 Python 代码做计算/数据处理。 - dispatcher:agentTools 拆出 discoverTools(subject),分别探 mcp-go / mcp-py 的 list_tools 并合并;某台 MCP 离线即跳过(降级,不阻断)。 验收:mcp-py 未启动时自主 agent 仍正常(探测秒跳过),一次自主调用了 history_get + memory_get 两个动态发现的 go 工具(注入 session_id/user_id); go 全绿、mcp-py py_compile 通过。mcp-py 起着时 run_code 即自动入 agent 菜单。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../internal/eino/react_agent.go | 21 +++++++++---- .../src/sundynix_mcp_py/mcp_gateway.py | 30 ++++++++++++++----- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/sundynix-dispatcher/internal/eino/react_agent.go b/sundynix-dispatcher/internal/eino/react_agent.go index d7efc38..8b2d733 100644 --- a/sundynix-dispatcher/internal/eino/react_agent.go +++ b/sundynix-dispatcher/internal/eino/react_agent.go @@ -101,16 +101,26 @@ type toolCatalogEntry struct { Inject []string `json:"inject"` } -// agentTools 动态构建 ReAct 可用的工具集:调 MCP list_tools 自描述目录 → 取 agent_exposed 的工具 -// → 用上报的参数 schema 建 InvokableTool;inject 参数(user_id/session_id/kb/task_id)服务端运行时 -// 绑定、不暴露给模型。新增工具只需在 mcp-go 注册表标 agent,无需改这里(杜绝硬编码)。 +// agentTools 动态构建 ReAct 可用的工具集:分别向 mcp-go / mcp-py 探 list_tools 自描述目录, +// 取 agent_exposed 的工具按上报参数 schema 建 InvokableTool;inject 参数(user_id/session_id/ +// kb/task_id)服务端运行时绑定、不暴露给模型。某台 MCP 离线即跳过(降级)。 +// 新增工具只需在对应 MCP 注册表标 agent,无需改这里(杜绝硬编码)。 func (o *Orchestrator) agentTools(b *board, taskID string, tr *execTracer) []tool.BaseTool { if o.tools == nil { return nil } + var out []tool.BaseTool + out = append(out, o.discoverTools(contract.ToolSubjectGo, b, taskID, tr)...) + out = append(out, o.discoverTools(contract.ToolSubjectPy, b, taskID, tr)...) + return out +} + +// discoverTools 向某台 MCP(subject 前缀决定 go/py)探 list_tools,把 agent_exposed 的工具 +// 转成 Eino InvokableTool。该 MCP 不可用 / 无应答时返回空(不阻断)。 +func (o *Orchestrator) discoverTools(subject func(string) string, b *board, taskID string, tr *execTracer) []tool.BaseTool { cctx, cancel := context.WithTimeout(context.Background(), toolCallTimeout) defer cancel() - res, err := o.tools.CallTool(cctx, contract.ToolSubjectGo("list_tools"), &contract.ToolCall{Tool: "list_tools"}) + res, err := o.tools.CallTool(cctx, subject("list_tools"), &contract.ToolCall{Tool: "list_tools"}) if err != nil || res == nil || !res.OK { return nil } @@ -120,7 +130,6 @@ func (o *Orchestrator) agentTools(b *board, taskID string, tr *execTracer) []too if json.Unmarshal([]byte(res.Content), &cat) != nil { return nil } - // inject 参数名 → 本任务的运行时值(不进模型菜单)。 injectVal := map[string]any{"user_id": b.uid, "session_id": b.sid, "task_id": taskID, "kb": b.kb} var out []tool.BaseTool @@ -144,7 +153,7 @@ func (o *Orchestrator) agentTools(b *board, taskID string, tr *execTracer) []too } out = append(out, &mcpTool{ mcpName: e.Name, - subject: contract.ToolSubjectGo, + subject: subject, caller: o.tools, taskID: taskID, tr: tr, bind: bind, info: &schema.ToolInfo{ diff --git a/sundynix-mcp-py/src/sundynix_mcp_py/mcp_gateway.py b/sundynix-mcp-py/src/sundynix_mcp_py/mcp_gateway.py index 4b60186..e4be237 100644 --- a/sundynix-mcp-py/src/sundynix_mcp_py/mcp_gateway.py +++ b/sundynix-mcp-py/src/sundynix_mcp_py/mcp_gateway.py @@ -25,12 +25,18 @@ log = logging.getLogger("mcp_py") SUBJECT_PY_ALL = "sundynix.tools.py.>" QUEUE_PY = "mcp-py-workers" -# 工具元信息:名称 → (中文名, 作用简述)。list_tools 据此上报给管理端展示。 +# 工具元信息(与 mcp-go 注册表同形):list_tools 上报给管理端展示 + dispatcher 动态构建自主 agent 工具集。 +# agent=是否暴露给自主 agent;params=模型可填参数;inject=服务端注入参数(不暴露给模型)。 TOOL_META = { - "echo": ("回显", "原样返回入参(调试用)"), - "run_code": ("代码执行", "静态守卫 + Docker 隔离沙箱运行代码(标准档 256m/10s)"), - "parse_document": ("文档解析", "文件 → 纯文本(MinerU / PaddleOCR)"), - "secure_sandbox": ("安全沙箱", "更严资源档(128m/5s)的隔离执行,用于高风险代码"), + "echo": {"cn": "回显", "desc": "原样返回入参(调试用)"}, + "run_code": { + "cn": "代码执行", + "desc": "在隔离沙箱里执行 Python 代码(静态守卫 + Docker,256m/10s)。用于计算、数据处理、逻辑验证。", + "agent": True, + "params": [{"name": "code", "type": "string", "desc": "要执行的 Python 代码", "required": True}], + }, + "parse_document": {"cn": "文档解析", "desc": "文件 → 纯文本(MinerU / PaddleOCR)"}, + "secure_sandbox": {"cn": "安全沙箱", "desc": "更严资源档(128m/5s)的隔离执行,用于高风险代码"}, } @@ -102,10 +108,18 @@ class McpGateway: return str(args.get("text", "")) async def _list_tools(self, args: dict) -> str: - """自省:上报业务工具清单(名称 + 中文名 + 作用),供管理端探活 + 展示。""" + """自省:上报业务工具清单(名称/中文名/作用 + agent 暴露元信息),供管理端展示 + dispatcher 动态发现。""" tools = [ - {"name": n, "cn": cn, "desc": d} - for n, (cn, d) in TOOL_META.items() + { + "name": n, + "cn": m["cn"], + "desc": m["desc"], + "agent_exposed": m.get("agent", False), + "agent_name": m.get("agent_name", ""), + "params": m.get("params", []), + "inject": m.get("inject", []), + } + for n, m in TOOL_META.items() if n in self._tools # 仅上报真正注册的业务工具(list_tools 自身不计入) ] return json.dumps({"service": "mcp-py", "tools": tools})