@Tool写了但tools/list为空?Spring AI MCP Server注册的两种路径和四个坑

tools/list 为空、Contributor 日志 {code-review=0}/mcp/status 有记录但 Client tools=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_codeget_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 同款。你要写的就三件事:

  1. 业务类上 @Tool
  2. 实现 McpToolContributortoolObjects() 返回 Bean 实例(不是 Method)
  3. 两个开关都开:spring.ai.mcp.server.enabled + dream-saas.mcp.server.enabledscanner 关

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/statusJava 通常不用写 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.ymlspring.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 传输尚未就绪------ConnectExceptionTimeoutException 20000mssessionFactory 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=truedream-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/listreview_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 端的三个问题:

  1. @Tool 为什么不生效 --- 两条注册路径,注解和开关必须对齐
  2. Contributor 怎么变成 /mcp 端点上的工具 --- SPI → Aggregator → AutoConfiguration 完整调用链
  3. 怎么确认真的调通了 --- 三层验收(协议面 → 集成面 → 模型面)+ 四个踩坑

下篇写完整业务链:/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 实战记录。


相关推荐
兰令水1 小时前
leecodecode【树形DP】【2026.6.11打卡-java版本】
java·算法·深度优先
骑士雄师1 小时前
19.3 langgraph的工作节点和路由函数
java·前端·数据库
程序员柒叔1 小时前
Hermes Agent 一周动态-2026-W24
人工智能·github·agent·openclaw·hermes
SWAGGY..2 小时前
Linux系统编程:(十三)环境变量
java·linux·算法
程序员黑豆2 小时前
AI全栈开发 - Java:基本数据类型 vs 引用数据类型的内存存储
java·前端·ai编程
布朗克1682 小时前
34 JVM深入理解
java·jvm
Flittly2 小时前
【AgentScope Java新手村系列】(4)结构化输出
java·spring boot·spring·ai
何以解忧,唯有..2 小时前
Python 中的继承机制:从基础到高级用法详解
java·开发语言·python
Yiyaoshujuku3 小时前
化合物数据集API接口(数据结构及样例)
java·网络·数据结构