3.4 命名约定、错误处理与动态增删


3.4 命名约定、错误处理与动态增删

本节摘要:前三节讲了注册与能力声明的机制,本节补上写真实服务端绕不开的三类工程细节。一是命名约定——工具/资源/提示词的名字不能随便起,有长度、字符、大小写的规则,违反会导致客户端无法调用。二是错误处理——工具执行会失败,SDK 提供两条路径把错误回传模型:抛异常(自动转成错误响应)或主动返回错误内容。三是动态增删——运行期能力集可以变化,用 add_tool 等方法增删原语,配合列表变更通知让客户端保持同步。读完本节,你能写出可维护、可调试、能动态调整的真实服务端。

一、命名约定:别让名字害了调用

工具、资源、提示词的名字不是任意字符串,有协议约束。违反规则的后果很直接——客户端构造不出合法调用,或服务端拒绝接受

原语 命名规则 示例
工具名 长度与字符有限制,通常 ASCII、避免特殊字符 addsearch_items(好);加(1)a&b(坏)
资源 URI 合法 URI,方案(scheme)自定义但需一致 config://appdb://users/{id}
提示词名 类工具名规则,简洁、ASCII、避免空格 summarizeanalyze_table(好)

这些规则的存在,是为了让名字能稳定地跨客户端传递——不同宿主(Claude Desktop、Cursor、自研 Host)对名字的处理能力不同,统一约束名字的字符集,才能保证「一个服务端,所有客户端都能调」。

💡 技巧:工具名建议用 snake_case(下划线小写),与 Python 函数名一致,既符合规则又好读。资源 URI 建议用一个稳定的自定义方案(如 db://file://api://),体现资源来源。提示词名建议动词开头(summarizeanalyzegenerate),让用户一眼看出「这个提示词会干什么」。

二、错误处理:两条回传路径

工具执行会失败——数据库连不上、参数越界、外部 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 自动通知客户端,工具列表已更新

这种「能力随上下文变化」的模式,让你能实现细粒度的权限控制——不是「调用了再拒绝」,而是「根本不暴露不该有的能力」。

四、动态增删 vs 能力族:再强调一次区分

回顾第 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),再看列表

本节要点回顾

  1. 命名有规则:工具名建议 snake_case ASCII,资源 URI 用一致方案,提示词名动词开头。
  2. 错误回传两条路径:抛异常(自动转 isError)或主动返回错误内容。
  3. 抛异常是最简单的方式,SDK 替你转成模型能读懂的错误响应。
  4. 不要吞异常,否则模型会基于错误结果继续推理,导致对话跑偏。
  5. 动态增删用 add_tool / remove_tool,配合列表变更通知让客户端同步。
  6. 动态增删改变列表,不改变能力族,能力族连接期内稳定。
  7. 排查「能力不出现」两步走:先看 server_capabilities 的能力族,再看具体列表。

第 3 章结束。你已经掌握了服务端入门的全部:容器、装饰器、能力声明、工程细节。第 4 章我们深入工具——全书最硬核的「类型即契约」。


作者与出处
原作者: 灏天文库
来源:modelcontextprotocol
许可证:MIT
整理: 灏天文库整理
由灏天文库结构化整理,提供目录导航、全文检索与在线阅读,便于系统化学习
发布者: 作者: 灏天文库 转发
评论区 (0)
U