第 4 篇把工具调用循环拆到了最细:手写 schema、手写分发、手写回传,一个 get_weather 从定义到执行全在你的仓库里。这套打法对项目专属工具毫无问题,但工具世界的大头本来就不在你的仓库里——GitHub、浏览器、数据库、网页抓取,每个都值得接,每个都手写一遍就是重复造轮子。第 9 篇结尾预告的主题正对着这里:MCP 把工具从"写死在代码里"变成"即插即用的服务"——工具由独立进程自我描述、按协议调用,你的代码只负责连接与消费。
本文是《Anthropic SDK 从入门到实战》第 10 篇。先交代当前 SDK 的 MCP 官方口径(有一处旧教程陷阱必须先纠正),再走本地 stdio 路线:连接现成服务、发现工具、与普通工具混用,最后用官方 fetch server 组装一个网页摘要助手。全文按"MCP 是什么 → 两条路线 → stdio 连接 → 工具发现 → 混合消费 → 取舍判断 → 实战 → 总结"组织。
1、MCP 是什么
2024 年 11 月,Anthropic 把内部连接模型与外部工具的协议开源,就是 MCP(Model Context Protocol)——一个基于 JSON-RPC 的开放协议,规定了客户端与服务端如何握手、如何列出能力、如何调用。它如今由社区共同演进,Claude 之外的主流编程助手与各家 Agent 框架都已支持,协议官网维护着规范与实现清单。
协议规定服务端能提供三类能力:
- 工具(tools):模型可调用的函数,如抓网页、查仓库
- 提示(prompts):服务预置的提示词模板
- 资源(resources):服务暴露的文档与数据
本篇聚焦第一类,它是第 4 篇工具调用在"供应链"上的升级;三类能力里工具与 Messages API 的对接也最直接,生态里的现成服务绝大多数以工具为主。手写与 MCP 两种形态对比如下:
| 维度 | 第 4 篇手写工具 | MCP 工具 |
|---|---|---|
| 定义来源 | 代码里手写 schema | 服务自我描述,发现即定义 |
| 执行位置 | 自己写的分发函数 | 服务进程内部 |
| 复用范围 | 单个项目独占 | 任意 MCP 客户端共享 |
| 新增工具 | 改代码重新部署 | 接一个新服务即可 |
解读:表里最后一行是关键差别。手写形态下工具的定义、执行、维护与你的项目同生共死;MCP 形态下这三件事全部移进独立进程,你的代码只剩"连接加消费"两步——换一个服务进程,模型的能力面立刻换一批,代码一行不动,这正是"即插即用"的含义。
2、两条接入路线
先纠正一个口径,避免照抄旧教程。早期版本的 Python SDK 曾把 stdio 客户端内置在 anthropic 包里:mcp_servers 参数直接传 MCPStdioServerParameters,SDK 负责拉起子进程并转换工具。
注意:这套内置客户端在当前 2.x SDK(本系列的 2.3.0)源码里已经移除,
MCPStdioServerParameters等类型不复存在,照抄 2025 年的旧教程会在导入这一步直接报错。现行口径以 SDK 的 helpers 文档为准。
现在的官方分工一句话:远程服务走连接器,本地服务走 helpers。两条路线对比如下:
| 维度 | 本地 stdio 路线 | 远程 connector 路线 |
|---|---|---|
| 服务位置 | 本机子进程 | 服务商托管的 URL |
| 连接方式 | mcp 包拉起进程 | mcp_servers 传 URL 定义 |
| 工具执行 | 本地子进程内 | Anthropic 服务端代办 |
| 能力范围 | 工具、提示、资源 | 工具 |
| 接口稳定性 | 正式能力 | beta |
connector 路线的代码极短,以连接 Notion 官方远程服务为例:
response = client.beta.messages.create(
model="claude-sonnet-5-5",
max_tokens=1024,
mcp_servers=[ # beta 命名空间下的连接器
{"type": "url", "url": "https://mcp.notion.com/mcp",
"name": "notion"}
],
messages=[{"role": "user", "content": "搜索我空间里关于发布的页面"}],
)
解读:连接器把发现、调用、回传的整个 MCP 往来交给 Anthropic 服务端代办,你只出 URL,代价是只覆盖工具一类能力且仍在 beta。本地 stdio 是另一头:子进程跑在你机器上,协议往来发生在你的事件循环里,控制权与能力面都完整。官方文档的判断标准原样转述:远程 URL 且只要工具,用 mcp_servers;要本地服务、要用提示与资源、要控制连接,用 helpers。细则见官方 MCP 连接器文档。本篇主线走本地 stdio。
3、stdio 路线:拉起服务进程
本地路线要装两个包,命令如下:
pip install "anthropic[mcp]" # SDK 附加项,带上官方 mcp 包
pip install mcp-server-fetch # 官方 fetch server,抓网页用
解读:anthropic[mcp] 的附加项装的是 modelcontextprotocol 官方 Python SDK(要求 Python 3.10 以上),SDK 的 MCP 转换层依赖它;mcp-server-fetch 是官方 servers 仓库里的现成服务,装好后可用 python -m mcp_server_fetch 运行。连接它的三件套如下:
import asyncio, sys
from mcp import ClientSession
from mcp.client.stdio import stdio_client, StdioServerParameters
SERVER = StdioServerParameters(
command=sys.executable, # 用当前解释器拉起子进程
args=["-m", "mcp_server_fetch"], # 官方 fetch server
)
async def main():
async with stdio_client(SERVER) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize() # 协议握手:能力协商
print("fetch server 已就绪")
asyncio.run(main())
解读:三层各司其职。stdio_client 按 command 加 args 拉起子进程,拿它的标准输入输出当传输通道——"stdio 路线"因此得名;ClientSession 在通道上讲协议;initialize() 是协议规定的握手,双方交换版本与能力清单。两段 async with 退出时依次关会话、关进程,不留孤儿进程。留意一个细节:StdioServerParameters 是 mcp 包的类而不是 SDK 的——连接归 mcp 包,转换归 SDK,这正是现行分工在代码里的样子。
4、list_tools 工具发现
连上之后第一件事不是调用,是问"你有什么"。协议为此规定了发现方法,一行可用:
result = await session.list_tools() # 工具发现:列出服务的能力
for t in result.tools:
print(f"{t.name} —— {t.description}")
一次运行的输出如下:
fetch —— Fetches a URL from the internet and extracts its contents as markdown.
解读:fetch server 只提供一个 fetch 工具,描述写明"抓取 URL 并转成 markdown"——这段描述就是给模型看的"什么时候该用我",与第 4 篇手写的 description 用途完全相同。每个工具对象还带输入 schema:url 必填;max_length 限制返回长度,默认 5000 字符;start_index 支持从指定位置续读,网页太长时模型自己分段翻页。发现即定义——list_tools 拿到的名字、描述、schema 三件套,与第 4 篇手写在 tools 参数里的字段一一对应,模型侧看不出任何差别。
5、MCP 工具与普通工具并存
消费环节 SDK 已经封好:anthropic.lib.tools.mcp 的 async_mcp_tool 把一个 MCP 工具连同它的会话包装成"可运行工具";普通函数工具用 beta_async_tool 装饰器定义,两者混进同一个 tools 列表,交给 tool_runner 跑完整个循环:
import asyncio
from anthropic import AsyncAnthropic
from anthropic.lib.tools import beta_async_tool
from anthropic.lib.tools.mcp import async_mcp_tool
client = AsyncAnthropic()
@beta_async_tool
def now() -> str:
"""返回当前时间,ISO 8601 格式。"""
from datetime import datetime
return datetime.now().isoformat()
async def main():
async with stdio_client(SERVER) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = [async_mcp_tool(t, session)
for t in (await session.list_tools()).tools]
tools.append(now) # 两类工具同列共存
runner = client.beta.messages.tool_runner(
model="claude-sonnet-5-5", max_tokens=1024, tools=tools,
messages=[{"role": "user",
"content": "现在几点?顺便抓取 example.com 概括。"}],
)
async for message in runner:
print(message.stop_reason) # 观察循环走到哪一步
asyncio.run(main())
解读:tool_runner 就是第 4 篇 while 骨架的官方封装——迭代它,遇到 stop_reason 为 tool_use 的消息就自动执行工具并回传,其余停止原因在产出最终消息后结束迭代。async_mcp_tool 内部做的事恰好是第 4 篇循环里手写的三步:用工具对象组装定义,模型点名时调用 session.call_tool,把 MCP 结果转成 tool_result。对模型来说两类工具毫无差别,整条消息流如下:
解读:fetch 调用走右支进子进程,now 调用直接在本地函数执行,两条支路的结果汇进同一条回传消息——工具的产地对模型透明,透明才有组合的自由。不想引入 runner 也行:把发现的工具转成定义列表放进 tools,在执行分支里调 session.call_tool,第 4 篇的手动循环骨架一字不改照样能转。
6、何时值得上 MCP
判断只看工具的供需关系。三种情况明确值得:
- 多应用共享:Claude Desktop、IDE 插件、你的脚本接同一个服务,工具维护一处、处处生效
- 吃现成生态:官方 servers 仓库里 GitHub、Postgres、Playwright 等开箱即用,Notion 等厂商也自托管了服务
- 团队统一入口:工具集中在一个服务里,权限、审计、升级统一管理
反向也同样明确:一次性专用工具参数简单、只此一家使用,第 4 篇手写 schema 加分发十行解决,起服务反而是负担;强延迟敏感的在线链路要多付一跳进程通信与协议握手,好在握手只发生一次,后续每次工具调用就是与常驻子进程的一次往返;工具定义会全部计入输入 token,服务越热闹前缀越贵——好在 async_mcp_tool 支持 cache_control 参数,给工具列表挂上第 7 篇的缓存断点,这块成本能压下去。
一句话决策:工具要"多处复用"或"别人已经写好"时上 MCP,否则手写更快。
7、实战:网页摘要助手
零件齐了,组装完整的一单:fetch 工具抓页面,模型用中文概括。完整脚本如下:
import asyncio, sys
from anthropic import AsyncAnthropic
from anthropic.lib.tools.mcp import async_mcp_tool
from mcp import ClientSession
from mcp.client.stdio import stdio_client, StdioServerParameters
client = AsyncAnthropic()
SERVER = StdioServerParameters(
command=sys.executable, args=["-m", "mcp_server_fetch"])
async def main():
async with stdio_client(SERVER) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = [async_mcp_tool(t, session)
for t in (await session.list_tools()).tools]
runner = client.beta.messages.tool_runner(
model="claude-sonnet-5-5", max_tokens=2048, tools=tools,
messages=[{"role": "user", "content":
"抓取 https://modelcontextprotocol.io 首页,"
"中文概括它是什么、站点提供哪些内容,100 字以内。"}],
)
final = None
async for message in runner:
final = message # 迭代停在最终回答那轮
print(next(b.text for b in final.content if b.type == "text"))
asyncio.run(main())
一次典型运行的输出如下:
MCP 是一个开放协议,标准化 AI 应用与外部数据和工具的连接方式;
站点提供协议规范、多种语言的 SDK、教程文档与服务器示例。
解读:从连接到答案约 25 行,其中没有一行是"工具代码"。三个行为值得注意:其一,概括这种小任务模型只调一次 fetch,问题更复杂时它会带着 start_index 自己分段翻页;其二,fetch server 抓不到目标(robots.txt 拒绝、目标是非公网地址)时会把原因作为工具结果返回,模型据此作答或放弃,异常不会抛进你的脚本;其三,换任务不用换骨架——把 SERVER 换成文件系统服务(Node.js 环境下用 npx -y @modelcontextprotocol/server-filesystem),同一个脚本立刻变成目录问答助手,即插即用在此兑现。
8、总结
本篇把 MCP 接入的 2026 年 10 月口径讲全:MCP 把工具从代码搬进自我描述的服务进程;SDK 旧内置 stdio 客户端已移除,现行两条路线——远程 URL 走 beta 的 mcp_servers 连接器,本地 stdio 走 mcp 包连接加 SDK helpers 转换;list_tools 发现即定义,async_mcp_tool 包装后与普通工具混进同一个 tools 列表,tool_runner 把第 4 篇的循环自动跑完。一句话带走:工具多处复用或已有现成服务就上 MCP,一次性小工具手写更快。
留三个练习:其一,把第 7 节脚本的 SERVER 换成文件系统服务(需本机有 Node.js),让它回答"这个目录里最长的 md 文件讲了什么";其二,给第 5 节的 tools 再加一个自写的中文工具,观察模型在 MCP 工具与本地工具之间的选择依据;其三,挑一个厂商托管的远程服务,用 connector 路线重跑摘要任务,对照两条路线的代码量与能力差异。对于工具需求已经超出自己仓库的开发者,建议今天就把 fetch server 这 25 行跑起来——即插即用的体感,跑一次就明白。下一篇讲错误处理与可观测性:限流、超时、断流这些线上必遇的问题,一次讲清。

