工具实现与 MCP 服务器开发
一次工具往返的确切形状,与「什么时候值得建一个 server」
TS 8.1 考「怎么把一个工具做对」——定义字段、description 写作、错误处理、工具使用模式;TS 8.2 考「什么时候该把它做成一个 MCP server」——三种原语、通信模式、client 与 server 的分界。官方样题 Q3 就出在本章,它的三条排除理由几乎是这一整章的提纲,所以最后一节专门精讲它。本章是配置速查与概念推演的混合体:字段与配置进速查栏,取舍进推演。
vb:。速查栏里标「真实」的都有登记支撑,标「不存在」的是本书按 D1 工艺自制的干扰项,无官方出处,捏造只发生在名字这一层。MCP 的原句我全部取自 Anthropic 自家页面,没有引 modelcontextprotocol.io(它不在本书的来源白名单内)。一、tool definition:三个字段,与那个「最重要的因素」
客户端工具定义放在请求顶层的 tools 参数里,一个用户自定义工具由三个必需字段构成。
| 字段 | 官方描述 |
|---|---|
name | 工具名,须匹配正则 ^[a-zA-Z0-9_-]{1,128}$ |
description | 一段详细的纯文本描述,说明工具做什么、何时该用、如何表现 |
input_schema | 一个 JSON Schema 对象,定义该工具预期的参数 |
input_examples | (可选)一组示例输入对象,帮助 Claude 理解怎么用这个工具 |
description 不是文档,是这个工具的运行时行为规格。官方把话说得非常重:写极其详细的 description 是工具表现里最重要的因素。它应当解释:工具做什么;何时该用以及何时不该用;每个参数的含义与它如何影响行为;以及重要的注意事项与限制——官方专门点了一类容易被忘的内容:这个工具不返回什么信息。篇幅上给了硬指标:每个工具的 description 至少 3–4 句,复杂工具更多。
二、tool set 构建:三条官方最佳实践
单个工具写好之后,还有一层是「这一套工具放在一起好不好用」。官方给了三条:
- 把相关操作合并成更少的工具。与其为每个动作各建一个(
create_pr、review_pr、merge_pr),不如合成一个带action参数的工具——更少、更强的工具减少选择歧义,让你的工具面更好导航。 - 工具名用有意义的命名空间。跨多个服务或资源时,用服务名做前缀(
github_list_prs、slack_send_message),工具库变大时选择才不会有歧义。 - 工具返回值只给高信号信息。返回语义化、稳定的标识符(slug 或 UUID)而不是不透明的内部引用;只包含 Claude 推理下一步所需的字段——臃肿的响应浪费 context,也让 Claude 更难提取要点。
第三条把工具设计和 ch01 P9 接上了:工具的返回值是你唯一能完全控制的那部分上下文。一个返回整行数据库记录的工具,和一个返回三个字段的工具,在十轮之后的上下文账单上差出一个量级。
速查栏 1 · tool definition 字段
tools[].name ✔ 真实 · 须匹配 ^[a-zA-Z0-9_-]{1,128}$
tools[].description ✔ 真实 · 至少 3–4 句
tools[].input_schema ✔ 真实 · JSON Schema 对象
tools[].input_examples ✔ 真实 · 可选,示例输入对象数组
tools[].strict ✔ 真实 · 保证入参严格符合 schema
tools[].output_schema ✘ 不存在 · 自制干扰项
tools[].required ✘ 不存在 · 自制干扰项
tools[].timeout_ms ✘ 不存在 · 自制干扰项
tools[].retry_policy ✘ 不存在 · 自制干扰项
三、一次工具往返的确切形状
先记住一条结构性差异:Claude API 与那些把 tool use 分开、或使用 tool / function 这类特殊 role 的 API 不同——它把工具直接集成进 user 与 assistant 的消息结构里。消息装的是 text / image / tool_use / tool_result block 的数组:user 消息装 client 内容与 tool_result,assistant 消息装 AI 生成的内容与 tool_use。从别家 SDK 迁过来的人最容易在这里找一个不存在的 tool role。
往返的三拍:
- 模型侧。响应的
stop_reason是tool_use,带一个或多个tool_useblock,字段是id(这一次工具使用的唯一标识,用来在稍后把结果对应回来)、name、input(符合该工具input_schema的输入对象)。 - 你的代码。客户端工具由你的应用执行,服务端工具由 Anthropic 执行。
- 回传。发一条
role为user的新消息,content block 类型tool_result,字段:tool_use_id(对应哪一次请求)、content(可选,字符串或嵌套 content block 列表)、is_error(可选,工具执行出错时设true)。
两条格式硬要求,违反直接 400。其一,tool_result block 必须紧跟在对应的 tool_use block 之后——assistant 的 tool use 消息与 user 的 tool result 消息之间不能夹任何消息。其二,在含 tool result 的那条 user 消息里,tool_result block 必须排在 content 数组的最前面,任何 text 都要排在所有 tool result 之后。「我先写一句 Here are the results 再放结果」正是这条的经典违反。
tool_result block 里,而不是放进 system prompt 或普通的 user text block。这是 ch01 P2 在工具层的官方落点;防护的完整层次见 ch14。速查栏 2 · content block 与回传字段
tool_use.id ✔ 真实 · 用来匹配结果
tool_use.name / tool_use.input ✔ 真实
tool_result.tool_use_id ✔ 真实 · 对应哪一次 tool_use
tool_result.content ✔ 真实 · 可选
tool_result.is_error ✔ 真实 · 可选,出错时设 true
tool_result.status ✘ 不存在 · 自制干扰项
tool_result.error_code ✘ 不存在 · 自制干扰项
role: "tool" ✘ 不存在 · 自制干扰项
role: "function" ✘ 不存在 · 自制干扰项
四、错误处理:三类错误,三种处置
| 错误类型 | 官方处置 |
|---|---|
| 工具执行出错(网络错误等) | 把错误消息放进 content 并带 "is_error": true;Claude 会把这个错误纳入它给用户的回应 |
| 调用无效(缺必填参数等) | 开发期最好的办法是把 description 写得更详细再试;也可以回一个标注错误的 tool_result,Claude 会带着补上的信息再试 |
| server tool 出错 | Claude 透明地处理,尝试给出替代回应;你不需要为 server tool 处理 is_error |
错误消息本身是一个 prompt。官方给了一条很实用的写作要求:写有指导性的错误消息——不要用 "failed" 这种泛化错误,而要说明出了什么问题、Claude 接下来该试什么,例如 "Rate limit exceeded. Retry after 60 seconds."。理由是这样 Claude 才有恢复或调整所需的上下文,不用瞎猜。把 str(exception) 直接塞回去往往就是那个泛化错误。
还有一个数字要记:请求无效或缺参数时,Claude 会带修正重试 2–3 次,然后向用户致歉。这解释了一个常见现象——日志里同一个工具连着三条参数错误,然后模型放弃:那不是你的重试逻辑在跑,是模型自己在试。
五、什么时候调工具:tool_choice、client / server、approval
TS 8.1 把工具使用模式点成三项:agentic harness dispatch、client-side vs. server-side tools、approval patterns。三项各对一个真实机制。
tool_choice:四个取值
auto——让 Claude 自行决定要不要调工具,提供 tools 时的默认值;any——必须用所提供工具中的一个,但不指定哪个;tool——强制总是使用某个特定工具;none——禁止使用任何工具,没有提供 tools 时的默认值。
两条副作用容易被忽略。其一,设为 any 或 tool 时,API 会 prefill assistant 消息来强制用工具,因此模型不会在 tool_use block 之前输出自然语言回应或解释,即使你明确要求它这么做。想要「既解释又必须调某个工具」,官方给的办法是保持 auto 并在 user 消息里明确要求。其二,改动 tool_choice 会让已缓存的 message block 失效——tool 定义与 system prompt 仍然缓存,但 message 内容必须重新处理。第三条约束在 ch06 讲过:manual extended thinking 下只支持 auto 与 none。
第四条是型号约束,而且越来越常见。Claude Opus 5.5、Sonnet 5.5、Fable 5.1 与 Mythos 5.1 无论 thinking 怎么设,收到 any 或 tool 都直接返回 400;在这些型号上要「保证调到工具且输入合 schema」,官方给的替代是 auto 加 strict tool use,要固定 JSON 形状时用 structured outputs。在仍接受强制调用的型号上(官方点名 Claude Opus 5 与 Claude Haiku 5.5),强制调用还有一个代价:它会跳过 thinking,响应直接以 tool_use 开头、没有 thinking block。所以看到 tool_choice 先查型号:同一份请求在一个型号上「只是没了开场白」,换到另一个型号就是 400。
client-side vs. server-side tools
判据只有一句:模型返回结构化调用后,client tool 由你的应用执行,server tool 由 Anthropic 执行。由此派生出两条工程后果:client tool 能触达你的内网与私有数据(因为代码跑在你这边),而 server tool 不需要你实现执行层、错误也由 Claude 透明处理。
agentic harness dispatch 与 approval patterns
「谁来跑这个循环」是 ch08 的题目:Client SDK 下 tool loop 自己写或交给 beta tool runner;Agent SDK 是在你运营的进程里跑 Claude Code 的库;Managed Agents 由 Anthropic 托管 agent harness,沙箱可以是 Anthropic 的云沙箱或自托管。approval patterns 落在同一个层级上——Agent SDK 的 permissions 控制哪些工具自动跑、哪些需要审批;Managed Agents 则用 toolset 的 configs 为单项工具设置 approval policy。本章只需记住一件事:审批发生在 harness 层,不在 tool 定义里——所以速查栏 1 里没有、也不该有一个 requires_approval 字段。
六、MCP:三种原语与它的通信模式
TS 8.2 点名了 MCP resources / tools / prompts 与通信模式 stdio、sockets。原语这一侧官方文档对得很整齐,通信模式那一侧需要一点措辞对照。
定位:MCP 是连接 AI 与外部工具、数据源的开源标准,MCP server 让 Claude Code 访问你的 tools、databases 与 APIs;它为一个外部系统提供专门打造的工具,而连接与认证由 server 处理——最后半句是它与「自己写一个 client tool」最实际的差别。
三种原语
| 原语 | 它在 Claude 应用里长什么样 |
|---|---|
| tools | server 暴露的可调用能力,与其他工具一起进入模型的工具面 |
| resources | 用 @ mention 引用,就像引用文件;输入 @ 会在自动补全里与文件一起出现,格式 @server:protocol://resource/path |
| prompts | 成为命令;输入 / 可见,菜单里列为 /servername:promptname (MCP),键入 /mcp__servername__promptname 也能运行 |
三者在协议层各有自己的发现端点:连接成功后跑的能力发现请求是 tools/list、prompts/list、resources/list;而且 server 可以在连接期间改它的 tools、prompts 或 resources 并发 list_changed 通知:交互式终端会话里 Claude Code 会重新拉取该 server 的列表,不需要断开重连;-p 非交互模式与 Agent SDK 里则只刷新 tool 列表。
通信模式:blueprint 说 stdio 与 sockets,官方文档怎么说
这一小节要如实对照,不能反过来编。blueprint 的原文清单是「stdio、sockets」。其中 stdio 与官方用词完全一致;而 sockets 这个词不是官方 transport 的名字——Anthropic 文档里列的远程 transport 是 http(MCP 规范里叫 streamable-http)、sse 与 ws(WebSocket)。下表按官方用词列,并注明它与 blueprint 措辞的关系:
| 官方 transport | 官方描述 | 与 blueprint 措辞的关系 |
|---|---|---|
stdio | 作为本地进程在你的机器上运行,适合需要直接系统访问或自定义脚本的工具 | 与 blueprint 的 stdio 逐字对应 |
http | 连接远程 MCP server 的推荐选项,云端服务最广泛支持的 transport;type 接受 streamable-http 作为别名,因为MCP 规范用的就是这个名字 | blueprint 未点名 |
sse | 已 deprecated,能用 HTTP 就改用 HTTP;仍只暴露 SSE 端点的服务,现在也用 claude mcp add --transport http 添加,Claude Code 先试 HTTP、server 不接受时自动切到 SSE(v2.1.265 起) | blueprint 未点名 |
ws | WebSocket,保持一条持久的双向连接,适合会主动向 Claude 推送事件的远程 server;只响应请求就该用 HTTP,因为 HTTP 支持 OAuth 与 --transport 标志而 WebSocket 两者都不支持 | 「socket」这一族里语义最接近的官方项,但官方从不用 sockets 这个词 |
type 的合法值是 http(别名 streamable-http)、sse、ws,以及「没有 type 就被读成 stdio」。本书不把 sockets 写成一个配置值,因为它不是。配置与部署:三个 scope
local 是默认:只在你添加它的那个项目里加载、仅对你可见,存在 ~/.claude.json 该项目路径下。project:配置存在项目根目录的 .mcp.json 里——把 .mcp.json 提交进版本控制,团队每个人就拿到同一套 MCP 工具与服务。user:存在 ~/.claude.json,跨项目可用但仍是你个人私有。「要让团队都有」= project scope + 提交 .mcp.json,这是本节最可能被直接考的一句。
速查栏 3 · MCP 配置
.mcp.json ✔ 真实 · 项目根目录,project scope,应提交
~/.claude.json ✔ 真实 · local 与 user scope 的存放处
"type": "stdio" ✔ 真实 · 本地进程(缺 type 即按此读)
"type": "http" ✔ 真实 · 远程首选
"type": "streamable-http" ✔ 真实 · http 的别名,MCP 规范用词
"type": "sse" ✔ 真实 · 已 deprecated
"type": "ws" ✔ 真实 · WebSocket,只能写在 JSON 配置里
"type": "sockets" ✘ 不存在 · 自制干扰项
"type": "grpc" ✘ 不存在 · 自制干扰项
.claude/mcp-servers.json ✘ 不存在 · 自制干扰项
七、样题 Q3 精讲:什么时候值得建一个 MCP server
官方样题只有三道,其中一道正好落在本章。题干形状:一个内部 REST 库存服务,要跨多个 Claude 应用复用,且要独立维护。正确项是建一个 MCP server 把库存操作暴露为 tools,官方理由两条:多个 Claude 应用可以共享("multiple Claude applications can share"),以及可以独立维护("maintained independently")。
三条排除理由比正确项更值得背,因为它们各自代表一整类干扰项:
| 排除项 | 官方理由 | 本书归类 |
|---|---|---|
| 把库存逻辑写进各应用的 system prompt | "is neither reusable nor maintainable" | D5 错层承载 / 违反 P4 |
| 每次请求把数据贴进 context | "gives no live access and wastes context" | D5(失守点在上下文开销时标 P9) |
| 指望 built-in tools 触达内部 API | "do not automatically reach arbitrary internal APIs" | D1 凭空捏造 / 违反 P5 |
把它反过来用,就得到本章的选型判据:三个问题里有一个答「要」,就不该住在某个应用的 prompt 里——要不要跨应用复用?要不要独立于任何单个应用维护?数据要不要是活的?这正是 ch01 P4「能力放对载体」的原文出处。而 MCP 相对于「自己写一个 client tool」的额外好处,官方也点了:连接与认证由 server 处理。
authServerMetadataUrl、server 的 health check 与 discovery cache、list_changed 失败时的降级细节、以及 Anthropic 提供的各个 server tool(web search、code execution 等)的逐项参数。前三类是运维粒度,最后一类属工具目录而非 blueprint 点名的能力。另外,MCP 协议本身的规范文本在 modelcontextprotocol.io,那不在本书的来源白名单内,所以本章所有 MCP 原句都取自 Anthropic 自家页面——这也意味着本章讲的是「Claude 应用怎么用 MCP」,不是「MCP 规范全文」。
name 正则/description/input_schema/input_examples、「极其详细的 description 是工具表现最重要的因素」与 3–4 句的下限、tool set 的合并/命名空间/高信号返回三条、Claude API 把工具集成进 user 与 assistant 消息结构而没有 tool role、tool_use 的 id/name/input 与 tool_result 的 tool_use_id/content/is_error、两条回传顺序硬要求、把不可信内容放进 tool_result、三类错误的处置与「写有指导性的错误消息」、模型自行重试 2–3 次、tool_choice 四取值与它对自然语言前言与缓存的副作用、client-side 与 server-side 的执行方分界、approval patterns 落在 harness 层)、TS 8.2(MCP 的定位与「连接与认证由 server 处理」、resources/tools/prompts 三原语在 Claude 应用里的形态与各自的 list 端点、list_changed、transport 的官方用词 stdio/http/streamable-http/sse/ws 及其与 blueprint「stdio、sockets」措辞的对应关系、local/project/user 三个 scope 与提交 .mcp.json 的团队分发、样题 Q3 的正确项与三条排除理由)。相邻考点:一次调用的解剖与 tools 往返见 ch02;plugin 打包与依赖见 ch04;agent loop、Agent SDK 与 hooks 见 ch08;tool description 与结构化输出的分工见 ch10;上下文预算与 tool output pruning 见 ch11;built-in/custom/Skills/MCP 四方取舍见 ch13;注入防护的完整层次见 ch14。本章题组
lookup_order whose full description reads Looks up an order. In production, Claude answers order-status questions from conversation history instead of calling it about 30% of the time, and when it does call it, it sometimes passes a customer email where an order ID is expected. The input_schema is correct and the tool works when called. What is the first change?description 写得更详细。Looks up an order. 正是文档里那个 poor description 的形状。any 或 tool 会让模型不再输出任何自然语言前言,并且改动 tool_choice 会让已缓存的 message block 失效。而且它一个字也没修「传错参数」。description 的官方职责就是说明工具做什么、何时使用、如何表现;写在 system prompt 里的版本不会随工具一起被复用,也不会随工具一起被维护。stop_reason: "tool_use", their code sends back a message with "role": "tool" containing the result string. That fails, so they change it to a user message shaped as [{"type":"text","text":"Here are the results:"}, {"type":"tool_result", ...}]. That fails too, with an error mentioning tool_use ids without tool_result blocks immediately after. What is wrong?user 与 assistant 的消息结构,不像那些使用 tool 或 function 特殊 role 的 API——user 消息装 tool_result。第二个正是报错原文对应的规则:在含 tool result 的 user 消息里,tool_result block 必须排在 content 数组最前面,任何 text 都要排在所有 tool result 之后。role 为 user 的新消息」来回传 tool_result;assistant 消息装的是 tool_use 而不是结果。tool_result 没有 name 字段。配对靠的是 tool_use_id——它对应 tool_use block 的 id,官方对 id 的说明就是「用来在稍后把结果对应回来」。tool_result 在前、text 在后。而且拆成两条会踩另一条硬规则——tool result 必须紧跟在 tool use 之后,中间不能夹任何消息。query_warehouse, which occasionally hits a 60-second per-tenant rate limit. The current handler catches the exception and returns {"type":"tool_result","tool_use_id":"...","content":"failed","is_error":true}. Traces show Claude then either gives up and apologizes, or immediately retries the identical call and fails again. The rate limit response includes a Retry-After: 60 header the handler discards. What should change?"failed" 这种泛化错误,而要说明出了什么问题、Claude 接下来该试什么,例句正是 "Rate limit exceeded. Retry after 60 seconds.";理由是这样 Claude 才有恢复或调整所需的上下文、不用瞎猜。题干里被丢掉的那个 Retry-After 头,正是这条消息该携带的信息。is_error 的语义就是「工具执行出错时设为 true」,抹掉它是在向下游撒谎,比报错危险得多。tool_result.error_code 与 tool_result.retry_after 都不存在。官方给的 tool_result 字段只有 tool_use_id、content、is_error——结构化恢复信息的正确载体就是 content 里那句话。本书自制的干扰项,无官方出处。claude-haiku-5-5 keeps a 50k-token cached prefix (tools plus knowledge base) and must always log a ticket via create_ticket before replying. An engineer sets tool_choice: {"type": "any"} on every request. Two regressions follow: the assistant's friendly opening line disappears from every reply, and cache_read_input_tokens drops on turns where the setting is toggled. What explains both?tool_choice 为 any 或 tool 时,API 会 prefill assistant 消息来强制用工具,因此模型不会在 tool_use block 之前输出自然语言回应或解释,即使你明确要求。其二:改动 tool_choice 会让已缓存的 message block 失效,tool 定义与 system prompt 仍缓存,但 message 内容必须重新处理。题干钉的是仍接受强制调用的 claude-haiku-5-5;同一份请求发给 Opus 5.5 或 Sonnet 5.5 不会出现这两个症状,而是直接 400。any 与 tool 同属「强制用工具」这一类,两者都会 prefill、都会压掉前言。换成 tool 不会恢复任何东西。.mcp.json entry for a remote inventory server: {"inventory": {"url": "https://mcp.acme.internal/mcp"}}. Claude Code refuses to start it and reports that the entry has a url but no type. A teammate suggests "type": "sockets" because the exam blueprint lists "stdio, sockets" as the MCP communication modes. What is correct?url 却没有 type 是配置错误,因为 Claude Code 会把没有 type 的条目读成 stdio server。该填什么:HTTP server 是连接远程 MCP server 的推荐选项,type 接受 streamable-http 作为别名,因为 MCP 规范用的就是这个名字。而 sockets 是 blueprint 的措辞,不是官方配置值——官方的远程 transport 是 http / sse / ws。"type": "sockets" 不存在。它之所以像真的,是因为 blueprint 里确实有 sockets 这个词——但那是考纲对通信模式的概括措辞,不是 JSON 里的合法取值。本书自制的干扰项,无官方出处。把考纲名词当配置值填,是本章最典型的一类错误。type 被读成 stdio」这半句是官方原话,但后半句是编的——stdio server 是在你机器上运行的本地进程,它不是「触达内网主机的唯一 transport」。题干那个 server 在内网 HTTPS 端点上,正是 HTTP transport 的场景。sse 确实是一个真实取值,但定性反了两处:官方说 SSE transport 已经 deprecated,能用 HTTP 就改用 HTTP;而 http 恰恰是远程 server 的推荐选项,不是本地开发专用。@server:protocol://resource/path——「想按引用拉进 prompt」正是这个形状。prompts:server 暴露的 prompts 成为可用命令,/ 菜单里列为 /servername:promptname (MCP),键入 /mcp__servername__promptname 也能运行——「按名字调用并填写」正是这个形状。@),按名字调起并填写的是 prompt(/)。tools/list、prompts/list、resources/list,而且 resources 与 prompts 在 Claude Code 里各有各的调起方式(@ 与 /)。全做成 tool 会让 roster 每次都占一次工具调用,也拿不到 @ 补全。claude mcp add --transport http deploy https://mcp.acme.internal/mcp and no other flags. What is the fix?.mcp.json,官方明说把 .mcp.json 提交进版本控制,团队每个人就拿到同一套 MCP 工具与服务。~/.claude.json 里;「让每个同事各跑一遍同样的命令」是把版本控制该做的事改成口头约定,新人 clone 仓库依旧什么都拿不到。~/.claude.json 是 local 与 user scope 的存放处,它混着这台机器上所有项目的配置与凭据;提交它既分享不了、也是一次真实的泄密风险。