雨天小六

读懂 Codex(9.17):MCP Adapter 与动态工具目录

· 更新于 2026-08-03 · 专栏:读懂 Codex

#Codex#Agent Runtime#Python#软件架构

具体问题与边界

外部 MCP server 的 list_tools 如何成为当前 Step 可见的 ToolSpec,又如何保持刷新原子性和调用命名稳定?

McpCatalogAdapter 覆盖 list_tools→namespace→Registry source replacement;McpToolHandler 覆盖 call_tool→ToolExecution。连接、认证、resource 与 elicitation 不在 Mini 范围。 下面同时给出状态所有者、顺序、伪代码、故障残留和不可外推边界。

状态所有权

对象所有者生命周期持久化
MCP transportHost/Adapterserver connection 生命周期
remote catalogMCP server刷新时读取
source-owned handlersToolRegistry跨 Step 可替换
public tool nameMcpCatalogAdaptermcp__server__tool随 ToolCall 记录
remote resultMcpToolHandler单调用编码进 ToolResult

正常路径

MCP Adapter 与动态工具目录正常路径图
图 9.17-1:外部 MCP server 的 list_tools 如何成为当前 Step 可见的 ToolSpec,又如何保持刷新原子性和调用命名稳定?
  1. Adapter 调用 transport.list_tools,逐项验证 name、description、inputSchema 的基本类型。
  2. public name 固定编码 server 与 remote tool,避免两个 server 的同名工具直接冲突。
  3. 所有 Handler 构造完成后,Registry.replace_source 一次删除该 server 旧目录并安装新目录,generation 只推进一个可观察版本。
  4. ToolPlanner 下一 Step snapshot 才看到新目录;正在采样的旧 ToolPlan 保持旧 Handler binding。
  5. 调用时 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、取消、外部副作用和结果反馈的实际顺序。

失败、取消与恢复

MCP Adapter 与动态工具目录失败路径图
图 9.17-2:失败不是一个 exception 方框,而是各状态所有者留下的可观察组合。
故障点残留/风险处理
catalog schema 非法不能构造可靠 ToolSpec整次 refresh 拒绝
public name 撞到其他 source可能覆盖 builtinRegistry 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_generation
  • test_router_uses_same_registry_snapshot_as_tool_specs

官方源码导航

Mini Codex 对照

  • src/mini_codex/tools/mcp.py:Catalog Adapter 与 Handler
  • src/mini_codex/tools/registry.py:source 原子替换
  • src/mini_codex/tools/planner.py:Step snapshot

本节结论

一个 MCP server 的目录刷新必须作为一个 Registry generation 可见;当前 Step 的 public spec 与 remote binding 不得漂移。

阅读导航

上一节:9.16 · 下一节:9.18

评论


← 返回文章列表