本节摘要:前三节讲了注册与能力声明的机制,本节补上写真实服务端绕不开的三类工程细节。一是命名约定——工具/资源/提示词的名字不能随便起,有长度、字符、大小写的规则,违反会导致客户端无法调用。二是错误处理——工具执行会失败,SDK 提供两条路径把错误回传模型:抛异常(自动转成错误响应)或主动返回错误内容。三是动态增删——运行期能力集可以变化,用
add_tool等方法增删原语,配合列表变更通知让客户端保持同步。读完本节,你能写出可维护、可调试、能动态调整的真实服务端。
工具、资源、提示词的名字不是任意字符串,有协议约束。违反规则的后果很直接——客户端构造不出合法调用,或服务端拒绝接受。
| 原语 | 命名规则 | 示例 |
|---|---|---|
| 工具名 | 长度与字符有限制,通常 ASCII、避免特殊字符 | add、search_items(好);加(1)、a&b(坏) |
| 资源 URI | 合法 URI,方案(scheme)自定义但需一致 | config://app、db://users/{id} |
| 提示词名 | 类工具名规则,简洁、ASCII、避免空格 | summarize、analyze_table(好) |
这些规则的存在,是为了让名字能稳定地跨客户端传递——不同宿主(Claude Desktop、Cursor、自研 Host)对名字的处理能力不同,统一约束名字的字符集,才能保证「一个服务端,所有客户端都能调」。
💡 技巧:工具名建议用
snake_case(下划线小写),与 Python 函数名一致,既符合规则又好读。资源 URI 建议用一个稳定的自定义方案(如db://、file://、api://),体现资源来源。提示词名建议动词开头(summarize、analyze、generate),让用户一眼看出「这个提示词会干什么」。
工具执行会失败——数据库连不上、参数越界、外部 API 超时。SDK 提供两条把错误告知模型的路径:
最简单的方式是直接抛异常,SDK 会捕获它,转成 isError: true 的错误响应回传模型:
@mcp.tool() def divide(a: int, b: int) -> float: """Divide a by b.""" if b == 0: raise ValueError("除数不能为零") # 抛异常 return a / b
模型收到的响应里会带 isError: true 和错误文本。模型能读懂这个错误,并据此决定下一步——比如「除数为零了,我换一个值重试」或「向用户说明这个错误」。
有时你想更精细地控制错误的结构,可以主动返回带错误标记的内容:
@mcp.tool() def divide(a: int, b: int) -> float: if b == 0: return "错误:除数不能为零,请提供一个非零的 b" # 主动返回错误文本 return a / b
两种路径的差别:
| 维度 | 抛异常 | 主动返回错误内容 |
|---|---|---|
| 模型看到的标记 | isError: true(明确是错误) |
看到文本,需自己判断 |
| 适合 | 真正的失败(必须重试或换策略) | 「软失败」(可恢复、要解释) |
| SDK 自动转换 | 是(异常 → 错误响应) | 否(你控制返回内容) |
⚠️ 注意:让模型看到错误,是工具调用循环的关键能力。不要吞掉异常——如果你
try/except把错误吃掉、返回一个空结果,模型会以为成功了,继续基于错误结果推理,导致整个对话跑偏。该抛就抛,让模型知道「这一步失败了」。
有时候你需要运行期动态调整能力——比如根据登录用户启用不同工具、根据配置加载不同资源。SDK 提供动态增删方法:
# 动态添加一个工具 mcp.add_tool(new_search_tool, name="search_v2") # 动态添加一个资源 mcp.add_resource(new_resource) # 动态添加一个提示词 mcp.add_prompt(new_prompt)
增删后,SDK 会自动发送列表变更通知(list_changed),客户端收到后重新拉清单,保持同步。这套机制对应第 3.3 节讲的 list_changed: True 标志。
async def setup_for_user(user): if user.is_admin: mcp.add_tool(admin_delete_tool) # 管理员才有删除工具 else: mcp.remove_tool("admin_delete") # 普通用户移除 # SDK 自动通知客户端,工具列表已更新
这种「能力随上下文变化」的模式,让你能实现细粒度的权限控制——不是「调用了再拒绝」,而是「根本不暴露不该有的能力」。
回顾第 3.3 节的一个关键区分,在动态场景里尤其重要:
| 操作 | 影响什么 | 发什么通知 |
|---|---|---|
add_tool / remove_tool |
工具列表(tools/list 的内容) | tools/list_changed |
add_resource / 移除 |
资源列表 | resources/list_changed |
| (无对应操作) | 能力族(tools/resources/prompts) | (能力族连接期内稳定) |
也就是说:动态增删改变的是「某个能力族下有哪些具体项」,不改变「有没有这个能力族」。如果你一个工具都没注册,tools 能力族根本不声明;一旦注册过(哪怕后来移除),tools 能力族就声明了,只是列表可能为空。
💡 技巧:这个区分在排查「为什么客户端看不到我的动态工具」时很关键。先确认能力族声明了(
server_capabilities里有tools),再确认列表里有(动态加的进了tools/list)。两步排查能定位绝大多数「能力不出现」的问题。
错误处理与动态增删,在实战里经常协作。一个典型场景:工具执行失败 N 次后自动禁用:
FAILURE_COUNT = {} @mcp.tool() def flaky_api_call(query: str) -> str: try: return call_external_api(query) except ExternalAPIError as e: FAILURE_COUNT["flaky_api_call"] = FAILURE_COUNT.get("flaky_api_call", 0) + 1 if FAILURE_COUNT["flaky_api_call"] > 5: mcp.remove_tool("flaky_api_call") # 失败太多,动态移除 raise # 同时抛错让模型知道
这种模式让服务端能自我保护——不稳定的能力自动下线,避免拖累整个对话。当然,这种逻辑要谨慎用,本节只展示可能性,实战需根据场景设计。
把本节的工程细节压成一张速查表,供后续查阅:
| 需求 | 怎么做 |
|---|---|
| 工具名符合规则 | snake_case,ASCII,避免特殊字符 |
| 资源 URI 稳定 | 用一致的自定义方案(db://、file://) |
| 提示词名清晰 | 动词开头,体现「会干什么」 |
| 工具失败告知模型 | 抛异常(自动转 isError)或主动返回错误文本 |
| 不要做的事 | 吞掉异常(模型会误以为成功) |
| 运行期加工具 | mcp.add_tool(tool, name=...) |
| 运行期移除 | mcp.remove_tool(name) |
| 客户端看到动态变化 | SDK 自动发 list_changed 通知 |
| 排查「能力不出现」 | 先看能力族(server_capabilities),再看列表 |
snake_case ASCII,资源 URI 用一致方案,提示词名动词开头。isError)或主动返回错误内容。add_tool / remove_tool 等,配合列表变更通知让客户端同步。server_capabilities 的能力族,再看具体列表。第 3 章结束。你已经掌握了服务端入门的全部:容器、装饰器、能力声明、工程细节。第 4 章我们深入工具——全书最硬核的「类型即契约」。