MCP 客户端检查 Server 能力时,应以初始化响应中的 `ServerCapabilities` 为唯一会话依据:先判断 Tools、Resources、Prompts 顶层能力是否存在,再判断 `listChanged`、`subscribe` 等子能力。顶层存在只表示 Server 提供这一类功能,不代表它支持该能力下所有可选行为。
MCP 连接首先执行 initialize。客户端发送自己支持的协议版本、ClientCapabilities 和实现信息;Server 返回协商后的协议版本、ServerCapabilities 与实现信息。客户端确认版本可接受后发送 initialized 通知,连接才进入正常操作阶段。
因此,不能在创建 transport 后、initialize 尚未完成时凭配置猜测 Server 能力。C# SDK 的 `McpClient.CreateAsync` 完成初始化后,`client.ServerCapabilities` 才代表这条连接实际协商得到的能力。后续请求应始终以该对象和协商版本为准。
await using var client = await McpClient.CreateAsync(transport);
var capabilities = client.ServerCapabilities;
var version = client.NegotiatedProtocolVersion;
当 `ServerCapabilities.Tools` 非空时,Server 声明自己暴露可调用工具。客户端此时可以调用 `ListToolsAsync` 获取具体目录。能力对象不是工具清单,不能仅看到 Tools 非空就假定某个名称一定存在;具体工具仍要从 list 结果中查找。
if (client.ServerCapabilities.Tools is not null)
{
var tools = await client.ListToolsAsync(cancellationToken: ct);
var selected = tools.FirstOrDefault(t => t.Name == requestedName);
if (selected is not null)
{
// 根据 schema 构造参数,再执行调用
}
}
若 Tools 为空,UI 应隐藏或禁用工具入口,编排器应选择没有工具的路径,而不是先发送 `tools/list` 再把 Method not found 当作普通空列表。能力协商的价值正是让双方在发送可选请求前避免无效尝试。
`Tools` 存在并不自动表示工具目录会推送变化。只有 `Tools.ListChanged` 为 true,客户端才应依赖 `notifications/tools/list_changed`。收到通知后重新执行 list,用新快照替换缓存。
if (client.ServerCapabilities.Tools is { ListChanged: true })
{
client.RegisterNotificationHandler(
NotificationMethods.ToolListChangedNotification,
async (_, ct) =>
{
var latest = await client.ListToolsAsync(cancellationToken: ct);
ReplaceToolCache(latest);
});
}
不支持 ListChanged 时,客户端仍可在连接时列举工具,也可以通过手动刷新或自己的轮询策略更新,但不能期待 Server 主动通知。若工具列表被产品设计视为静态,缓存生命周期通常与连接一致;若业务必须感知变化,应明确提供刷新入口。
`ServerCapabilities.Resources` 非空表示 Server 提供资源能力,客户端可列举资源和资源模板,并读取可访问内容。`Resources.Subscribe` 为 true 才表示可以订阅某个资源 URI 的内容变化;`Resources.ListChanged` 为 true 则表示资源目录本身可能变化并会发送列表变化通知。
if (client.ServerCapabilities.Resources is not null)
{
var resources = await client.ListResourcesAsync(cancellationToken: ct);
}
if (client.ServerCapabilities.Resources is { Subscribe: true })
{
await client.SubscribeToResourceAsync(
"config://app/settings",
cancellationToken: ct);
}
这三个层次不能混淆。资源内容更新通知对应已订阅 URI;资源列表变化通知表示新增、删除或重命名了目录项。Server 可以支持资源读取但不支持 Subscribe,也可以支持列表变化却不允许订阅单个资源。
`ServerCapabilities.Prompts` 非空时,客户端可以列举模板,并按模板名称和参数获取生成结果。只有 `Prompts.ListChanged` 为 true,客户端才注册并依赖 prompt 列表变化通知。
if (client.ServerCapabilities.Prompts is not null)
{
var prompts = await client.ListPromptsAsync(cancellationToken: ct);
}
if (client.ServerCapabilities.Prompts is { ListChanged: true })
{
client.RegisterNotificationHandler(
NotificationMethods.PromptListChangedNotification,
async (_, ct) =>
{
var latest = await client.ListPromptsAsync(cancellationToken: ct);
ReplacePromptCache(latest);
});
}
通知处理器应尽早注册,避免初始化后首次 list 与处理器安装之间出现静默窗口。实现还要对短时间内多次通知做合并,防止每个变化都触发一次并发 list。
能力检查解决“这类方法能不能调用”,目录检查解决“这个具体名称是否存在”。客户端调用工具、读取资源或获取 prompt 前,都应从当前目录找到目标并验证输入,而不是根据历史会话或硬编码名称直接发送请求。
即使目录中存在目标,Server 仍可能因当前身份、参数、资源状态或并发变化拒绝操作。能力不是授权凭证,也不是成功保证。客户端要处理标准错误、超时和取消,并将用户可修复的问题与连接故障区分开。
一种常见反模式是无条件调用 Tools、Resources 和 Prompts,然后根据错误决定是否显示功能。这会制造无意义日志,增加延迟,并可能触发旧 Server 的兼容性问题。正确流程是先读协商结果,只调用已声明能力。
异常仍然必须处理,因为 capability 可能被错误实现,网络也可能中断。但异常属于运行时失败路径,而不是正常的功能发现机制。对于未声明能力,客户端应直接降级;对于已声明却返回 Method not found 的 Server,应记录协议实现不一致。
能力协商是双向的。客户端在 `McpClientOptions.Capabilities` 中声明 Roots、Sampling、Elicitation 等自己真正能够处理的特性。不要为了“兼容更多”而声明没有实现处理器的能力,否则 Server 可能合法发起客户端无法完成的请求。
var options = new McpClientOptions
{
Capabilities = new ClientCapabilities
{
Roots = new RootsCapability { ListChanged = true },
Sampling = new SamplingCapability(),
Elicitation = new ElicitationCapability
{
Form = new FormElicitationCapability(),
Url = new UrlElicitationCapability()
}
}
};
声明 Roots.ListChanged 意味着客户端不仅能给出 roots,还会在列表变化时通知 Server。声明 Sampling 意味着能处理 Server 发起的采样请求。每一项声明都应与实际处理器、权限提示和用户体验相匹配。
初始化除了交换能力,还协商协议版本。Server 若不支持客户端请求版本,会返回自己支持的版本;客户端若不支持该响应版本,应断开而不是继续发送看似相同的方法。使用 HTTP transport 时,后续请求还要携带协商后的协议版本头。
同名能力在不同规范版本中可能有字段和语义差异。库通常负责序列化,但应用层不能绕过协商版本,用最新文档的假设解释旧连接。诊断日志应同时记录客户端版本、Server 版本、协商协议版本和 capabilities 快照。
大型客户端不要让每个页面和服务层各自读取 capability。连接完成后可生成只读 FeatureMatrix,明确 `CanListTools`、`CanWatchTools`、`CanReadResources`、`CanSubscribeResources`、`CanWatchResourceList`、`CanUsePrompts` 和 `CanWatchPromptList`。
var features = new FeatureMatrix(
CanListTools: caps.Tools is not null,
CanWatchTools: caps.Tools?.ListChanged == true,
CanReadResources: caps.Resources is not null,
CanSubscribeResources: caps.Resources?.Subscribe == true,
CanWatchResourceList: caps.Resources?.ListChanged == true,
CanUsePrompts: caps.Prompts is not null,
CanWatchPromptList: caps.Prompts?.ListChanged == true);
UI、缓存管理和编排器共用同一矩阵,可以避免一个页面把 Resources 非空理解成支持订阅,另一个页面又作不同判断。连接重建后重新生成矩阵,并销毁旧连接关联的缓存和通知处理器。
初次 list 结果可以缓存,但 `listChanged` 是缓存失效信号。收到事件后不要试图从通知内容推断差异,应重新拉取权威列表。刷新过程中可继续展示旧数据并标记 updating;刷新成功后原子替换,失败则保留旧快照并标记 stale。
通知可能重复、合并或在断线期间丢失,所以重新连接后应重新 list,而不是假定旧缓存仍有效。资源内容订阅也要在连接恢复后根据新的 capability 和业务状态重新建立。
至少准备以下组合:三类顶层能力全部为空;仅 Tools 存在但 ListChanged 为 false;Resources 存在但 Subscribe 为 false;Resources 同时支持 Subscribe 与 ListChanged;Prompts 存在且支持 ListChanged;Server 声明能力但方法返回错误。
测试应断言客户端不会发送未声明能力对应的请求,UI 不展示不可用操作,通知处理器只在子能力为 true 时注册。还要覆盖连接重建、版本不兼容、通知风暴、刷新失败和取消令牌,确保缓存不会跨连接误用。
MCP 客户端检查 Server 能力应遵循两级判断:Tools、Resources、Prompts 顶层对象决定能否使用该功能族,`listChanged` 和 `subscribe` 决定能否使用对应可选行为。具体工具、资源与 prompt 是否存在,则由各自 list 结果决定。
在 C# SDK 中,使用 `client.ServerCapabilities` 的空值检查与属性模式即可清晰表达这些条件。把协商结果集中转换为功能矩阵,配合版本记录、权威目录缓存和通知驱动的失效刷新,能让客户端既避免无效请求,也能在 Server 动态变化时保持一致。