feat(mcp-py,dispatcher): 自主 agent 工具动态发现覆盖 mcp-py

把动态工具发现从 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) <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-23 14:34:07 +08:00
parent 95f4752d9e
commit 79437e1c1c
2 changed files with 37 additions and 14 deletions
@@ -101,16 +101,26 @@ type toolCatalogEntry struct {
Inject []string `json:"inject"` Inject []string `json:"inject"`
} }
// agentTools 动态构建 ReAct 可用的工具集:调 MCP list_tools 自描述目录 → 取 agent_exposed 的工具 // agentTools 动态构建 ReAct 可用的工具集:分别向 mcp-go / mcp-py 探 list_tools 自描述目录,
// → 用上报参数 schema 建 InvokableToolinject 参数(user_id/session_id/kb/task_id)服务端运行时 // 取 agent_exposed 的工具按上报参数 schema 建 InvokableToolinject 参数(user_id/session_id/
// 绑定、不暴露给模型。新增工具只需在 mcp-go 注册表标 agent,无需改这里(杜绝硬编码)。 // kb/task_id)服务端运行时绑定、不暴露给模型。某台 MCP 离线即跳过(降级)。
// 新增工具只需在对应 MCP 注册表标 agent,无需改这里(杜绝硬编码)。
func (o *Orchestrator) agentTools(b *board, taskID string, tr *execTracer) []tool.BaseTool { func (o *Orchestrator) agentTools(b *board, taskID string, tr *execTracer) []tool.BaseTool {
if o.tools == nil { if o.tools == nil {
return 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 向某台 MCPsubject 前缀决定 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) cctx, cancel := context.WithTimeout(context.Background(), toolCallTimeout)
defer cancel() 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 { if err != nil || res == nil || !res.OK {
return nil 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 { if json.Unmarshal([]byte(res.Content), &cat) != nil {
return nil return nil
} }
// inject 参数名 → 本任务的运行时值(不进模型菜单)。
injectVal := map[string]any{"user_id": b.uid, "session_id": b.sid, "task_id": taskID, "kb": b.kb} injectVal := map[string]any{"user_id": b.uid, "session_id": b.sid, "task_id": taskID, "kb": b.kb}
var out []tool.BaseTool var out []tool.BaseTool
@@ -144,7 +153,7 @@ func (o *Orchestrator) agentTools(b *board, taskID string, tr *execTracer) []too
} }
out = append(out, &mcpTool{ out = append(out, &mcpTool{
mcpName: e.Name, mcpName: e.Name,
subject: contract.ToolSubjectGo, subject: subject,
caller: o.tools, taskID: taskID, tr: tr, caller: o.tools, taskID: taskID, tr: tr,
bind: bind, bind: bind,
info: &schema.ToolInfo{ info: &schema.ToolInfo{
@@ -25,12 +25,18 @@ log = logging.getLogger("mcp_py")
SUBJECT_PY_ALL = "sundynix.tools.py.>" SUBJECT_PY_ALL = "sundynix.tools.py.>"
QUEUE_PY = "mcp-py-workers" QUEUE_PY = "mcp-py-workers"
# 工具元信息:名称 → (中文名, 作用简述)。list_tools 据此上报给管理端展示。 # 工具元信息(与 mcp-go 注册表同形):list_tools 上报给管理端展示 + dispatcher 动态构建自主 agent 工具集
# agent=是否暴露给自主 agentparams=模型可填参数;inject=服务端注入参数(不暴露给模型)。
TOOL_META = { TOOL_META = {
"echo": ("回显", "原样返回入参(调试用)"), "echo": {"cn": "回显", "desc": "原样返回入参(调试用)"},
"run_code": ("代码执行", "静态守卫 + Docker 隔离沙箱运行代码(标准档 256m/10s)"), "run_code": {
"parse_document": ("文档解析", "文件 → 纯文本(MinerU / PaddleOCR"), "cn": "代码执行",
"secure_sandbox": ("安全沙箱", "更严资源档(128m/5s)的隔离执行,用于高风险代码"), "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", "")) return str(args.get("text", ""))
async def _list_tools(self, args: dict) -> str: async def _list_tools(self, args: dict) -> str:
"""自省:上报业务工具清单(名称 + 中文名 + 作用),供管理端探活 + 展示""" """自省:上报业务工具清单(名称/中文名/作用 + agent 暴露元信息),供管理端展示 + dispatcher 动态发现"""
tools = [ 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 自身不计入) if n in self._tools # 仅上报真正注册的业务工具(list_tools 自身不计入)
] ]
return json.dumps({"service": "mcp-py", "tools": tools}) return json.dumps({"service": "mcp-py", "tools": tools})