具体问题与边界
外部 MCP server 的 list_tools 如何成为当前 Step 可见的 ToolSpec,又如何保持刷新原子性和调用命名稳定?
McpCatalogAdapter 覆盖 list_tools→namespace→Registry source replacement;McpToolHandler 覆盖 call_tool→ToolExecution。连接、认证、resource 与 elicitation 不在 Mini 范围。 下面同时给出状态所有者、顺序、伪代码、故障残留和不可外推边界。
状态所有权
| 对象 | 所有者 | 生命周期 | 持久化 |
|---|---|---|---|
| MCP transport | Host/Adapter | server connection 生命周期 | 否 |
| remote catalog | MCP server | 刷新时读取 | 否 |
| source-owned handlers | ToolRegistry | 跨 Step 可替换 | 否 |
| public tool name | McpCatalogAdapter | mcp__server__tool | 随 ToolCall 记录 |
| remote result | McpToolHandler | 单调用 | 编码进 ToolResult |
正常路径
- Adapter 调用 transport.list_tools,逐项验证 name、description、inputSchema 的基本类型。
- public name 固定编码 server 与 remote tool,避免两个 server 的同名工具直接冲突。
- 所有 Handler 构造完成后,Registry.replace_source 一次删除该 server 旧目录并安装新目录,generation 只推进一个可观察版本。
- ToolPlanner 下一 Step snapshot 才看到新目录;正在采样的旧 ToolPlan 保持旧 Handler binding。
- 调用时 Handler 把 public name 还原为保存的 remote_name,传递原 arguments;string content 直接返回,结构化 content JSON 编码,is_error 保留。
Python 风格伪代码
async def refresh():
remote_tools = await transport.list_tools()
handlers = []
for tool in remote_tools:
require_nonempty_string(tool.name)
require_object(tool.inputSchema)
handlers.append(McpToolHandler(
public_name=f"mcp__{server}__{tool.name}",
remote_name=tool.name,
spec=ToolSpec(...),
transport=transport,
))
registry.replace_source(f"mcp:{server}", tuple(handlers))
return tuple(h.spec.name for h in handlers)
class McpToolHandler:
async def execute(arguments, ctx):
ctx.cancellation.raise_if_cancelled()
result = await transport.call_tool(remote_name, arguments)
return ToolExecution(encode_content(result.content), result.is_error)
伪代码没有复制 Rust 语法;它保留了状态修改、await、取消、外部副作用和结果反馈的实际顺序。
失败、取消与恢复
| 故障点 | 残留/风险 | 处理 |
|---|---|---|
| catalog schema 非法 | 不能构造可靠 ToolSpec | 整次 refresh 拒绝 |
| public name 撞到其他 source | 可能覆盖 builtin | Registry collision |
| 刷新时 server 断开 | 旧 generation 仍在 Registry | 不安装半目录 |
| 调用返回 is_error | 远端明确失败 | 同 call_id error ToolResult |
| transport exception | 未知远端结果 | Router 编成 tool failed Result |
| 刷新与采样并发 | 目录版本变化 | 当前 ToolPlan 冻结,下一 Step 更新 |
不变量
一个 MCP server 的目录刷新必须作为一个 Registry generation 可见;当前 Step 的 public spec 与 remote binding 不得漂移。
设计取舍与不能外推的结论
Mini 只实现 Tool catalog/call 的窄适配,不是完整 MCP Client:没有 stdio/http lifecycle、OAuth、resource/template、elicitation、approval mode、progress 或 truncation hook。
测试与复现
cd examples/mini-codex
uv run pytest -q -k 'test_mcp_refresh_replaces_one_server_catalog_generation or test_router_uses_same_registry_snapshot_as_tool_specs'
uv run mypy src
uv run python benchmarks/runtime_baseline.py
test_mcp_refresh_replaces_one_server_catalog_generationtest_router_uses_same_registry_snapshot_as_tool_specs
官方源码导航
- codex-rs/core/src/mcp.rs:MCP ConnectionManager、启动与目录聚合
- codex-rs/core/src/session/mcp_refresh.rs:会话内目录刷新
- codex-rs/core/src/session/mcp_runtime.rs:MCP runtime binding
- codex-rs/core/src/tools/handlers/mcp.rs:MCP ToolCall Handler
- codex-rs/core/src/mcp_tool_call.rs:审批、调用、结果编码与 telemetry
Mini Codex 对照
src/mini_codex/tools/mcp.py:Catalog Adapter 与 Handlersrc/mini_codex/tools/registry.py:source 原子替换src/mini_codex/tools/planner.py:Step snapshot
本节结论
一个 MCP server 的目录刷新必须作为一个 Registry generation 可见;当前 Step 的 public spec 与 remote binding 不得漂移。
评论
登录后即可评论