从MCP客户端连不上到工具列表为空:一次Spring AI联调踩坑全记录

本文记录一次基于 Spring AI 的 MCP(Model Context Protocol)实践项目联调过程,包含四个真实踩坑的完整排查与解决记录。技术栈:Spring Boot 3.5.11 + Spring AI 1.1.2 + MCP Java SDK 0.17.0 + JDK 21。

一、项目背景

项目包含两个模块:

  • mcp-tools-server:MCP 服务端,提供工具、资源、提示词
  • mcp-tools-client:MCP 客户端,通过 stdio 方式启动并连接服务端

整个联调过程踩了四个坑,从”进程启动即失败”到”能连上但工具列表为空”,下面逐一记录。

二、坑一:addEnvVar 误用导致服务端进程根本没启动

现象

客户端启动时报错:

1
2
java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 20000ms in 'map'
Caused by: java.lang.RuntimeException: Client failed to initialize by explicit API call

同时日志里大量 STDERR 输出乱码:

1
2
STDERR Message received:     -cp <Ŀ¼�� zip/jar �ļ��������·��>
STDERR Message received: -classpath <Ŀ¼�� zip/jar �ļ��������·��>

原因

客户端构建 ServerParameters 时写成了:

1
2
3
ServerParameters.builder("java")
.addEnvVar("-jar", "D:\\...\\mcp-tools-server-1.0.0.jar")
.build()

addEnvVar 是用来设置环境变量的,不是传命令行参数。这行代码等价于”启动 java 程序,并设置一个名为 -jar 的环境变量”。java 命令没收到任何有效参数,于是打印帮助信息(就是那些乱码)后退出。客户端在 stdout 上等不到 JSON-RPC 响应,20 秒后超时。

解决

传命令行参数要用 args:

1
2
3
ServerParameters.builder("java")
.args("-jar", "D:\\...\\mcp-tools-server-1.0.0.jar")
.build()

经验:MCP 的 stdio 传输要求服务端 stdout 只输出 JSON-RPC 协议消息,任何日志、帮助信息都必须走 stderr。客户端日志里出现大量 STDERR 输出时,第一反应应该是”服务端没正常启动或没在 stdout 上说话”。

三、坑二:OpenAI 自动配置缺 API Key 导致启动失败(连环出现两次)

现象

MCP 连接成功后,应用启动仍然失败:

1
2
3
Error creating bean with name 'openAiAudioSpeechModel':
OpenAI API key must be set. Use the connection property:
spring.ai.openai.api-key or spring.ai.openai.speech.api-key property.

按提示排除或配置后,下一次启动又冒出另一个:

1
2
Error creating bean with name 'openAiAudioTranscriptionModel':
OpenAI API key must be set. ...

原因

Spring AI 的 OpenAI starter 不是单一自动配置,而是按模型类型拆成多个独立配置类:

配置类 模型类型
Chat 对话
Embedding 向量嵌入
Image 图像
Audio Speech 语音合成
Audio Transcription 语音转写
Moderation 内容审核

每个配置类在创建 Bean 时都会独立检查 API Key。只排除 Speech 那个,Transcription 的配置仍然激活,于是”打地鼠”式报错。

解决

相比在启动类里逐个写 exclude,更干净的是用 Spring AI 提供的顶层属性一次性关闭不用的模型。在 application.yml 中:

1
2
3
4
5
6
7
8
9
10
spring:
ai:
model:
chat: none
embedding: none
image: none
moderation: none
audio:
speech: none
transcription: none

这样所有 OpenAI 模型都不会尝试初始化,也就不会去检查 API Key。如果确实要用 Chat 模型,只保留 spring.ai.model.chat: openai(或删掉那行),并配置好 spring.ai.openai.api-key 即可。

经验:Spring AI 1.1.x 中,部分 OpenAI 自动配置之间存在 @ImportAutoConfiguration 依赖关系,用 exclude 可能排除不干净。属性禁用方案(spring.ai.model.*=none)比 exclude 更可靠。

四、坑三:listTools() 返回空,日志提示 No tool methods found

现象

MCP 连接成功、应用正常启动,但客户端调用 listTools() 拿不到任何工具。服务端日志持续出现:

1
WARN o.s.m.p.tool.SyncMcpToolProvider - No tool methods found in the provided tool objects: []

排查过程

BusinessTools 本身写得没问题:类上有 @Component,方法都是 public,注解也加了。问题出在注册路径上。

最初用的是 Spring AI 通用工具注解 @Tool,并手动创建 ToolCallbackProvider:

1
2
3
4
5
6
@Bean
public ToolCallbackProvider businessToolCallbacks(BusinessTools tools) {
return MethodToolCallbackProvider.builder()
.toolObjects(tools)
.build();
}

结果直接报错:

1
No @Tool annotated methods found in com.jichi.mcp.BusinessTools@73fb1d7f.

注意对象地址 @73fb1d7f 没有 $$SpringCGLIB$$ 后缀,说明传入的不是代理对象,但 MethodToolCallbackProvider 依然扫不到 @Tool 方法。这是 Spring AI 1.1.x 在某些场景下对 @Tool 扫描的已知缺陷,社区有多个 Issue 记录。

解决:切换到 MCP Server 原生注解 @McpTool

Spring AI MCP Server 提供了专用的 @McpTool / @McpToolParam 注解,配合 McpServerAnnotationScannerAutoConfiguration 自动扫描注册,不需要也不应该手动创建 ToolCallbackProvider。

关键点有两个:

1. 用对包路径。 classpath 里同时存在 spring-ai-mcp-annotations-1.1.2.jar 和早期的 mcp-annotations-0.8.0.jar,但 Spring AI 1.1.2 的自动扫描器只认自己的命名空间:

1
2
3
4
5
6
// 正确
import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;

// 错误(早期社区包,扫描器不认)
// import org.springaicommunity.mcp.annotation.McpTool;

2. 删掉手动注册的 Bean。 保留 BusinessTools 上的 @Component,让自动扫描器接管即可。

改完后,四个工具方法用 @McpTool 标注:

1
2
3
4
5
6
7
8
9
10
11
@Component
public class BusinessTools {

@McpTool(description = "按 URI 读取 MCP 资源的内容。...")
public String readResource(
@McpToolParam(description = "资源 URI,例如:docs://...") String uri) {
return resourceReader.read(uri);
}

// querySales、queryInventory、getDateInfo 同理
}

服务端启动后不再出现 No tool methods found,客户端 listTools() 正常返回四个工具。

经验:@Tool 和 @McpTool 是两条不同的注册路径。@Tool 需要手动 ToolCallbackProvider,且在某些代理/生命周期场景下有扫描缺陷;@McpTool 配合自动扫描更稳定,是 MCP Server 场景的推荐用法。此外,注意注解的包路径——早期社区包和 Spring AI 官方包的注解不互通。

五、坑四(非致命):几条可以忽略的 WARN

最后应用完整启动成功:

1
2
Tomcat started on port 8080 (http) with context path '/'
Started clientApplication in 5.876 seconds

日志里还剩几条 WARN,都不影响功能,记录一下含义:

WARN 日志 含义
SyncMcpSamplingProvider - No sampling methods found 客户端没有注册 sampling(服务端反向调用大模型)回调
SyncMcpElicitationProvider - No elicitation methods found 客户端没有注册 elicitation(服务端向用户发起交互)回调
BeanPostProcessorChecker ... is not eligible for getting processed by all BeanPostProcessors 来自 Spring AI 自动配置框架内部,非致命

只有需要服务端主动发 sampling/createMessage 或 elicitation/create 请求时才需要实现对应回调,否则可以忽略这两条 WARN。

六、踩坑总结

坑 现象 根因 解决
1 客户端初始化 20 秒超时,STDERR 大量乱码 addEnvVar 误用来传命令行参数 改用 .args("-jar", path)
2 启动失败,提示 OpenAI API key 缺失(连环出现) 多个 OpenAI 自动配置独立检查 Key spring.ai.model.*=none 一次性禁用
3 listTools() 为空,No tool methods found @Tool 扫描缺陷 + 注解包路径错误 改用 org.springframework.ai.mcp.annotation.McpTool,删掉手动 Provider
4 sampling/elicitation 等 WARN 未注册对应回调 按需实现,否则忽略

几个关键心法

点开查看心法详情
  1. stdio 通信的纯净性:服务端 stdout 只能输出 JSON-RPC,日志必须走 stderr
  2. 自动配置的粒度:Spring AI 按模型类型拆分自动配置,禁用要用属性而不是逐个 exclude
  3. 注解的命名空间:@Tool 和 @McpTool 是两条路,包路径要对,注册方式要匹配
  4. 看日志的顺序:先确认进程是否正常启动,再确认握手是否成功,最后才看业务逻辑——本次四个坑正好对应这四个层次

希望这篇踩坑记录能帮到正在折腾 Spring AI MCP 的同学。