Spring AI MCP Stdio 报错信息排查

本文汇总一下 Spring AI 通过 STDIO 方式接入 MCP Server 时,常见报错的排查思路。这里讨论的场景是:Spring AI 客户端通过 spring.ai.mcp.client.stdio 启动外部 MCP Server,并通过 stdio 与它通信。

具体课程 MCP 链接:https://www.codefather.cn/course/1915010091721236482/section/1923324591245287425

一、先看结论

如果你遇到了 Spring AI MCP stdio 相关报错,建议按下面顺序排查:

  1. 先单独验证 yu-image-search-mcp-server 本身的业务逻辑,不要一上来就从 AI 对话入口排查。
  2. 只要改过 MCP Server 代码,就重新执行 package,因为 Spring AI 客户端最终拉起的是 jar,不是 IDEA 里的源码。
  3. 手动运行 jar,确认进程能否正常拉起。对 stdio 场景来说,“进程没有立刻退出”往往比“终端打印了日志”更重要。
  4. 核对 application.ymlmcp-servers.json,尤其是命令、参数、工作目录和 jar 路径。
  5. stdio 模式下,MCP Server 只能把协议消息写到 stdout;普通日志应该写到 stderr 或文件,不能污染 stdout
  6. 调试时看到 exitValue() 取不到值,只能说明子进程还没退出,不能单独当作“启动成功”的充分证据。

二、推荐排查顺序

1. 先单独验证 yu-image-search-mcp-server

需要先把 ImageSearchToolAPI_KEY 替换成你自己的真实 Key。Pexels API Key 获取地址:https://www.pexels.com/api/key/

配置完成后,先单独运行 ImageSearchToolTest#searchImage,确认图片链接能否正常返回。

如果这一步就失败了,那么问题通常还不在 MCP,而在下面这些地方:

  1. API_KEY 不正确或未生效。
  2. 工具代码本身有问题。
  3. 依赖版本和源码示例不一致。

image.png

2. 改完代码后重新打包 jar

只要改了 yu-image-search-mcp-server 的代码,就一定要重新执行 package。因为 Spring AI 通过 ProcessBuilder 拉起的是磁盘上的 jar,不是你当前 IDEA 里尚未打包的代码!

image.png

3. 手动运行 jar,确认 stdio Server 能否启动

建议在父项目目录打开终端,然后手动执行和 mcp-servers.json 中一致的命令。

找到父项目,右键选择 OpenIn -> Terminal

image.png

执行命令:

bash
复制代码
java "-Dspring.ai.mcp.server.stdio=true" "-Dspring.main.web-application-type=none" -jar yu-image-search-mcp-server/target/yu-image-search-mcp-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=stdio

这里有几个关键点:

  1. spring.ai.mcp.server.stdio=true 表示以 STDIO 模式启动 MCP Server。

  2. spring.main.web-application-type=none 表示不要按 Web 应用启动。

正常可以发现启动成功:

如果启动失败需要排查一下自己的 Java 版本是否是 >= 21

image.png

4. 核对 application.ymlmcp-servers.json

先看客户端配置:

yaml
复制代码
spring: ai: mcp: client: stdio: servers-configuration: classpath:mcp-servers.json

这里的 servers-configuration 是 Spring AI 官方支持的 stdio 外部配置方式。

准备 mcp-servers.json

排查阶段建议先只保留一个待排查的 MCP Server,不要同时把多个 MCP 都配进去(amap-maps 不要先写进入),避免互相干扰。

json
复制代码
{ "mcpServers": { "yu-image-search-mcp-server": { "command": "java", "args": [ "-Dspring.ai.mcp.server.stdio=true", "-Dspring.main.web-application-type=none", "-Dspring.main.banner-mode=off", "-Dlogging.pattern.console=", "-jar", "yu-image-search-mcp-server/target/yu-image-search-mcp-server-0.0.1-SNAPSHOT.jar", "--spring.profiles.active=stdio" ], "env": {} } } }

这一段最容易出错的地方主要有二个:

  1. commandargs 与你手动验证成功的命令不一致。
  2. jar 路径写错。

5. 通过 Debug 或独立测试进一步定位

如果上面的步骤都做过了,还是报错,那么可以继续往下定位。

首先运行测试类 doChatWithMcp()

image.png

然后可以在 io.modelcontextprotocol.client.transport.StdioClientTransport 附近打断点,再 Debug 运行 doChatWithMcp()

image.png

如果这里的 exitValue 是 not exited 的话,那么大概率是启动成功了。如果没有走到这个断点,那么就检查自己的配置文件是否配置正确,也就是上面一节。

可以走到断电,但是值不是 not exited 的话就需要再单独测试,具体如何测试需要看下面的代码。

下面这段代码可以放到 src/test/java/com/yupi/yuaiagent/app 包下:

java
复制代码
import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.concurrent.TimeUnit; public class McpServerChecker { public static void main(String[] args) { List<String> command = List.of( "java", "-Dspring.ai.mcp.server.stdio=true", "-Dspring.main.web-application-type=none", "-Dspring.main.banner-mode=off", "-Dlogging.pattern.console=", "-jar", "yu-image-search-mcp-server/target/yu-image-search-mcp-server-0.0.1-SNAPSHOT.jar", "--spring.profiles.active=stdio" ); ProcessBuilder pb = new ProcessBuilder(command); pb.redirectErrorStream(true); try { Process process = pb.start(); boolean exited = process.waitFor(5, TimeUnit.SECONDS); if (!exited) { System.out.println("进程 5 秒内没有退出,说明服务大概率已经拉起,正在等待 MCP Client 通过 stdin 建连。"); process.destroy(); return; } String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); System.err.println("进程启动失败,exitCode = " + process.exitValue()); System.err.println(output); } catch (IOException e) { System.err.println("启动异常: " + e.getMessage()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("等待进程结果时被中断"); } } }

这个测试的意义是:

  1. 验证命令本身能不能成功拉起子进程。
  2. 如果进程立即退出,就能直接看到退出码和错误输出。
  3. 如果进程没有立即退出,至少说明“命令层面”基本没问题,下一步就要继续看 MCP 握手和工具调用链路。
  4. 最后需要手动停止这个测试方法

三、系统编码的问题

如果遇到的报错信息类似下面的报错信息:

text
复制代码
2025-10-20 10:10:31.298 [pool-2-thread-1] ERROR i.m.client.transport.StdioClientTransport - Error processing inbound message for line: Active code page: 65001 com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'Active': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')

并且打开 cmd 可以发现这个信息:

image-20260401133800231

满足以上两点就证明系统编码有问题,我是不太推荐解决的(因为编码问题一旦随便修改可能会导致系统环境变量出现问题,如果要修改也建议先设置一下系统还原点),如果是这个问题我个人建议是使用 Linux 系统测试代码是否可以正常运行,如果正常运行就直接继续写代码即可。解决的话也可以参考一下这位大佬的文章:https://www.codefather.cn/post/1980098355279175681

四、如何判断最终排查成功

最终用下面这条链路判断是否真的排查完成:

  1. ImageSearchToolTest#searchImage 单独运行成功。
  2. 重新 package 后,jar 手动运行不会立刻退出。
  3. doChatWithMcp() 能正常拿到图片 URL。
  4. 图片 URL 可以实际访问,说明结果来自工具调用,而不是模型幻觉。

正常情况下,doChatWithMcp() 的返回内容里会包含真实图片链接,并且链接可以正常访问:

image.png

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
leikooo
作者分享
ARTS 0815: 分隔链表、大删除是加活不是减负与协议栈如何一层层拆信封
3
DeepSeek Harness 缓存命中率太惊人了,有时候竟然能到 99%,看鱼皮哥的视频竟然还出现过 100% 😱 https://www.bilibili.com/video/BV1VkgK6NEZS
7
ARTS 0809: 反转链表、Shopify 如何用 MySQL 解决超卖与 AI 时代程序员的价值
5
译文:《SwiftUI 七年:平庸的故事》 SwiftUI 在 2019 年高调发布,本应成为苹果全平台成熟、可量产的 UI 未来。七年过去,到了 2026 年,它仍像一场永不结束的 beta:布局难预期、性能不稳、数据流混乱,还几乎没有可靠的向后兼容,开发者被迫写一堆 shim 和 workaround。 作者用苹果官方教程(甚至是有问题的)以及与 UIKit 的对比说明:SwiftUI 用“看起来方便”换掉了精确的工程控制。更深一层,他认为这反映了苹果从 Cocoa、Aqua、Auto Layout 那种不妥协的工艺,转向“够用就行”的企业文化。 SwiftUI 为何存在 苹果并非单纯想提供更好工具,而是不得不应对竞争:React、React Native、Flutter 让“一套代码多端跑”变得诱人;Mac 上原生应用又日渐被网页和 Electron 吃掉。SwiftUI 要同时拴住原生生态、并降低移植到 Mac 的成本。卖点是:响应式数据流、声明式布局、跨平台复用。 数据流 “单一数据源”听起来很美,实际却是 @State、@Binding、ObservedObject,再到 Observation / @Observable 的不断换代。你很难确定视图会更新几次、为何更新;它该忽略的变化会反应,该关心的变化又可能忽略 - - 像个黑盒。 布局系统 基于尺寸协商的布局在 Keynote 里很合理,做浮动视图、自定义侧边栏时却极度不稳定。官方教程里一个很普通的侧边栏,多年仍有问题。布局脆弱到最后往往只能上 GeometryReader - - 一旦用了,声明式优势就没了,还要手算坐标,而且下一版布局规则一变,数学还得重写。 API 稳定与功能对等 代码里满是 if #available。滚动收起键盘要到 iOS 16;工具栏定制很晚才来;网络图片 AsyncImage 要到 iOS 15,缓存相关 API 到 2026 年 7 月仍在 beta。旧 API 常被换掉(如 NavigationView → NavigationStack),开发者要维护多套实现,等于替苹果做 QA。对比 Android 的 Jetpack Compose 可作为依赖打包回退到旧设备,SwiftUI 做不到“写最新 API、稳定回退”。 性能 在真实对比里,即便做了后台解码等优化,SwiftUI 图片网格滚动仍明显不如 UIKit。若展示一堆 JPEG 都得靠顶级芯片撑,架构本身就有问题。 跨平台神话 苹果说的是“学一次、到处用”,不是“写一次、到处跑”。iOS 上学到的布局很少直接适用 Mac;同一套 view 跨平台实现也不一致。结果常变成:学一次、再学一次、某处能用、处处要调。 哲学转向 最大的问题是“够用就行”:覆盖 90% 用例就算成功,用 velocity 掩盖质量下降。作者列举系统与一线应用中的各种瑕疵,认为这不是偶然,而是苹果主动降低质量门槛 - - 所以即使过了七年,他仍不信任 SwiftUI。 结论 对构建稳定、高性能、可维护系统真正重要的部分,SwiftUI 几乎都有问题。它不是“极差”,而是平庸 - - 用假便利换真精度,要么你花时间给框架打补丁,要么把半成品发出去。作者更宁愿继续用“遗留”的 UIKit / AppKit。 最后小总结: 最让人不能接受的不是“SwiftUI 还有 bug”,而是它把“看起来很快”当成了工程上的完成态。声明式、预览、跨平台,每一项都在秀高级感,可真正写进业务后,你面对的是难预测的重绘、脆弱的布局、层层 #available,以及把兼容和排错外包给业务方的现实。七年够长了,若还靠“框架还年轻”解释,那更像是对标准的侮辱。技术选型从来不只是语法偏好,而是你选的是可预期性、可维护成本,以及对用户体验的态度。平庸的“成功”往往比明显失败更危险 ,你说它能上线、能 demo、能交差,但是却在细节里一点点磨损信任。工具可以换代,但对质量的要求不该跟着一起降级。
4
ARTS 0802: 合并有序链表、AI 时代的技术断层与 TCP 200ms 延迟之谜
5
下载 APP