Learn-Claude-Code | 笔记 | Multi-Agent Platform | s19_new MCP Plugin
目录
写在前面
learn-claude-code 项目目前有两条教程线,一条是之前的 s12 章节的,另一条是最近更新的 s20 章节的,大家如果刚学习这个项目的话推荐直接看最新的 s20 章节的教程即可,由于博主之前学习过 s12 章节的内容,因此打算把 s20 章节中新增章节的内容给补充学习,内容重复的章节博主这边就跳过了。
下面是旧版到新版的对应关系:
| Legacy 12-lesson track | Current 20-lesson track | Topic |
|---|---|---|
| old s01 | new s01 | Agent Loop |
| old s02 | new s02 | Tool Use |
| old s03 | new s05 | TodoWrite |
| old s04 | new s06 | Subagent |
| old s05 | new s07 | Skill Loading |
| old s06 | new s08 | Context Compact |
| old s07 | new s12 | Task System |
| old s08 | new s13 | Background Tasks |
| old s09 | new s15 | Agent Teams |
| old s10 | new s16 | Team Protocols |
| old s11 | new s17 | Autonomous Agents |
| old s12 | new s18 | Worktree Isolation |
| new only | s03, s04, s09, s10, s11, s14, s19, s20 | Permission, Hooks, Memory, System Prompt, Error Recovery, Cron, MCP, Comprehensive Agent |
从上表中我们可以看出我们需要补充的内容包括 s03(已补充)、s04(已补充)、s09(已补充)、s10(已补充)、s11(已补充)、s14(已补充)、s19 以及 s20 八个章节的内容。
新版学习路径如下:
主线:能动手 → 能做复杂任务 → 能记住和恢复 → 能长期运行 → 能协作 → 能扩展并合体

前言
在上篇文章 Learn-Claude-Code | 笔记 | Concurrency | s14_new Cron Scheduler 中,我们介绍了开源项目 learn-claude-code 新版第十四个章节 s14_new: Cron Scheduler 的内容,这篇文章我们继续跟着教程文档来学习多 Agent 相关内容,记录下个人学习笔记,和大家一起分享交流😄
Note:本篇文章主要学习记录 新版教程 第五部分 Multi-Agent Platform 中 s19: MCP Plugin 章节的内容。
github:https://github.com/shareAI-lab/learn-claude-code
reference:https://chatgpt.com/
1. s19: MCP Plugin
到了 s19,这个项目开始进入一个新的阶段:Agent 不再只依赖本地手写工具,而是开始具备接入外部工具系统的能力。
在前面的章节中,我们已经一步步构建出了一个相当完整的 Agent Harness:有 Agent Loop,有工具调用,有权限检查,有 Hooks,有 TodoWrite,有 Subagent,有 Skill,有 Context Compact,有 Memory,有 System Prompt,有 Error Recovery,有 Task System,有 Background Tasks,有 Cron Scheduler,有 Agent Teams,有 Team Protocols,有 Autonomous Agents,也有 Worktree Isolation。
但是这些能力还有一个明显的边界:工具都是我们自己写死在代码里的。
比如 bash、read_file、write_file、create_task、claim_task、create_worktree、remove_worktree 等,都是在 harness 内部手动定义、手动注册、手动实现的。这样做适合教学,也适合理解 Agent Harness 的基础结构,但在真实工程中会遇到一个很大的问题:外部系统太多了。
比如公司内部有 Jira、Notion、部署平台、监控平台、日志系统、知识库、CI/CD 系统,如果每接入一个服务都要在 Agent 代码里重新写一套工具定义、参数 schema、调用逻辑和错误处理,那么工具系统很快就会变得臃肿,而且无法复用。
所以 s19 引入了一个新的概念:MCP Plugin。
MCP 的核心作用,就是让外部服务按照统一协议暴露工具,然后 Agent 通过标准方式发现工具、注册工具、调用工具。换句话说,从 s19 开始,Agent 的工具来源不再只有本地内置工具,还可以来自外部 MCP Server。
这也是 s19 相对于 s18 最关键的变化:s18 解决的是任务执行隔离问题,而 s19 解决的是外部能力接入问题。
2. 问题
在 s18 中,我们已经有了 worktree 隔离能力。Agent 可以把不同任务放到不同 worktree lane 中执行,避免多个任务同时修改同一个目录而发生冲突。
但是,哪怕有了任务隔离,Agent 能调用的工具仍然是固定的。它能读文件、写文件、执行命令、创建任务、创建 worktree、和 teammate 通信,但它并不能天然访问外部系统。
如果我们想让 Agent 查询公司文档、触发部署、查看服务状态、创建 issue,就必须继续在代码里手写工具。例如:
def jira_search_issue(...):
...
def deploy_trigger(...):
...
def notion_search_page(...):
...
然后还要给每个工具写 input_schema,写 handler,写错误处理,写权限规则,写工具描述。这样做的问题是很明显的:工具越多,harness 越重;外部服务越多,Agent 主程序越难维护。
更重要的是,不同外部服务可能由不同团队、不同语言、不同进程实现。如果每个服务都必须改 Agent 主程序才能接入,那么 Agent 的扩展能力就被限制住了。
所以 s19 要解决的问题就是:
当 Agent 需要外部工具时,能不能不要把所有工具都写死在 Agent 里,而是让外部服务按照统一协议暴露工具,Agent 只负责发现、组装和调用?
这就是 MCP Plugin 要解决的核心问题。
它不是再新增一个普通工具,而是新增一种工具接入方式。以前是 “Agent 内置什么工具,就只能用什么工具”;现在是 “外部服务只要实现 MCP 协议,Agent 就可以把它接入自己的工具池”。
3. 解决方案
s19 的解决方案可以概括为一句话:
用 MCPClient 连接外部 MCP Server,发现 server 暴露的工具,然后把这些工具动态组装进 Agent 的工具池中。
这时 Agent 的工具系统就从固定工具池变成了动态工具池。内置工具仍然存在,比如 bash、read_file、write_file、create_task、create_worktree 等;但在这些内置工具之外,Agent 还可以通过 connect_mcp 连接外部 MCP Server,然后把外部工具加入同一个工具池。
教程文档中提供了一张 MCP 架构图:

这张图的重点不在于多了一个工具,而在于工具来源发生了变化。上半部分仍然是熟悉的 Agent Loop:turn → messages → prompt → LLM → tool dispatch → tool results → next turn。这说明 s19 并没有推翻前面的循环结构,Agent 仍然是通过模型选择工具、harness 执行工具、工具结果回填 messages 的方式运行。
真正新增的是中间的 MCP 架构。Agent 侧有一个 MCPClient,它负责连接外部 MCP Server、发现工具、组装工具池,并在模型调用 MCP 工具时把请求转发给对应 server。外部 MCP Server 则负责提供真实能力,例如文档检索、部署触发、服务状态查询等。
也就是说,s19 的设计把工具系统拆成了两层:
Agent Side:
connect_mcp → discover → assemble_tool_pool → call_tool
MCP Server Side:
tools/list → tools/call → return result
在教学部代码中,MCP Server 是用 mock handler 模拟的,不会真正启动外部线程,也不会真的走 JSON-RPC 网络通信。但是它保留了 MCP 的核心形态:server 暴露工具定义,client 发现工具定义,Agent 把这些工具注册进自己的工具池,然后像调用普通工具一样调用它们。
这也是 MCP 最重要的工程意义:Agent 不需要知道工具是谁写的,也不需要知道工具运行在哪个语言或哪个进程里,只要 server 按协议暴露工具,Agent 就能接入。
4. MCP Tool Bridge 流程图分析
Web 教程提供的 MCP Tool Bridge 六张图,实际上是在用一个更直观的方式说明:MCP 工具是如何从 “外部工具箱” 一步步变成 Agent 可调用工具的。
1. Need a New Tool

第一张图展示的是初始状态。Agent 现在只有 built-in belt,也就是本地内置工具,例如 read_file、edit_file、bash。这些工具足够完成本地文件操作,但当任务需要访问外部系统时,这些工具就不够了。
右侧的 External toolbox 里有一个 docs-server,但是它还处于 offline 状态。也就是说,外部服务存在,但 Agent 还没有连接它,也不知道提供什么工具。
这张图想表达的是:Agent 遇到新任务时,不一定要靠自己写死更多工具,而是可以接入一个外部工具箱。
2. Plug In a Server

第二张图中,docs-server 变成 connected 状态。这对应代码里的 connect_mcp("docs")。
连接完成后,Agent 知道有一个外部 MCP Server 已经接入,但此时图中仍然显示 schemas hidden until connected。这个过程可以理解为:连接 server 是第一步,真正可用还要等 server 暴露工具 schema。
这里的关键是 “plug in”。MCP Server 就像一个外部工具箱,Agent 不需要把工具提前写死到自己代码里,而是在运行时把这个工具箱接进来。
3. Read the Tool Labels

第三张图展示了 MCP Server 暴露出来的工具,例如 search、fetch、list_sections。这些工具不是 Agent 原本就有的,而是 MCP Server 通过工具发现机制告诉 Agent 的。
这一步对应 MCP 协议中的 tools/list。server 不只是说 “我存在”,还要告诉 Agent:我有哪些工具?每个工具叫什么?每个工具需要什么参数?每个工具是只读的还是可能产生副作用?
在教学部代码中,这一步由 mock server 的 register() 模拟。它把工具定义和 handler 注册到 MCPClient 里。
4. Name the Tools Clearly

第四张图中,外部工具进入了 Agent workbench,并且名字变成了 mcp__docs__search、mcp__docs__fetch 这样的形式。
这一步非常关键,因为外部工具可能会重名。例如 docs server 有一个 search,notion server 也可能有一个 search,jira server 也可能有一个 search。如果全部直接叫 search,工具池里就会冲突。
所以 s19 使用统一命名规则:mcp__{server}__{tool},例如 mcp__docs__search、mcp__deploy__trigger,这样一来,不同 server 的工具就不会互相覆盖。模型看到工具名时,也能大致知道这个工具来自哪个外部服务。
5. Use It Like Any Tool

第五张图展示的是调用阶段。此时 MCP 工具已经进入 Agent 的工具池,模型可以像调用普通工具一样调用它:mcp__docs__search({ query })。
这说明 MCP 工具在 Agent Loop 中不是特殊旁路,它仍然遵循前面所有章节都在使用的节奏:LLM 选择工具 → harness 执行工具 → tool_result 回填 messages → 下一轮模型继续推理。也就是说,MCP 改变的是工具来源,不改变 Agent Loop 的基本结构。
6. Result Comes Back

第六张图展示的是 MCP 工具调用结果返回。外部 server 返回的数据会被包装成普通 tool_result,然后进入下一轮 messages。
这一步很重要,因为它说明 MCP 工具调用结果对于 Agent 来说并不是一个特殊对象,而是和 bash、read_file、write_file 的结果一样,都会进入对话历史,供下一轮模型继续理解和决策。
所以 MCP Tool Bridge 的完整链路可以概括为:连接 MCP Server → 发现工具 schema → 规范化工具名 → 加入 Agent 工具池 → 模型正常调用 → 结果作为 tool_result 回到循环。
这就是 s19 的核心:外部工具通过 MCP 被桥接进 Agent Harness,并且一旦进入工具池,就和内置工具一样参与模型-工具-结果循环。
完整动画演示如下图所示:

5. 工作原理(代码分析)
s19 的代码是在 s18 的基础上继续扩展的,所以前半部分仍然保留了 task system、worktree system、message bus、protocol state、teammate thread 等内容。真正新增的重点在于 MCP 系统,也就是 MCPClient、mock server、connect_mcp、assemble_tool_pool 以及动态 agent_loop。
我们从上到下分析。
首先,s19 新增了一个 MCPClient 类:
class MCPClient:
"""Discovers and calls tools on an MCP server (mock for teaching)."""
def __init__(self, name: str):
self.name = name
self.tools: list[dict] = []
self._handlers: dict[str, callable] = {}
def register(self, tool_defs: list[dict],
handlers: dict[str, callable]):
self.tools = tool_defs
self._handlers = handlers
def call_tool(self, tool_name: str, args: dict) -> str:
handler = self._handlers.get(tool_name)
if not handler:
return f"MCP error: unknown tool '{tool_name}'"
try:
return handler(**args)
except Exception as e:
return f"MCP error: {e}"
这个类就是教学版 MCP Client 的核心抽象,它里面有两个重要字段:tools 和 _handlers。
tools 保存的是 server 暴露给 Agent 的工具定义,也就是工具名、description、input schema 等信息;_handlers 保存的是工具名到实际执行函数的映射。
在真实 MCP 中,tools/list 会返回工具定义,tools/call 会调用外部工具。而在教学版中,这两件事被简化成了 register() 和 call_tool()。也就是说,register() 模拟工具发现,call_tool() 模拟工具调用。
这一步的工程意义是:Agent 端不再直接拥有工具实现,而是通过 MCPClient 间接调用外部 server 的能力。
接着看 mock server 的实现:
def _mock_server_docs():
client = MCPClient("docs")
client.register(
tool_defs=[
{"name": "search", "description": "Search documentation. (readOnly)",
"inputSchema": {"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"]}},
{"name": "get_version", "description": "Get API version. (readOnly)",
"inputSchema": {"type": "object", "properties": {},
"required": []}},
],
handlers={
"search": lambda query: f"[docs] Found 3 results for '{query}'",
"get_version": lambda: "[docs] API v2.1.0",
})
return client
这里定义了一个 docs server。它暴露两个工具:search 和 get_version。其中 search 需要一个 query 参数,get_version 不需要参数。
注意这里的 description 中有 (readOnly) 标注。这说明工具本身可以声明自己的性质,例如只读工具、危险工具、会产生副作用的工具等。教学版只是把这些信息写在 description 里,真实 Claude Code 中会有更结构化的 tool annotations,用来参与权限判断。
再看另一个 mock server:
def _mock_server_deploy():
client = MCPClient("deploy")
client.register(
tool_defs=[
{"name": "trigger",
"description": "Trigger a deployment. (destructive — requires approval in real CC)",
"inputSchema": {"type": "object",
"properties": {"service": {"type": "string"}},
"required": ["service"]}},
{"name": "status", "description": "Check deployment status. (readOnly)",
"inputSchema": {"type": "object",
"properties": {"service": {"type": "string"}},
"required": ["service"]}},
],
handlers={
"trigger": lambda service: f"[deploy] Triggered: {service}",
"status": lambda service: f"[deploy] {service}: running (v1.4.2)",
})
return client
deploy server 暴露了 trigger 和 status 两个工具。status 是只读的,trigger 是 destructive,因为它会触发部署。这说明 MCP 不只是用来扩展 “查询类工具”,也可以接入会产生真实副作用的外部工具。
当然,在真实系统中,这类 destructive MCP 工具不能直接执行,需要经过权限系统审批。教学版这里没有接入完整权限管线,只是在 description 里标注出来,帮助我们理解 MCP 工具也可以携带风险语义。
两个 mock server 最后被放进 MOCK_SERVERS 字典:
MOCK_SERVERS = {
"docs": _mock_server_docs,
"deploy": _mock_server_deploy,
}
这相当于教学版的 MCP Server 注册表。用户调用 connect_mcp("docs") 时,就会从这里找到对应工厂函数,创建一个 MCPClient 实例。
然后是名称规范化:
_DISALLOWED_CHARS = re.compile(r'[^a-zA-Z0-9_-]')
def normalize_mcp_name(name: str) -> str:
"""Replace non [a-zA-Z0-9_-] with underscore."""
return _DISALLOWED_CHARS.sub('_', name)
这段代码看起来很小,但它很重要。因为 MCP server 名和工具名可能来自外部,不一定符合 Agent 工具名的规范。如果直接拼接到工具名里,可能导致冲突、非法字符,甚至引入命名注入问题。
所以 s19 会把所有非 [a-zA-Z0-9_-] 的字符替换成 _。这也是为什么 MCP 工具最后会变成类似这样的名字:
mcp__docs__search
mcp__deploy__trigger
接着看 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:
available = ", ".join(MOCK_SERVERS.keys())
return f"Unknown server '{name}'. Available: {available}"
mcp_client = factory()
mcp_clients[name] = mcp_client
tool_names = [t["name"] for t in mcp_client.tools]
print(f" \033[31m[mcp] connected: {name} → {tool_names}\033[0m")
return (f"Connected to MCP server '{name}'. "
f"Discovered {len(mcp_client.tools)} tools: {', '.join(tool_names)}")
这就是 MCP server 接入的入口。它先判断 server 是否已经连接,如果已经连接就直接返回;如果没有连接,就从 MOCK_SERVERS 中找到对应工厂函数,创建 MCPClient,并放入全局的 mcp_clients。
这里最关键的是 mcp_clients:
mcp_clients: dict[str, MCPClient] = {}
它记录了当前已经连接的 MCP server。后续 assemble_tool_pool() 会遍历这个字典,把所有 MCP server 的工具都加入 Agent 工具池。
也就是说,connect_mcp 只是连接和发现,真正让工具变成 Agent 可用工具的是下面的 assemble_tool_pool()。
def assemble_tool_pool() -> tuple[list[dict], dict]:
"""Assemble builtin tools + all MCP tools into one pool."""
tools = list(BUILTIN_TOOLS)
handlers = dict(BUILTIN_HANDLERS)
for server_name, mcp_client in mcp_clients.items():
safe_server = normalize_mcp_name(server_name)
for tool_def in mcp_client.tools:
safe_tool = normalize_mcp_name(tool_def["name"])
prefixed = f"mcp__{safe_server}__{safe_tool}"
tools.append({
"name": prefixed,
"description": tool_def.get("description", ""),
"input_schema": tool_def.get("inputSchema", {}),
})
handlers[prefixed] = (
lambda *, c=mcp_client, t=tool_def["name"], **kw: c.call_tool(t, kw))
return tools, handlers
这段代码是 s19 最重要的代码之一。它把工具池从 “固定列表” 变成了 “动态组装”。
首先,它会复制一份内置工具:
tools = list(BUILTIN_TOOLS)
handlers = dict(BUILTIN_HANDLERS)
然后遍历已经连接的 MCP server:
for server_name, mcp_client in mcp_clients.items():
对于每个 server,它会规范化 server 名和 tool 名,然后生成带命名空间的工具名:
prefixed = f"mcp__{safe_server}__{safe_tool}"
最后把这个工具追加到 tools 中,并把对应 handler 加到 handlers 中:
tools.append({
"name": prefixed,
"description": tool_def.get("description", ""),
"input_schema": tool_def.get("inputSchema", {}),
})
handlers[prefixed] = (
lambda *, c=mcp_client, t=tool_def["name"], **kw: c.call_tool(t, kw))
这里有一个细节:lambda 中使用了默认参数 c=mcp_client, t=tool_def["name"]。这是为了避免 Python 闭包变量延迟绑定问题。如果直接写 lambda **kw: mcp_client.call_tool(tool_def["name"], kw),循环结束后所有 handler 可能都指向最后一个 tool。通过默认参数固定当前循环变量,就能保证每个 MCP 工具都绑定到正确的 server 和原始工具名。
所以这一段代码完成了三件事:
- 1. 保留内置工具
- 2. 加入所有已连接 MCP server 的工具
- 3. 为 MCP 工具生成
mcp__server__tool形式的安全名称
接着看系统提示词组装部分:
PROMPT_SECTIONS = {
"identity": "You are a coding agent. Act, don't explain.",
"tools": "Available tools: bash, read_file, write_file, "
"create_task, list_tasks, get_task, claim_task, complete_task, "
"spawn_teammate, send_message, check_inbox, "
"request_shutdown, request_plan, review_plan, "
"create_worktree, remove_worktree, keep_worktree, "
"connect_mcp. MCP tools are prefixed mcp__{server}__{tool}.",
"workspace": f"Working directory: {WORKDIR}",
"memory": "Relevant memories are injected below when available.",
}
相比 s18,s19 的 prompt section 中新增了 connect_mcp,并明确告诉模型:MCP 工具会使用 mcp__{server}__{tool} 前缀。
这一步很重要,因为模型需要知道 MCP 工具的命名规则。否则即使工具池里有 MCP 工具,模型也不一定知道这些工具来自哪里、应该如何理解它们。
系统提示词的组装逻辑如下:
def assemble_system_prompt(context: dict) -> str:
sections = [PROMPT_SECTIONS["identity"],
PROMPT_SECTIONS["tools"],
PROMPT_SECTIONS["workspace"]]
if context.get("memories"):
sections.append(f"Relevant memories:\n{context['memories']}")
mcp_names = list(mcp_clients.keys())
if mcp_names:
sections.append(f"Connected MCP servers: {', '.join(mcp_names)}")
return "\n\n".join(sections)
这里新增了一个判断:
mcp_names = list(mcp_clients.keys())
if mcp_names:
sections.append(f"Connected MCP servers: {', '.join(mcp_names)}")
也就是说,一旦连接了 MCP server,system prompt 中就会出现已连接 server 的信息。这样模型不仅能看到工具列表,也能知道当前已经接入了哪些外部服务。
然后看内置工具列表:
BUILTIN_TOOLS = [
...
{"name": "connect_mcp",
"description": "Connect to an MCP server (docs, deploy) and discover tools.",
"input_schema": {"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"]}},
]
s19 新增的内置工具就是 connect_mcp。它本身不是外部工具,而是一个 “连接外部工具箱” 的工具。模型先调用 connect_mcp,harness 连接 MCP server,然后动态工具池中才会出现新的 MCP 工具。
对应的 handler 是:
def run_connect_mcp(name: str) -> str:
return connect_mcp(name)
并注册到内置 handler 中:
BUILTIN_HANDLERS = {
...
"connect_mcp": run_connect_mcp,
}
最后看 Agent Loop:
def agent_loop(messages: list, context: dict):
tools, handlers = assemble_tool_pool()
system = assemble_system_prompt(context)
while True:
try:
response = client.messages.create(
model=MODEL, system=system, messages=messages,
tools=tools, max_tokens=8000)
except Exception as e:
messages.append({"role": "assistant", "content": [
{"type": "text", "text": f"[Error] {type(e).__name__}: {e}"}]})
return
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return
results = []
for block in response.content:
if block.type != "tool_use":
continue
print(f"\033[36m> {block.name}\033[0m")
handler = handlers.get(block.name)
output = handler(**block.input) if handler else "Unknown"
print(str(output)[:300])
results.append({"type": "tool_result",
"tool_use_id": block.id, "content": output})
messages.append({"role": "user", "content": results})
if any(b.name == "connect_mcp" for b in response.content
if b.type == "tool_use"):
tools, handlers = assemble_tool_pool()
context = update_context(context, messages)
system = assemble_system_prompt(context)
这段代码和前面章节的 Agent Loop 结构保持一致:调用模型、追加 assistant response、判断是否 tool_use、执行工具、把 tool_result 回填 messages。
但是 s19 有两个关键变化。
第一个变化是开头不再直接使用固定的 BUILTIN_TOOLS 和 BUILTIN_HANDLERS,而是调用:
tools, handlers = assemble_tool_pool()
这说明当前工具池是动态生成的。只要 mcp_clients 发生变化,工具池就可能发生变化。
第二个变化是在执行完工具后,判断本轮是否调用了 connect_mcp:
if any(b.name == "connect_mcp" for b in response.content
if b.type == "tool_use"):
tools, handlers = assemble_tool_pool()
context = update_context(context, messages)
system = assemble_system_prompt(context)
如果模型刚刚调用了 connect_mcp,说明外部工具池已经变化了。此时必须重新组装工具池,并重新生成 system prompt。否则模型下一轮仍然只能看到旧工具列表,无法调用新发现的 MCP 工具。
这也是为什么教程文档中特别强调 s19 去掉了 prompt cache。因为从 s10 开始,系统提示词可以缓存;但到了 s19,工具池会在运行时变化,缓存可能会导致工具列表过期。教学版为了简化,直接每次动态组装工具池,保证模型看到的是最新工具状态。
所以 s19 的工作原理可以总结成下面这条链路:用户要求连接外部服务 → 模型调用 connect_mcp → harness 创建 MCPClient → MCPClient 发现 server 工具 → assemble_tool_pool 生成 mcp__server__tool 工具 → system prompt 更新已连接 server 信息 → 模型下一轮可以调用 MCP 工具 → MCP 工具结果以 tool_result 返回 messages。
这条链路说明,MCP 并没有改变 Agent Loop 的本质。模型仍然只是选择工具,harness 仍然负责执行工具,工具结果仍然回到 messages。MCP 真正改变的是工具来源:工具不再只能来自本地代码,而可以来自外部 server。
博主在给定下面的提示词情况下:
Connect to the docs MCP server and search for something
想通过调试看看整个过程发生了什么,我们来具体分析下:
Loop 1

第一次循环的工具和系统提示词

第一次循环模型响应

第一次循环 connect mcp 工具执行

第一次循环工具执行结果

第一次循环重新整合工具和系统提示词
从第一次循环可以看到,Agent Loop 一开始调用 assemble_tool_pool() 时,工具池里还只有内置工具,例如 bash、read_file、write_file、create_task、create_worktree、connect_mcp 等。此时 handlers 中也只有这些内置工具对应的执行函数,还没有任何 mcp__docs__xxx 形式的外部工具。
这也说明了一个关键点:MCP 工具不是一开始就默认出现在工具池里的,而是需要通过 connect_mcp 先完成连接和发现。因此系统提示词里虽然已经告诉模型 “MCP tools are prefixed mcp__{server}__{tool}”,但当前真正可用的外部 MCP 工具还没有被注册进来。
随后模型第一次响应时,并没有直接调用 mcp__docs__search,而是先调用了内置工具 connect_mcp,参数是:
{"name": "docs"}
这一步非常符合 s19 的设计:模型先意识到自己需要外部 docs 能力,于是向 harness 申请连接 docs MCP server。真正的连接动作发生在 connect_mcp() 里,它会从 MOCK_SERVERS 中找到 docs 对应的 mock server 工厂函数,然后创建 MCPClient,并把它放入全局的 mcp_clients 字典中。
调试图里可以看到,mcp_clients 中新增了 docs,并且 tool_names 被解析为:
["search", "get_version"]
终端也打印出:
[mcp] connected: docs → ['search', 'get_version']
这说明 docs server 已经连接成功,并且 server 暴露的两个工具也已经被发现。工具执行结果随后被包装成标准的 tool_result:
{
"type": "tool_result",
"tool_use_id": "...",
"content": "Connected to MCP server 'docs'. Discovered 2 tools: search, get_version"
}
也就是说,对模型来说,connect_mcp 本身仍然只是一次普通工具调用;对 harness 来说,这次工具调用改变了后续工具池的结构。
因此在第一次循环末尾,代码检测到本轮调用过 connect_mcp,于是重新执行:
tools, handlers = assemble_tool_pool()
context = update_context(context, messages)
system = assemble_system_prompt(context)
这一步就是 s19 的关键。重新组装之后,tools 中已经新增了:
mcp__docs__search
mcp__docs__get_version
handlers 中也新增了对应的 lambda handler,用来把模型调用转发到 MCPClient.call_tool()。同时 system prompt 也更新出:
Connected MCP servers: docs
所以第一次循环完成了从 “只有内置工具” 到 “内置工具 + MCP 外部工具” 的转变。换句话说,Loop 1 的核心不是完成搜索,而是完成外部工具箱的接入和工具池刷新。
Loop 2

第二次循环模型响应

第二次循环工具执行结果
第二次循环时,模型已经可以看到更新后的工具池了。因此它没有再次调用 connect_mcp,而是直接调用新注册进来的 MCP 工具:
mcp__docs__search
参数为:
{"query": "Claude Code SDK"}
这说明 MCP 工具一旦进入 tools,对模型来说就和普通工具没有本质区别。模型并不需要知道背后是 mock server、外部进程,还是 JSON-RPC;它只需要看到工具名、description 和 input schema,然后按照工具 schema 生成工具调用即可。
执行阶段仍然走 Agent Loop 中统一的工具执行逻辑:
handler = handlers.get(block.name)
output = handler(**block.input) if handler else "Unknown"
区别只在于,这里的 handler 不再是普通的本地函数,而是 assemble_tool_pool() 里为 MCP 工具动态生成的 lambda。这个 lambda 会继续调用:
c.call_tool(t, kw)
也就是转发给 docs MCPClient 中的 search handler。
最终返回结果是:
[docs] Found 3 results for 'Claude Code SDK'
这个结果同样被包装成普通的 tool_result,追加到 messages 中,供下一轮模型继续使用。
所以 Loop 2 证明了一点:MCP 工具虽然来自外部 server,但一旦完成注册,它在 Agent Loop 里的行为和内置工具完全一致,仍然是 “tool_use → handler → tool_result → messages”。
Loop 3

第三次循环模型响应

第三次循环工具执行结果
第三次循环中,模型根据第二次循环返回的搜索结果继续推理。它认为搜索已经返回了 3 条结果,但为了完整展示 docs MCP server 的能力,还需要再查询一下 API version,于是继续调用另一个 MCP 工具:
mcp__docs__get_version
这个工具没有输入参数,因此 input 是空字典:
{}
这一步说明 MCP server 暴露的多个工具可以在同一个连接状态下被连续调用。第一次连接 docs server 后,mcp__docs__search 和 mcp__docs__get_version 都已经进入工具池,模型可以根据任务需要自由选择其中任意一个。
工具执行结果为:
[docs] API v2.1.0
它同样被加入 results,并作为 tool_result 回填到 messages。这说明 MCP 工具的结果不会走特殊通道,而是继续复用原来的消息循环机制。
这也是 s19 设计得很干净的地方:MCP 插件只负责扩展工具来源,不改变主循环的数据结构。无论是 read_file 的结果,还是 mcp__docs__get_version 的结果,最终都以 tool_result 的形式进入下一轮上下文。
Loop 4

第四次循环模型响应
第四次循环中,模型已经拿到了两个关键信息:一个是 mcp__docs__search 返回的搜索结果,另一个是 mcp__docs__get_version 返回的 API 版本。此时模型不再继续调用工具,而是生成最终文本响应,因此 response.stop_reason 不再是 tool_use,Agent Loop 直接返回。
最终模型整理出一个 docs MCP server 的摘要,包括连接状态、API version、可用工具、搜索 query 和搜索结果:
Status: Connected
API Version: v2.1.0
Tools: search, get_version
Search query: Claude Code SDK
Results: 3 results found
这一步说明 MCP 工具调用并不是孤立执行的。模型会把前几轮 MCP 工具返回的 tool_result 综合起来,然后形成最终答复。换句话说,MCP 只是把外部能力接入进来,最终的理解、总结和决策仍然由模型完成。
本次调试过程中的完整输出如下:

从完整输出可以看到,整个过程非常清晰:
> connect_mcp
[mcp] connected: docs -> ['search', 'get_version']
Connected to MCP server 'docs'. Discovered 2 tools: search, get_version
> mcp__docs__search
[docs] Found 3 results for 'Claude Code SDK'
> mcp__docs__get_version
[docs] API v2.1.0
这几行输出正好对应 s19 的完整 MCP 调用链路。第一步,模型先调用内置工具 connect_mcp,让 harness 连接 docs MCP server,并发现 search、get_version 两个外部工具。第二步,工具池刷新后,模型开始调用动态生成的 MCP 工具 mcp__docs__search。第三步,模型继续调用 mcp__docs__get_version 补充信息。最后,模型根据这些工具结果生成总结。
这次调试最能说明的点是:MCP 并不是给 Agent Loop 另开一条旁路,而是把外部工具 “翻译” 成 Agent 已经认识的工具格式。模型看到的是工具名、描述和 schema;harness 执行的是 handler;结果返回的仍然是 tool_result。
所以,s19 的核心工程价值可以概括为一句话:MCP 让工具池从静态内置列表变成动态外部能力集合,但 Agent Loop 的运行节奏仍然保持不变。
OK,以上就是 s19 MCP Plugin 工作原理的完整分析了。
那大家感兴趣的话可以试试下面这些 prompt 感受下 MCP Tools 引入后的一些变化:
1. Connect to the docs MCP server and search for something
2. Connect to the deploy server and trigger a deployment
3. Connect both servers — what tools are now available?
6. 相对 s18 的变更
| 组件 | 之前 (s18) | 之后 (s19) |
|---|---|---|
| 工具来源 | 全部手写 builtin | 手写 + MCP 外部工具动态发现 |
| 工具池 | 固定 BUILTIN_TOOLS | assemble_tool_pool 动态组装 mcp__ 前缀工具 |
| 名称安全 | 无 | normalize_mcp_name 规范化 |
| 新类型 | — | MCPClient 类(模拟 tools/list + tools/call) |
| 命名空间 | — | mcp__server__tool 避免冲突 |
| 工具描述 | 无标注 | (readOnly)/(destructive) 标注 |
| prompt 缓存 | 有(s10 起) | 去掉——工具池动态变化后缓存失效 |
| Lead 工具 | 17 (s18) | 18 (+connect_mcp) |
| Teammate 工具 | 8 (s18) | 8(不变,MCP 工具仅 Lead 可用) |
| 扩展方式 | 写代码加工具 | 标准协议,任意语言实现 server |
7. 小结
在 s18 中,Agent 已经可以通过 worktree 隔离多个任务的执行空间;而到了 s19,Agent 开始通过 MCP 接入外部工具系统。这个变化让工具不再必须写死在 Agent 主程序里,而是可以由外部 MCP Server 按标准协议暴露出来。
从代码上看,s19 新增了 MCPClient、connect_mcp、normalize_mcp_name 和 assemble_tool_pool。其中 MCPClient 负责模拟 server 工具发现和调用,connect_mcp 负责连接外部 server,normalize_mcp_name 负责保证工具命名安全,assemble_tool_pool 则负责把内置工具和 MCP 工具组装成统一工具池。
从运行流程上看,模型先调用 connect_mcp 连接外部服务,harness 发现该 server 暴露的工具,然后把这些工具以 mcp__server__tool 的形式加入工具池。下一轮模型就可以像调用普通工具一样调用 MCP 工具,工具结果也会以 tool_result 的形式回到 messages。
所以,s19 的重点不是 “多了几个工具”,而是 “工具扩展方式变了”。以前扩展工具需要改 harness 代码;现在外部服务只要实现 MCP 协议,就可以被 Agent 动态接入。
至此,Agent Harness 已经不只是一个本地任务执行系统,而是开始具备连接外部世界的能力。MCP 让 Agent 的工具边界从本地代码扩展到外部服务,也为下一节 s20 Comprehensive Agent 把所有机制整合到同一个完整 harness 中做好了准备。
OK,以上就是本期想要分享的全部内容了。
结语
本篇文章我们围绕 s19 MCP Plugin 这一节,从问题出发,结合流程图与代码实现,完整梳理了 Agent 是如何通过 MCP 协议,将外部服务动态接入自身工具体系的。
相比前面的章节,s19 最本质的变化并不是新增了某个工具,而是第一次改变了工具的来源。此前无论是
bash、read_file、create_task还是create_worktree,所有能力都来自 Agent Harness 内部;而到了这一节,工具开始可以来自外部 MCP Server。Agent 不再需要提前知道工具的实现细节,只需要按照统一协议发现、注册和调用这些能力即可。从工程视角来看,这一步实际上完成了一次非常重要的架构解耦:Agent 不再直接依赖具体工具,而开始依赖工具协议。 外部服务负责暴露工具定义和执行能力,MCPClient 负责发现和转发,Agent Harness 则负责统一组装工具池并驱动模型使用。这样一来,新增能力不再意味着修改主程序,而是可以通过接入新的 MCP Server 来完成。
如果说前面的章节一直在构建一个越来越完整的 Agent Runtime,那么 s19 则完成了另一件同样重要的事情:让这个 Runtime 开始具备连接外部生态的能力。 Agent 不再只是一个封闭运行的本地系统,而开始演化为一个能够持续吸收外部能力的平台。
从整个 learn-claude-code 的演进路径来看,前面的章节一直在回答 “Agent 自己如何变强”,而 s19 开始回答另一个问题—当 Agent 自己的能力不够时,如何借助外部世界的能力继续扩展。 这也正是 MCP 最核心的价值所在。
也正因为如此,MCP Plugin 的意义并不只是新增了一种工具接入方式,而是在整个系统层面完成了一次从 “封闭能力集合” 到 “开放能力生态” 的跃迁。而这一步,也正是下一节 Comprehensive Agent 将所有能力整合到统一平台之前,最重要的一块拼图。
下篇文章我们将来学习新版教程最后一个章节 s20 Comprehensive Agent 的内容,敬请期待🤗
参考
更多推荐



所有评论(0)