MCP Java SDK 2.2.0-M2 把 modern 服务端(协议修订 2026-07-28)里的工具级 requiredCapabilities 落到可下载的里程碑:客户端在 _meta 里没声明对应能力时,服务端在 handler 之前直接返回 JSON-RPC 错误码 -32021(Missing required client capability),本地对照调用里 confirm_handler_ran=false。同版本还带了 McpTransportContext.putAll:后写入的 map 键覆盖用户 extractor 的同名键。本地用 Spring Boot 4.0.5 + io.modelcontextprotocol.sdk:mcp:2.2.0-M2 起了 Servlet modern 服务并用 JDK HttpClient 打 tools/call;代码已编译验证,未实际调用外部模型 API。
Git 注解标签 v2.2.0-M2 的 tagger 时间是 2026-11-03 01:26:39(Asia/Shanghai);仓库里目前没有同名的 GitHub Release 页面,以标签与 Maven Central 为准。Central 上 mcp-2.2.0-M2.pom 的 Last-Modified 是 2026-11-03 01:41:22(Asia/Shanghai)。CHANGELOG「Unreleased (2.2.0)」写明:modern 包预览无状态服务端、支持 required client capabilities,以及 McpTransportContext.putAll(Map)。相关 PR:#1283 Enforce required client capabilities(合并 2026-10-27 19:30:55 Asia/Shanghai)、#1282 find 返回 handler(同日)、#1288 putAll(2026-10-31 23:12:36 Asia/Shanghai)、#1286 SEP-2243 Mcp-Param(2026-11-02 16:45:04 Asia/Shanghai)。官方文档 server-modern.md 同步在该标签上。
MCP Java SDK 2.1.0 工具过滤那篇讲的是经典协议线上的 addToolFilter;本文只核 modern 线上的能力门禁与 putAll 优先级,不复述 2.1 会话清扫。Spring AI 侧 #7284 已于 2026-11-03 05:23:26(Asia/Shanghai)合入 main,为 2026-07-28 modern 协议加服务端支持,但本稿截稿时仍走未发布快照线,下面动手用的是裸 MCP SDK + spring-boot-starter-web。
工具怎么声明 requiredCapabilities
McpSyncTool.of(tool, ClientCapabilities, handler) 的三参数重载会把能力要求绑在工具上。缺能力时 RequiredCapabilities 拦截,handler 不会执行。本例给 confirm_delete 要求 elicitation.form;无 ping 不要求能力。
McpSyncTool confirmDelete = McpSyncTool.of(
Tool.builder("confirm_delete", Map.of(
"type", "object",
"properties", Map.of("id", Map.of("type", "string")),
"required", List.of("id")))
.description("needs elicitation form")
.build(),
ClientCapabilities.builder()
.elicitation(new ClientCapabilities.Elicitation(
new ClientCapabilities.Elicitation.Form(), null))
.build(),
(ctx, request) -> {
CONFIRM_HANDLER_RAN.set(true);
String id = String.valueOf(request.arguments().get("id"));
return McpSyncResponse.result(InputRequiredResult.builder()
.elicit("confirm", ElicitFormRequest.builder(
"Delete task " + id + "?",
Map.of(
"type", "object",
"properties", Map.of("ok", Map.of("type", "boolean")),
"required", List.of("ok")))
.build())
.requestState(id)
.build());
});
客户端能力写在每次请求 params._meta 的键 io.modelcontextprotocol/clientCapabilities;协议版本键是 io.modelcontextprotocol/protocolVersion,值 2026-07-28。Servlet 传输还要求 HTTP 头 Mcp-Method、MCP-Protocol-Version,以及 tools/call 时的 Mcp-Name。
本地验证
环境:JDK 21.0.12.1、Maven 3.9.9、Spring Boot 4.0.5、mcp 2.2.0-M2。进程监听 18089,注册 HttpServletMcpTransport 到 /mcp,ApplicationRunner 用 JDK HttpClient 连本机。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.5</version>
</parent>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>2.2.0-M2</version>
</dependency>
mvn -q -DskipTests package
mvn -q spring-boot:run
启动后的关键输出如下(原文摘录):
=== putAll precedence (in-process) ===
user.role=guest merged.role=transport-admin merged.tenant=acme merged.region=cn
=== server/discover ===
HTTP 200
{
"jsonrpc" : "2.0",
"id" : 1,
"result" : {
"supportedVersions" : [ "2026-07-28" ],
"capabilities" : {
"tools" : {
"listChanged" : false
}
},
...
}
}
=== tools/call ping (no required caps) ===
HTTP 200
{
"result" : {
"content" : [ { "type" : "text", "text" : "pong:guest" } ],
"resultType" : "complete"
}
}
=== tools/call confirm_delete WITHOUT elicitation ===
HTTP 400
{
"error" : {
"code" : -32021,
"message" : "Missing required client capability",
"data" : {
"requiredCapabilities" : {
"elicitation" : { "form" : { } }
}
}
}
}
confirm_handler_ran=false
=== tools/call confirm_delete WITH elicitation.form ===
HTTP 200
{
"result" : {
"inputRequests" : {
"confirm" : {
"method" : "elicitation/create",
"params" : {
"message" : "Delete task t-42?",
"mode" : "form"
}
}
},
"resultType" : "input_required"
}
}
confirm_handler_ran=true
-32021 拦在 handler 前
同一进程里:空 clientCapabilities 调 confirm_delete 得到 HTTP 400、error.code=-32021,且 confirm_handler_ran=false;补上 "elicitation":{"form":{}} 后 handler 才跑,返回 resultType=input_required 的 elicitation 表单。不要求能力的 ping 在空能力声明下仍返回 pong:guest(角色来自 X-Role → transport context)。
{
"params": {
"name": "confirm_delete",
"arguments": { "id": "t-42" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {
"elicitation": { "form": {} }
}
}
}
}
putAll:后写入的键覆盖
in-process 断言:用户 context 是 role=guest, tenant=acme;putAll({role=transport-admin, region=cn}) 之后 merged.role=transport-admin,保留 tenant=acme,并带上 region=cn。CHANGELOG 写明后写入条目优先,便于传输层在用户 extractor 之上叠自己的键。
McpTransportContext user =
McpTransportContext.create(Map.of("role", "guest", "tenant", "acme"));
McpTransportContext merged =
user.putAll(Map.of("role", "transport-admin", "region", "cn"));
// user.role=guest merged.role=transport-admin
// merged.tenant=acme merged.region=cn
以下是对官方说明的归纳,与里程碑原文区分开:写需要用户确认或 sampling 的工具时,应在服务端用 McpSyncTool.of(..., requiredCapabilities, handler) 声明门槛,让不支持该能力的客户端在进业务前就被拒;传输层若要覆盖 extractor 里的同名键,用 putAll,后写覆盖先写。M2 仍是里程碑,API 在 3.0 前可能微调;Spring AI 对 modern 的 starter 封装需等其发布线,当前可先用 SDK 直接挂 Servlet。
一起交流
分享你的思考,让讨论更进一步。