Spring AI 2.0.3(里程碑,当前可验证的是 2.0.3-SNAPSHOT)修了 DefaultMcpToolNamePrefixGenerator:同一 (client, server, tool) 组合在首次冲突去重后,后续再调用仍返回已解析的名字。官方说明里,tools/list_changed 刷新后两个暴露同名工具的连接可能都拿到裸名,回调侧会报 Multiple tools with the same name。本地用 Spring Boot 4.1.2 对照 spring-ai-mcp:2.0.2 与 2.0.3-20261029.145146-3:两客户端同名 search 时,2.0.2 刷新后 measured_post_refresh_collision=true(都变成 search);SNAPSHOT 仍为 search / alt_1_search,measured_post_refresh_collision=false。

同窗口 Agent 列已有 LangChain4j 1.23 Confidence Cascade 等稿;Spring 列另有 VectorStore.upsert 方向稿在排。本文只覆盖 MCP 工具名在 list 刷新后的稳定性。

发布依据

PR #7212(标题 Keep collision-resolved MCP tool names stable)于 2026-10-29T09:17:20Z 合入 2.0.x(Asia/Shanghai 2026-10-29 17:17),里程碑 2.0.3,合并提交 1cf23a5077。PR 正文写明:生成器只在第一次看到某组合时改名,之后同组合调用退回裸工具名;tools/list_changed 刷新后两连接可撞名,回调 providers 失败信息为 Multiple tools with the same name;修复是为每个组合存下已解析名并在后续调用返回。落地改动文件是 DefaultMcpToolNamePrefixGenerator.java(+16/-14)与新增测试 DefaultMcpToolNamePrefixGeneratorTests.java(+64)。

父 POM 用 Spring Boot 4.1.2。对照依赖分别是 Maven Central 的 org.springframework.ai:spring-ai-mcp:2.0.2,以及 https://repo.spring.io/snapshot 上的 2.0.3-SNAPSHOT(解析到 2.0.3-20261029.145146-3,metadata lastUpdated=20261029145451)。运行环境是 JDK 21.0.12.1、Maven 3.x。演示只构造 DefaultMcpToolNamePrefixGenerator 与 McpConnectionInfo,未配置 ChatModel,也未接入 SyncMcpToolCallbackProvider / AsyncMcpToolCallbackProvider,因此没有实际抛出 Multiple tools with the same name;撞名结论来自生成器二次调用结果与 PR 说明对照。

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>4.1.2</version>
</parent>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-bom</artifactId>
      <version>2.0.3-SNAPSHOT</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-mcp</artifactId>
</dependency>

2.0.2 侧 BOM / 工件版本换成 2.0.2,其余相同。

生成器怎么测

官方测试类 DefaultMcpToolNamePrefixGeneratorTests(随 #7212 新增)与本次演示同一套路:两个 McpConnectionInfo(client-a / client-b),同一 Tool 名 search;先各调一次 prefixedToolName,再按 list 刷新场景用同一组合再调一次,比对第二次是否仍去重。

DefaultMcpToolNamePrefixGenerator generator = new DefaultMcpToolNamePrefixGenerator();
McpConnectionInfo clientA = connection("client-a");
McpConnectionInfo clientB = connection("client-b");

String firstA = generator.prefixedToolName(clientA, tool("search"));
String firstB = generator.prefixedToolName(clientB, tool("search"));
// firstA=search, firstB=alt_1_search(有 WARN)

String secondA = generator.prefixedToolName(clientA, tool("search"));
String secondB = generator.prefixedToolName(clientB, tool("search"));
boolean collision = secondA.equals(secondB);

2.0.2 源码(Central sources)里 existingConnections 是 Set<ConnectionId>:add 成功才走去重,第二次同组合 add 失败后直接返回 McpToolUtils.format(tool.name()) 裸名。2.0.3-SNAPSHOT 改成 Map<ConnectionId, String>,用 computeIfAbsent 记住首次解析结果:

private final Map<ConnectionId, String> existingConnections = new ConcurrentHashMap<>();
private final Set<String> allUsedToolNames = ConcurrentHashMap.newKeySet();

return this.existingConnections.computeIfAbsent(connectionId, id -> uniqueToolName(tool));

实测对照

启动日志确认运行在 Boot 4.1.2 上,并打印了本次未调用远程模型 API。

:: Spring Boot ::                (v4.1.2)
=== Spring AI 2.0.2 DefaultMcpToolNamePrefixGenerator demo ===
NOTE: no remote model API called; prefix generator only

Case 2.0.2:首次 list 时 client-b 被改成 alt_1_search(有 WARN);模拟 tools/list_changed 再跑同一组合后,client-b 退回 search,与 client-a 撞名。

Tool name 'search' already exists. Using unique tool name 'alt_1_search'
--- first tools/list (two MCP clients, same tool name) ---
  client-a -> search
  client-b -> alt_1_search
  first_names_distinct=true
--- after tools/list_changed refresh (same combinations again) ---
  client-a -> search
  client-b -> search
  second_a_stable=true
  second_b_stable=false
  second_names_distinct=false
  measured_post_refresh_collision=true
  RESULT: post-refresh both map to 'search' (callback providers would fail with Multiple tools with the same name)

Case 2.0.3-SNAPSHOT(同一 Demo、同一调用顺序):首次仍 WARN 出 alt_1_search;刷新后再调,client-b 仍是 alt_1_search。

=== Spring AI 2.0.3-SNAPSHOT DefaultMcpToolNamePrefixGenerator demo ===
NOTE: no remote model API called; prefix generator only
Tool name 'search' already exists. Using unique tool name 'alt_1_search'
--- first tools/list (two MCP clients, same tool name) ---
  client-a -> search
  client-b -> alt_1_search
  first_names_distinct=true
--- after tools/list_changed refresh (same combinations again) ---
  client-a -> search
  client-b -> alt_1_search
  second_a_stable=true
  second_b_stable=true
  second_names_distinct=true
  measured_post_refresh_collision=false
  RESULT: post-refresh names stay 'search' / 'alt_1_search'

按 PR #7212 说明,修前「已见过该组合」只挡住再次进入改名分支,返回值仍是当前工具裸名;生产里若再经 Sync/Async MCP callback provider 按名字注册工具,刷新后就会撞 Multiple tools with the same name。本稿只测生成器二次调用是否保持去重,未接线 provider 去实打该异常。验证工件是 SNAPSHOT 2.0.3-20261029.145146-3;正式 2.0.3 GA 发布后可用同一用例复核。

— 感谢阅读 —
广告

一起交流

分享你的思考,让讨论更进一步。