tools/list为空、Contributor 日志{code-review=0}、/mcp/status有记录但 Clienttools=0------多半不是协议没学,而是注解和注册路径没对齐。本篇只管一件事:Server 怎么注册工具、怎么进平台。
系列导航
| 篇目 | 主题 | 核心问题 |
|---|---|---|
| (一)架构+Client | Dream-MCP架构 + SSE/Streamable HTTP + 三层观测 | Java后端怎么接MCP才能放心上线? |
| (二)Server端 ⬅️ 本篇 | 两种注册路径 + SPI平台聚合 + 踩坑 | @Tool写了为什么tools/list为空? |
| (三)串联 | MCP+Graph+RAG完整Agent | 工具调完怎么编排决策? |
版本:Spring AI 1.1.6 · Boot 3.5.5 · JDK 21 · MCP SDK 0.18.2
先记三个模块
| 模块 | 角色 | 端口 |
|---|---|---|
code-review-agent |
MCP Server ,暴露 review_code、get_review_history |
8095 |
dream-ai-app |
MCP Client + mcpChatClient,/mcp/ask 入口 |
8090 |
dream-ai-test |
练手模块,验 hello 工具 |
7090 |
两个注解,两条路
Spring AI MCP Server 注册工具有两条路径,二选一:
| 方式 A:Scanner | 方式 B:Contributor(生产线) | |
|---|---|---|
| 注解 | @McpTool / @McpToolParam |
@Tool / @ToolParam |
| 包名 | org.springaicommunity.mcp.annotation |
org.springframework.ai.tool.annotation |
| 注册方式 | annotation-scanner 自动扫 Bean |
McpToolContributor → Aggregator 聚合 |
| 额外类 | 无 | 需写 Contributor |
| 平台开关 | 不需要 dream-saas.mcp.server |
必须开 |
| 依赖 | spring-ai-starter-mcp-server-webmvc |
+ dream-saas-mcp-server 等平台 jar |
关键约束 :annotation-scanner.enabled=true 时只扫 @McpTool 。方法写了 @Tool 却开 scanner,列表一定空。同名 Tool 双开会报 already registered。
1.1.6 坑 :旧文里的
org.springframework.ai.mcp.server.annotation.*不存在 ;换小版本先mvn dependency:tree对一下,以org.springaicommunity为准。

图1 · 方式A:Scanner练手;方式B:Contributor生产线
方式 A:练手(不进平台)
单模块先把协议跑通,不引入 Dream-MCP 五个 jar。
代码
bash
@Component
public class McpService {
@McpTool(name = "hello", description = "一个MCP Server的测试方法,输出调用日志")
public String hello(
@McpToolParam(description = "请求的类型") String type,
@McpToolParam(description = "请求的名称") String name) {
return new TestTool().Hello(type, name);
}
}
配置
bash
spring:
ai:
mcp:
server:
enabled: true
annotation-scanner:
enabled: true # 方式A必须开
type: sync
protocol: streamable
streamable-http:
mcp-endpoint: /mcp
dream-saas:
mcp:
server:
enabled: false # 方式A必须关
验收
bash
npx --yes @modelcontextprotocol/inspector --cli http://127.0.0.1:7090/mcp --method tools/list
npx --yes @modelcontextprotocol/inspector --cli http://127.0.0.1:7090/mcp --method tools/call \
--tool-name hello --tool-arg "type=test" --tool-arg "name=world"
Inspector 看到 hello 就够。不引入 Dream-MCP 平台 jar,也不是 code-review-agent 的生产形态。
方式 B:生产线(Dream-MCP 平台)
多模块仓库、code-review-agent 同款。你要写的就三件事:
- 业务类上
@Tool - 实现
McpToolContributor,toolObjects()返回 Bean 实例(不是 Method) - 两个开关都开:
spring.ai.mcp.server.enabled+dream-saas.mcp.server.enabled;scanner 关
Step 1:业务类写 @Tool
bash
@Component
public class McpService {
@Tool(name = "hello", description = "一个MCP Server的测试方法,输出调用日志")
public String hello(
@ToolParam(description = "请求的类型") String type,
@ToolParam(description = "请求的名称") String name) {
return new TestTool().Hello(type, name);
}
}
注意 :同一个
McpService可以同时保留@McpTool和@Tool两套方法(注释掉一套),但同一次启动不能双开同名 Tool ,否则IllegalArgumentException: Tool with name 'hello' is already registered。
Step 2:写 Contributor
bash
@Component
public class TestToolContributor implements McpToolContributor {
@Autowired
private McpService mcpService;
@Override
public String moduleId() { return "test"; }
@Override
public Object[] toolObjects() {
return new Object[] { mcpService }; // 返回Bean实例,不是Method
}
}
为什么返回 Bean 而不是 Method? 因为平台层 McpToolContributorAggregator 内部用 MethodToolCallbackProvider.toolObjects() 扫描 Bean 上的 @Tool 方法,再转为 MCP SyncToolSpecification。你传 Method 进去它不认识。
Step 3:配置
bash
spring:
ai:
mcp:
server:
enabled: true
annotation-scanner:
enabled: false # 方式B必须关,避免与Contributor重复
type: sync
protocol: streamable
streamable-http:
mcp-endpoint: /mcp
dream-saas:
mcp:
server:
enabled: true # 方式B必须开
成功标志
启动日志出现:
bash
[MCP-SERVER] aggregated tools from contributors: {test=1}
{test=0} → McpService 上缺 @Tool,或 Contributor 未扫到 Bean。
方式 B 的调用链:从 @Tool 到 /mcp 端点
这是我觉得现有教程最缺的部分------只说"加注解就行",但注解怎么变成 HTTP 端点上的工具,没人画出来。
bash
业务模块 McpService (@Tool方法)
→ TestToolContributor.toolObjects() 返回Bean实例
→ McpToolContributorAggregator.aggregate() 遍历所有Contributor
→ MethodToolCallbackProvider.toolObjects() 扫描@Tool方法 → ToolCallback[]
→ McpToolUtils.toSyncToolSpecification() → SyncToolSpecification列表
→ Spring AI MCP Server 注册到 /mcp 端点
对应代码(McpToolContributorAggregator,平台层核心):
bash
public static List<SyncToolSpecification> aggregate(List<McpToolContributor> contributors) {
List<SyncToolSpecification> specifications = new ArrayList<>();
Map<String, Integer> toolCountsByModule = new LinkedHashMap<>();
for (McpToolContributor contributor : contributors) {
if (!contributor.enabled()) continue;
Object[] toolObjects = contributor.toolObjects();
if (toolObjects == null || toolObjects.length == 0) continue;
// @Tool方法 → ToolCallback → MCP SyncToolSpecification
ToolCallback[] callbacks = MethodToolCallbackProvider.builder()
.toolObjects(toolObjects).build().getToolCallbacks();
toolCountsByModule.put(contributor.moduleId(), callbacks.length);
specifications.addAll(McpToolUtils.toSyncToolSpecification(Arrays.asList(callbacks)));
}
log.info("[MCP-SERVER] aggregated tools from contributors: {}", toolCountsByModule);
return specifications;
}
注意 :此处产出 SyncToolSpecification,不要 注册 ToolCallbackProvider------否则会被 ChatModel 的 toolCallbackResolver 收集,与 CodeReviewServiceNew → ChatModel 形成循环依赖。
自动装配怎么生效
McpServerPlatformAutoConfiguration 把聚合结果注册为 Spring Bean:
bash
@AutoConfiguration
@ConditionalOnProperty(prefix = "dream-saas.mcp.server", name = "enabled", havingValue = "true")
public class McpServerPlatformAutoConfiguration {
@Bean
public List<SyncToolSpecification> dreamSaasAggregatedMcpTools(
List<McpToolContributor> contributors) {
return McpToolContributorAggregator.aggregate(contributors);
}
}
Spring AI 的 McpServerAutoConfiguration 通过 ObjectProvider<List<SyncToolSpecification>> 自动收集这个 Bean,挂到 MCP Server。你不用手动注册任何东西。
只开 Spring AI 开关、没开平台开关会怎样? SSE 能连、但工具列表为空------因为 Aggregator 没生效,没有 Contributor 被扫描。
Dream-MCP 平台落地
方式 B 对应篇 1 的 dream-ai-mcp :业务写 Tool,平台做聚合,对话应用做 Client。

图2 · Dream-MCP:Server侧写Tool → 平台层聚合 → Client侧消费
五模块分工
| 模块 | 干什么 | 改什么动它 |
|---|---|---|
| common | 常量(连接ID、模块ID) | 加常量 |
| spi | McpToolContributor 接口 |
加接口方法 |
| registry | 名册配置 + 启动自检 | 加Server配置 |
| client | SSE + Streamable HTTP建连 + 观测 + 诊断 | 换传输/加观测 |
| server | SPI聚合 → MCP工具暴露 | 加工具实现 |
每个模块只有一个改的理由。加 Server 改 registry,加工具实现 spi,换传输方式改 client,互不影响。
Server 侧(code-review-agent)
Maven:dream-saas-mcp-spi + spring-ai-starter-mcp-server-webmvc,运行时 dream-saas-mcp-server。不要自己注册全局 ToolCallbackProvider(易循环依赖)。打包 -Pmcp、--spring.profiles.active=mcp。
Client 侧(dream-ai-app)
引 dream-saas-mcp-client 即有 mcpChatClient、/mcp/ask、/mcp/tools、/mcp/status;Java 通常不用写 MCP 协议代码。
Client 侧核心装配(McpConfig):
bash
@Configuration
public class McpConfig {
@Bean("mcpChatClient")
public ChatClient mcpChatClient(
ChatModel chatModel,
ObjectProvider<SyncMcpToolCallbackProvider> syncMcpToolCallbackProvider) {
ChatClient.Builder builder = ChatClient.builder(chatModel);
syncMcpToolCallbackProvider.ifAvailable(provider -> {
// 每个ToolCallback包一层日志装饰器
ToolCallback[] wrapped = Arrays.stream(provider.getToolCallbacks())
.map(LoggingToolCallback::wrap)
.toArray(ToolCallback[]::new);
builder.defaultToolCallbacks(wrapped);
});
return builder.build();
}
}
SyncMcpToolCallbackProvider 由 Spring AI MCP Client Starter 根据 registry YAML 自动创建,这里只负责挂到 ChatClient 并加观测包装。
名册:连接信息的单一来源
名册在 dream-saas-mcp-registry-default.yml(spring.config.import 引入即可):
bash
dream-saas:
mcp:
registry:
servers:
code-review-local:
module-id: code-review
url: ${MCP_CODE_REVIEW_SERVER_URL:http://127.0.0.1:8095}
sse-endpoint: /mcp/sse
connections:
code-review-local:
transport: sse
url: http://127.0.0.1:8095
sse-endpoint: /mcp/sse
zhipu-web-search:
transport: sse
url: https://open.bigmodel.cn
sse-endpoint: /api/mcp/web_search/sse?Authorization=${ZHIPU_API_KEY:}
amap-maps:
transport: streamable-http
url: https://mcp.amap.com
endpoint: /mcp?key=${AMAP_MAPS_API_KEY:}
# Spring AI 实际建连(须与 registry.connections 键名一致)
spring:
ai:
mcp:
client:
sse:
connections:
code-review-local:
url: http://127.0.0.1:8095
sse-endpoint: /mcp/sse
zhipu-web-search:
url: https://open.bigmodel.cn
sse-endpoint: /api/mcp/web_search/sse?Authorization=${ZHIPU_API_KEY:}
streamable-http:
connections:
amap-maps:
url: https://mcp.amap.com
endpoint: /mcp?key=${AMAP_MAPS_API_KEY:}
双轨名册(排障重点) :dream-saas.mcp.registry.* 管 /mcp/status 登记;spring.ai.mcp.client.*.connections.* 管真正建连。键名错位 → status 有记录、tools=0。
踩坑清单
坑1:@Tool ≠ @McpTool
| 注解 | 包名 | 作用 |
|---|---|---|
@Tool |
org.springframework.ai.tool.annotation |
ChatClient / LLM 进程内工具 |
@McpTool |
org.springaicommunity.mcp.annotation |
MCP Server + annotation-scanner |
annotation-scanner.enabled=true 只扫 @McpTool ,扫不到 @Tool。写了 @Tool 开 scanner,tools/list 一定空。
坑2:同进程 Server + Client 自连不稳
同一 JVM 里 Client 在启动阶段 HTTP 连本机 /mcp,Server 的 streamable 传输尚未就绪------ConnectException、TimeoutException 20000ms、sessionFactory is null 全来了。
推荐:两个 Boot 进程(同一 jar 起两次,参数不同):
bash
# 终端A --- Server
mvn spring-boot:run "-Dspring-boot.run.jvmArguments=-Dspring.ai.mcp.client.enabled=false"
# 终端B --- Client(A先起来)
mvn spring-boot:run "-Dspring-boot.run.jvmArguments=-Dserver.port=7091 -Dspring.ai.mcp.server.enabled=false -Dspring.ai.mcp.client.enabled=true"
一个应用可以 同时当 Server 和 Client(Client 连外部 Server),但不要 自连自己的 /mcp。
坑3:Client 工具数为 0
| 原因 | 处理 |
|---|---|
spring.ai.mcp.client.enabled=false |
开 Client 开关 |
| YAML 路径写错 | 收到 spring.ai.mcp 下 |
无 streamable-http.connections / sse.connections |
配好 url + endpoint |
| 同进程自连失败 | 改两进程 |
注入了错的 ToolCallbackProvider |
看 SyncMcpToolCallbackProvider 日志 |
| registry 键名与 Spring AI connections 键名不一致 | 逐个对齐 |
坑4:只开 Spring AI 开关、没开平台开关
spring.ai.mcp.server.enabled=true 但 dream-saas.mcp.server.enabled=false:SSE 能连,但工具列表为空。因为 Aggregator 没生效,没有 Contributor 被扫描。
验收:三层递进
① 协议面 --- Inspector 打 code-review-agent:
bash
npx --yes @modelcontextprotocol/inspector --cli http://127.0.0.1:8095/mcp --method tools/list
tools/list 含 review_code。
② 集成面 --- dream-ai-app 启动日志:
bash
[MCP-CLIENT] startup | enabled=true | toolCount=24 | toolGroups={code-review-local=[review_code, ...], ...}
toolCount=0 查上面踩坑清单。
③ 模型面 (篇 3)--- /mcp/ask 出现 [MCP-TOOL] 且带 connection=。①② 稳了再跟篇 3。
A ↔ B 切换检查清单
从 A 切 B(或反过来)时逐项改:
McpService只保留对应注解块(另一块注释)annotation-scanner.enabled与方案一致dream-saas.mcp.server.enabled与方案一致TestToolContributor.toolObjects()非空仅方案 B- 重启后无
already registered - Inspector
tools/list数量正确
怎么选
| 场景 | 建议 |
|---|---|
| 学协议、跑 demo | 方式 A,依赖少 |
多模块聚合、对齐 code-review-agent |
方式 B,Contributor 模式 |
| 新增业务能力 | 新模块 Contributor,不动平台 jar |
| 新增外部 MCP | registry 加 connection + 环境变量,不动平台 jar |
小结
这篇解决 Server 端的三个问题:
- @Tool 为什么不生效 --- 两条注册路径,注解和开关必须对齐
- Contributor 怎么变成 /mcp 端点上的工具 --- SPI → Aggregator → AutoConfiguration 完整调用链
- 怎么确认真的调通了 --- 三层验收(协议面 → 集成面 → 模型面)+ 四个踩坑
下篇写完整业务链:/mcp/ask 一问,本机审查与智谱检索怎么选型,[MCP-CLIENT] / [MCP-TOOL] / [MCP-SERVER] 三类日志,以及启动与 REST 验收命令。
版本:spring-ai-bom 1.1.6 + spring-ai-alibaba 1.1.2.2 + MCP SDK 0.18.2
本文同步发布于 Java宋转AI 公众号,专注于 Java 工程师转 AI Agent 实战记录。
