本节摘要:本节讲的是连接服务端与客户端的那座桥——能力声明(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/list、tools/call |
注册任意 @mcp.resource() |
resources |
resources/list、resources/read 等 |
注册任意 @mcp.prompt() |
prompts |
prompts/list、prompts/get |
| 注册一个补全处理器 | completions |
completions/complete |
| 注册订阅相关 | resources.subscribe |
资源订阅(第 10 章) |
这条规则是自动的——你不用写「我要声明 tools 能力」这种代码,MCPServer 替你判断。
第 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/list 与 tools/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) 看声明了什么。
server_capabilities 字典是声明的客户端可见形态,可用它排查「为什么客户端看不到某能力」。list_changed 表示列表可变(运行期增删),不改变能力族;能力族在连接期内稳定。server_capabilities。注册与声明清楚了,下一节讲工程细节:命名约定、错误处理、动态增删。