learn-claude-code s14 实战:MCP 工具发现、命名空间与动态工具池组装机制解析
本篇基于 learn-claude-code 仓库的 s14 章节文档 展开,讲解如何用 MCP(Model Context Protocol)思想把外部服务的工具接入 Agent 的工具循环:MCPClient 保存 server 返回的工具定义与调用入口,connect_mcp 负责连接与工具发现,assemble_tool_pool 把基础工具与已连接的 MCP 工具组装进同一个每轮动态生成的工具池。读完后,你将掌握动态工具池的完整组装链路、mcp__{server}__{tool} 命名规范与冲突防护、宿主侧权限策略的设计理由,以及错误如何被约束在工具边界内而不中断 Agent Loop。
问题背景:每接一个服务就要手写一批工具
前面章节的基础工具都直接写在 code.py 里。要接入文档系统和部署平台时,最直接的做法是继续手写 search_docs、deploy_status、trigger_deploy,但每增加一个服务,都要重新维护一套工具定义、参数 JSON Schema 和调用代码,工具来源与 Harness 深度耦合。
s14 的解法是把职责拆成两个角色:
- server:提供工具列表(相当于
tools/list)和调用入口(相当于tools/call); - Harness:负责连接、为工具分配模型可见的名字、做权限检查,并把发现的工具交给模型。
课程里的 docs 和 deploy 是进程内模拟 server,用来展示 tools/list、tools/call 和动态工具池;真实的 MCP transport 不在本章实现,这一点在文档中明确说明,属于课程的教学边界。
方案骨架:三个新增组件
本章从 s04 Hooks 章节 的五个基础工具(bash、read_file、write_file、edit_file、glob)和 Hooks 出发,增加三个部分:
MCPClient:保存 server 返回的工具定义(tools)和调用入口(_handlers);connect_mcp:连接一个 server 并取得它的工具列表,作为暴露给模型的工具;assemble_tool_pool:把基础工具与所有已连接 server 的工具组装到同一个工具池。
对应源码集中在 s14_mcp_plugin/code.py 的 "New in s14: MCP discovery and dispatch" 段落(约 L158-L356)。
工作原理详解
1. 基础 Agent Loop 不需要改变
每轮调用模型前,Harness 组装当前工具池,工具列表随连接状态动态变化:
def agent_loop(messages: list):
while True:
tools, handlers = assemble_tool_pool()
response = client.messages.create(
model=MODEL,
system=assemble_system_prompt(),
messages=messages,
tools=tools,
max_tokens=8000,
)
...
连接新 server 后,下一轮 assemble_tool_pool() 会把新工具加入模型输入。工具执行后,结果仍作为 tool_result 追加到 messages,与基础工具的处理路径完全一致。s14 章节文档 与 code.py 主循环 中的 agent_loop 实现了这段逻辑:模型返回 stop_reason != "tool_use" 时触发 Stop hooks 并退出,否则逐块执行 tool_use 并把 tool_result 以 user 消息回注。
从源码结构看,系统提示词也会同步更新:assemble_system_prompt() 在没有已连接 server 时返回 BASE_SYSTEM,有连接时追加 Connected MCP servers: ...(L359-L362),让模型知道当前有哪些外部能力可用。
2. MCPClient:保存发现结果和调用入口
class MCPClient:
def register(self, tool_defs, handlers):
self.tools = list(tool_defs)
self._handlers = dict(handlers)
def call_tool(self, tool_name, args):
handler = self._handlers.get(tool_name)
if not handler:
return f"MCP error: unknown tool '{tool_name}'"
try:
return str(handler(**args))
except Exception as error:
return f"MCP error: {type(error).__name__}: {error}"
源码中的实现比文档骨架更严格(L160-L187):register() 在注册时会做三重校验——每个工具必须有非空字符串 name、server 内部不允许重名、每个工具名都必须有对应 handler,任一条件不满足直接抛 ValueError。这相当于把 server 侧的契约校验前置到发现阶段。
call_tool() 的注释写得很直白:错误会返回给模型,不会直接结束 Agent Loop。未知工具返回 MCP error: unknown tool '...',handler 抛异常则返回 MCP error: {异常类型}: {异常信息},模型可以在下一轮自行修正。
3. connect_mcp:只负责连接和发现
def connect_mcp(name: str) -> str:
if name in mcp_clients:
return f"MCP server '{name}' already connected"
factory = MOCK_SERVERS.get(name)
if not factory:
return f"Unknown server '{name}'"
server = factory()
mcp_clients[name] = server
...
开始时,模型只看到五个基础工具和 connect_mcp。调用 connect_mcp(name="docs") 后,Harness 在模块级字典 mcp_clients 中保存 docs client。源码里 connect_mcp 还有两点值得注意(L279-L292):
- 重复连接同名 server 不报错,而是返回
already connected,幂等友好; - 未知 server 名会返回可用清单,例如
Unknown server 'x'. Available: docs, deploy,方便模型在下一轮自我纠正。
而暴露给模型的 CONNECT_TOOL 定义中,name 参数直接用了 enum: ["docs", "deploy"](L299-L307),从工具 Schema 层面就限制了可连接的范围。
4. 前缀命名:区分不同 server 的同名工具
多个 server 都可能提供 search 或 status。Harness 统一使用如下命名:
mcp__{server}__{tool}
normalize_mcp_name()(L203-L208)用正则 [^a-zA-Z0-9_-] 把不适合模型工具名的字符替换为下划线,并把"规范化后为空"视为非法。组装工具池时还会检查规范化后的名称冲突和 64 字符长度限制:
prefixed = f"mcp__{safe_server}__{safe_tool}"
if len(prefixed) > 64:
raise ValueError(f"MCP tool name is longer than 64 characters: {prefixed}")
if prefixed in origins:
raise ValueError("MCP tool name collision after normalization")
因此 docs.one/get.version 和 docs_one/get_version 规范化后都会变成 mcp__docs_one__get_version,Harness 会抛出 ValueError 而不是让两个工具悄悄映射到同一个名字。tests/test_agent_teams_runtime.py 的 test_normalized_mcp_tool_name_collisions_are_rejected 正是构造这两个 server 来验证碰撞必被拒绝。
这里的设计取向值得注意:命名冲突属于配置/集成错误,直接 raise 让 Harness 停止组装,而不是静默降级;而工具调用期的错误才走"返回给模型"的容错路径。
5. 工具定义和 handler 一起加入工具池
tools.append({
"name": prefixed,
"description": tool_def.get("description", ""),
"input_schema": schema,
})
handlers[prefixed] = (
lambda *, client=server, tool=raw_name, **kwargs:
client.call_tool(tool, kwargs)
)
模型看到带前缀的名字;handler 内部仍使用 server 的原始工具名调用 MCPClient。两个细节(L313-L356):
- lambda 闭包陷阱:
lambda *, client=server, tool=raw_name用默认参数固化当前迭代的 client 和工具名。如果不加默认参数,循环结束后所有 lambda 都会捕获到最后一个工具,这是此类循环注册 handler 的经典 bug; - Schema 校验:
inputSchema必须存在且type为object(缺省按object处理),否则抛ValueError: Invalid input schema,保证交给模型的 Schema 合法。
同时组装过程会重建 mcp_tool_policies:每个 mcp__ 工具的策略取自 MCP_HOST_POLICY.get((server_name, raw_name), "confirm"),供后续权限检查使用。
6. 权限由宿主配置决定,而非 server 自述
MCP server 的工具定义里可以带 readOnlyHint 或 destructiveHint 注解(模拟 server 中 docs.search 标了 readOnlyHint: True,deploy.trigger 标了 destructiveHint: True),但这些信息来自 server 自己,不能直接作为授权依据。本章使用宿主侧策略:
# Authorization comes from host configuration, never server descriptions.
MCP_HOST_POLICY = {
("docs", "search"): "allow",
("docs", "get_version"): "allow",
("deploy", "status"): "allow",
("deploy", "trigger"): "confirm",
}
permission_hook() 是一个 PreToolUse hook(L384-L408),对 mcp__ 前缀的工具按规范化名查询策略:策略不是 allow 就打印 [permission] 提示并要求用户输入 y/N 确认,拒绝则返回 Permission denied by user。未配置的外部工具默认 confirm——即使 description 写着 readOnly,也不会自动放行。
tests/test_agent_teams_runtime.py 的 test_mcp_permission_uses_host_policy 覆盖了三条路径:mcp__deploy__status 按策略直接放行(返回 None);mcp__deploy__trigger 用户答 no 时返回 Permission denied by user;构造一个未在策略中的 mcp__third_party__erase(description 里伪装成 readOnly),同样需要人工确认,验证了"server 自述不可信"这条边界。
7. 工具输入错误留在工具边界内
模型可能漏传参数,也可能传入 server 不接受的字段。execute_tool()(L450-L462)先触发 PreToolUse hooks,权限被拦截则直接返回拦截信息;否则执行 handler,异常被捕获为 Error: {类型}: {信息}。MCPClient.call_tool() 再兜一层,返回形如:
MCP error: TypeError: <lambda>() missing 1 required argument: 'query'
模型可以在下一轮修正参数,而不是让课程脚本直接退出。这就是"错误留在工具边界内"的含义:两层 try/except 分别覆盖 Harness 分发层和 MCP client 层,任何一层出错都转化为普通的 tool_result 文本。
相对 s04 的变化
| 组件 | s04 | s14 |
|---|---|---|
| 基础工具 | 五个固定工具 | 保持不变 |
| 工具来源 | code.py 中的定义 |
基础工具加动态发现的 MCP 工具 |
| 工具池 | 固定 TOOLS |
每轮由 assemble_tool_pool() 组装 |
| 外部工具名 | 无 | mcp__{server}__{tool} |
| 权限 | Shell 和路径检查 | 增加宿主侧 MCP 策略 |
| MCP transport | 无 | 使用进程内模拟 server 展示协议边界 |
本章不带入 Task、Background、Cron、Team 或 Worktree;这些机制会在 s15 的 Integrated Harness 中与 MCP 合并。
试一下:完整运行流程
依赖见 requirements.txt(anthropic>=0.25.0、python-dotenv>=1.0.0),另外需要在 .env 中配置 ANTHROPIC_API_KEY,MODEL_ID 从环境变量读取。运行:
cd learn-claude-code
python s14_mcp_plugin/code.py
输入:
连接 docs server,搜索 agent hooks,并告诉我当前文档 API 版本。
一次典型工具轨迹是:
connect_mcp(name="docs")
mcp__docs__search(query="agent hooks")
mcp__docs__get_version()
connect_mcp 的返回是 Connected to MCP server 'docs'. Discovered 2 tools: search, get_version,模型据此在下一轮直接按规范化名调用 mcp__docs__search。仓库中的场景数据 web/src/data/scenarios/s14.json 记录了同一条轨迹(连接 docs → 规范化命名 → 搜索 → 摘要),可作为交互过程的对照样本。
再输入:
连接 deploy server,查看 web 服务状态,不要触发部署。
status 会按宿主策略直接执行;trigger 需要用户确认。测试侧 test_mcp_lesson_builds_on_the_base_kernel 验证了核心断言:连接前工具池只有六个内置工具(五个基础工具加 connect_mcp)且不含 mcp__docs__search,connect_mcp("docs") 之后 mcp__docs__search 出现在工具池中,且 handlers_after"mcp__docs__search" 返回 [docs] Found 3 results for 'hooks'。
设计要点小结
从源码结构看,s14 传递的是一套可以迁移到真实 MCP 集成中的工程模式:
- 发现与调用分离:
register()保存定义、call_tool()执行调用,两者契约独立,替换成真实 transport 时只需换掉 client 内部实现; - 命名空间即边界:
mcp__{server}__{tool}前缀加规范化、碰撞检测、64 字符上限,把多 server 同名冲突变成显式错误; - 授权只认宿主配置:server 的
readOnlyHint/destructiveHint仅作展示参考,MCP_HOST_POLICY才是授权来源,未配置默认confirm; - 错误降级为文本:调用期异常一律变成
MCP error: ...的tool_result,交给模型自我修正,保持 Agent Loop 不中断。
下一章 s15 Integrated Harness 会把基础工具、Hooks、Skills、Context、Memory、Task、Background、Cron、Teams 和 MCP 放进同一个运行时;s14 英文文档 与 日文文档 提供同一内容的对照版本。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00