3.3 能力声明:注册即声明,客户端只问声明过的


3.3 能力声明:注册即声明,客户端只问声明过的

本节摘要:本节讲的是连接服务端与客户端的那座桥——能力声明(Capability Declaration)。第 1.3 节你打印过 server_capabilities 字典,看到过 tools / resources / prompts 这几个键。但这个字典是怎么来的?答案是:它不是你写的,是 MCPServer 从你注册的处理器自动推断的。注册一个工具,tools 能力就声明;注册一个资源,resources 能力就声明;什么都没注册,对应能力就不声明。这条「注册即声明」的规则贯穿整个 SDK,本节把它讲透,并说明它为什么是协议设计的精妙之处。

一、能力声明是什么

能力声明(Capability Declaration),是服务端在客户端连接时,告诉客户端「我愿意回应哪些族的请求」的一份清单。

客户端连接服务端 │ ▼ 服务端发送能力声明: "我支持 tools、resources、prompts" "我不支持 completions(因为没注册补全处理器)" │ ▼ 客户端据此决定: 该问 tools/list、tools/call(因为声明了 tools) 不该问 completions/complete(因为没声明 completions)

这是客户端与服务端「互相了解」的第一步——客户端知道该问什么、不该问什么,避免发出服务端无法回应的请求。

二、注册即声明:规则全貌

MCPServer 的能力声明遵循一条简单而严格的规则:

你注册了什么处理器,就声明什么能力;什么都没注册,对应能力就不声明。

具体对应关系:

你做的事 服务端声明的能力 客户端据此可调用
注册任意 @mcp.tool() tools tools/listtools/call
注册任意 @mcp.resource() resources resources/listresources/read
注册任意 @mcp.prompt() prompts prompts/listprompts/get
注册一个补全处理器 completions completions/complete
注册订阅相关 resources.subscribe 资源订阅(第 10 章)

这条规则是自动的——你不用写「我要声明 tools 能力」这种代码,MCPServer 替你判断。

三、用 client.server_capabilities 验证

第 1.3 节我们用内存客户端看过这个机制:

async with Client(mcp) as client: print(client.server_capabilities.model_dump(exclude_none=True))

对第 1.2 节的服务端(注册了一个工具、一个资源模板),输出是:

{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}}

逐项解读:

输出里的键 含义
tools 声明了 tools 能力(因为注册了 add 工具)
resources 声明了 resources 能力(因为注册了 greeting 资源模板)
prompts 声明了 prompts 能力(MCPServer 默认会准备空的提示词管理器)
list_changed: True 「列表可能变化」标志,意味着可发列表变更通知
subscribe: True 资源可订阅(第 10 章)

注意没有 completions——因为我们没注册补全处理器。这就是「不注册就不声明」的体现。

四、为什么是「声明驱动」而不是「试了再说」

你可能会问:为什么客户端要先看能力声明,而不是「直接试,失败了再说」?答案在协议效率与清晰度:

做法 A:声明驱动(SDK 用的) 客户端看声明 → 知道该问什么 → 只发有效的请求 → 不浪费往返、不会发无效请求 做法 B:试了再说(假设的替代) 客户端盲目发请求 → 服务端回「我不支持」→ 客户端重试 → 浪费往返、错误处理复杂、行为不可预测

声明驱动还有一个深层好处:它让能力成为「契约」。服务端声明了 tools,就承诺会回应 tools/listtools/call;客户端看到声明,就能放心地集成,不用担心「这个请求会不会突然失败」。这种契约性,是协议能在跨厂商环境下稳定工作的基础。

💡 技巧:把能力声明理解成 Web API 的「能力发现端点」(类似 OpenAPI 的能力描述)。客户端先看描述,再决定调用——这与「先读 OpenAPI 文档再写客户端代码」是同一个模式。

五、运行期动态变化与列表变更通知

能力声明里那个 list_changed: True 标志,指向一个进阶能力:运行期增删原语时,通知客户端列表变了

运行期: mcp.add_tool(new_tool) # 动态加一个工具 mcp.remove_tool(old_tool) # 动态移除 │ ▼ 服务端发 notifications/tools/list_changed │ ▼ 客户端收到通知,重新拉 tools/list

这个机制让你能在运行期动态调整能力集——比如根据用户权限启用/禁用某些工具。客户端收到 list_changed 通知后,会重新拉清单,保持与服务端同步。

⚠️ 注意:list_changed 只表示「列表变了」(加了/减了哪个工具),不改变「能力族」(始终是 tools)。换句话说,你动态加一个工具,tools 能力族不会变(本来就有),变的是工具清单——所以发的是列表变更通知,不是能力重新声明。能力族(tools/resources/prompts/completions)在连接期内是稳定的。

六、这条规则的实践影响

理解「注册即声明」,对实战有几个直接影响:

场景 你该做什么
工具/资源/提示词不出现 检查是否真的注册了(没注册就不声明,客户端看不到)
补全不工作 确认注册了补全处理器(否则 completions 能力不声明)
客户端报「方法不存在」 多半是服务端没声明对应能力,客户端却在问
动态启用能力 add_tool 等(影响列表,不影响能力族)

最常见的坑是第三条——客户端报「方法不存在」,根因往往是服务端没注册对应处理器,所以没声明能力,客户端的请求被拒绝。排查时第一步永远是 print(client.server_capabilities) 看声明了什么。

本节要点回顾

  1. 能力声明是服务端告诉客户端「我愿意回应哪些族请求」的清单,连接时发送。
  2. 规则:注册即声明,不注册就不声明,完全自动,不用手写。
  3. 注册工具声明 tools、注册资源声明 resources、注册提示词声明 prompts,注册补全才声明 completions。
  4. server_capabilities 字典是声明的客户端可见形态,可用它排查「为什么客户端看不到某能力」。
  5. 声明驱动优于「试了再说」:省往返、行为可预测、能力成为契约。
  6. list_changed 表示列表可变(运行期增删),不改变能力族;能力族在连接期内稳定。
  7. 客户端报「方法不存在」多半是服务端没声明能力,排查第一步看 server_capabilities

注册与声明清楚了,下一节讲工程细节:命名约定、错误处理、动态增删。


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