一开始折腾 Spring AI大家基本都是同一个路径聊天、流式、Spring AI Alibaba 或者 OpenAI 兼容接口跑通就算完事。但聊到后面你就会发现一个尴尬的问题——大模型只能“嘴上说说”它没办法真的看你磁盘上的文件更别说帮你整理目录。要让模型真正干点活就得走 MCP 这条路。我说的 MCP全称是 Model Context ProtocolSpring AI 2 已经把它当成一等公民来支持了。今天这篇我用 filesystem MCP Server 做例子手把手带你把两条主流通道全部跑通SSE 模式和 stdio 模式并且最终真的让模型调用文件工具、读取文件内容、把结果返回给你。不是只连个连接看看日志而是实打实把工具调起来。适合谁看想用 Spring AI 集成 MCP、又不太清楚 SSE 和 stdio 怎么选、或是配置完一直报错不知道哪出问题的 Java 开发这篇文章应该能帮你省掉大半天的排查时间。下面我会把原理、配置、代码、排查全部串起来讲。1. 折腾之前先把 MCP、filesystem Server 和 Spring AI 2 的角色捋清楚1.1 MCP 到底是什么一个类比就懂MCP 你可以理解成 AI 场景里的 “USB 接口”。没有 MCP 之前每个大模型应用要接一个工具就得单独写一套对接逻辑接文件系统一套、接数据库一套、接 GitHub 又一套接得越多越痛苦。MCP 做的事情是把“工具能力”统一包装成一个标准协议客户端这边只要实现了协议就能接上所有符合规范的 server不需要为每个工具单独定制对接。在这个架构里Spring AI 2 是 MCP 客户端filesystem MCP Server 是服务端。服务端把“读取文件、列目录、搜索文件、写入文件”这些能力一个个暴露成标准工具Spring AI 通过协议发现这些工具把它们的定义交给大模型大模型在回答问题时根据上下文决定“我要调用哪一个”调用结果再拿回来生成最终答案。1.2 filesystem Server 到底提供了哪些工具官方 filesystem MCP Server 提供的核心工具包括list_directory列出指定路径下的目录项read_file读取指定文件的完整内容read_multiple_files批量读取多个文件write_file创建或覆盖写入一个文件edit_file对文件内容做局部编辑替换search_files按文件名模式在目录中搜索get_file_info读取文件的元信息比如大小、修改时间这些工具名字在 Spring AI 日志里都能看到后面真调工具那一节我会演示其中几个的调用过程。1.3 SSE 和 stdio两条通道的本质区别很多人一上来就被 SSE 和 stdio 搞晕其实它俩只是客户端与服务端之间的“传输管道”不同stdio 模式Spring AI 在你本地启动一个子进程比如用 npx 拉起 Node 写的 filesystem server然后通过子进程的标准输入stdin发请求、通过标准输出stdout读响应。父子进程共用一台机器由 Spring AI 管理这个子进程的整个生命周期。SSE 模式filesystem server 跑在一个独立的 HTTP 服务里Spring AI 通过发送 HTTP 请求、监听服务器推送的 SSE 事件流来完成通信。client 和 server 可以不在同一台机器上甚至可以跨网络。哪种更好没有标准答案。stdio 模式胜在简单Spring AI 一条配置就能拉起本地进程适合开发和单机部署SSE 模式适合 server 独立托管、多个客户端共享同一个工具服务的场景。很多人在这里有个误解觉得“stdio 是老古董SSE 才是正规军”。实际恰恰相反官方 TS 版 filesystem server 默认只支持 stdioSSE 模式你得用 Python 版或者加代理适配层。搞清楚这一点你会少踩一个隐形的坑。2. 动手前的硬条件JDK、Spring Boot、Node 和一份可用的模型 API2.1 版本选型我这次用的是这套组合版本匹配是 MCP 集成最容易翻车的地方先说我的验证环境JDK 17Spring Boot 3.3.xSpring AI 2.0.x GA 版本Node.js 20确保 npx 可用Python 3.10这一步只在 SSE 模式实验时需要stdio 模式用官方 TS 版不需要 PythonSpring AI 2.x 和 1.x 在 MCP 上最大的区别是1.x 时代你还要手动引入一堆 MCP 适配器、在配置类里手动 new McpToolSpecification而 2.x 提供了更成熟的 starter 自动配置引入依赖后把连接信息写在 application.yml 里容器中会自动出现对应的工具回调对象。整体体验提升非常大。2.2 模型接口用 OpenAI 兼容配置其他模型只需要换 base-urlSpring AI 2 对模型供应商抽象得不错OpenAI、智谱、通义、DeepSeek 这些只要支持 OpenAI 兼容接口的改一下 base-url 和 api-key 就能用。我这次为了测试稳定直接用了 OpenAI 兼容配置spring: ai: openai: api-key: ${AI_API_KEY} base-url: ${AI_BASE_URL} chat: options: model: ${AI_MODEL:gpt-4o-mini}如果你是智谱 AI就把 base-url 换成智谱的兼容地址模型名换成 glm-4-flash 之类后面 MCP 逻辑完全不用动。2.3 先手动把 filesystem server 跑起来别急着写代码这一步非常关键很多同学一上来就写 Spring Boot 配置报错之后在 Java 侧查半天其实问题根本不在 Spring AI 那边而是 filesystem server 根本没拉起来。先用 npx 手动启动一下官方 TS 版npx -y modelcontextprotocol/server-filesystem /tmp/test-files启动之后你会看到类似 “MCP server started” 的日志说明 Node 环境没问题、包能正常下载。我习惯先建一个测试目录比如/tmp/test-files里面放一个readme.txt内容写上几行文字后面真调工具时能直接验证读取效果。如果这一步因为网络原因卡住说明 npx 下载包失败先去处理镜像或者手动安装再回来看 Spring 侧的报错。这一步做好能替你挡掉至少一半的“连接失败”问题。3. SSE 模式手把手Python 启动服务Spring AI 2 远程连接3.1 为什么我建议先测 SSE 模式stdio 模式虽然简单但它有个特点spring 应用启动时自动拉子进程子进程的日志、退出状态对新手来说比较黑盒一旦失败你很难判断是 Node 的问题还是 Spring 配置的问题。SSE 模式则相反filesystem server 是一个独立 HTTP 服务你可以先用 curl 验证服务活着再让 Spring AI 去连接问题隔离非常干净。所以我建议第一次跑 MCP 的同学先走一遍 SSE 模式建立信心再切换到 stdio 模式会更从容。3.2 启动一个 SSE 模式的 filesystem server官方 TS 版不支持 SSE我在这里用的是 Python 生态的mcp-server-filesystem包它基于 FastMCP 实现原生支持 SSE 传输方式pip install mcp-server-filesystem然后启动服务注意指定传输方式为 SSEmcp-server-fs --transport sse --host 127.0.0.1 --port 8000 /tmp/test-files如果你装的版本里命令名不是mcp-server-fs可以用python -m mcp_server_fs --help看一下不同版本的入口命令略有差异这个非常正常。启动后你会看到 FastMCP 打印出服务地址SSE 端点通常在http://127.0.0.1:8000/sse。先不要接着写 Spring 代码直接用 curl 验证一下服务还活着curl http://127.0.0.1:8000/sse如果连接保持不退出说明 HTTP 服务正常。这一步做到什么程度算成功看到 HTTP 200 或者连接挂在那里没有关闭就算活。如果端口被占用先处理端口再继续。3.3 Spring AI 2 的 SSE 客户端配置接下来在 Spring Boot 项目中引入 MCP 客户端依赖。我用的是 Spring AI 2.x 的 starterdependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency然后写 application.yml。这里提醒一句Spring AI 2.0 从 RC 到 GA 期间MCP 客户端配置的属性名做过调整有的版本用connections有的用servers。我会把两种常见写法都贴出来你以自己 IDE 里的自动提示为准不要死记某个 key 名。常见写法一spring: ai: mcp: client: sse: connections: - name: filesystem url: http://127.0.0.1:8000/sse常见写法二spring: ai: mcp: client: sse: servers: - name: filesystem url: http://127.0.0.1:8000/sse两个写法的本质都是告诉 Spring AI“有一个通过 SSE 连接的 MCP 服务名字叫 filesystem地址是这个 URL”。配置完成后启动应用如果连接成功你会看到类似 “MCP SSE client connected” 的日志。3.4 SSE 模式下最小可用代码工具注册的核心思路是把容器里的 MCP 工具对象塞进 ChatClient这样模型就能在对话过程中看到并使用这些工具。RestController public class FileAskController { private final ChatClient chatClient; public FileAskController(ChatClient.Builder builder, ListMcpToolSpecification mcpTools) { this.chatClient builder .defaultTools(mcpTools.toArray(new ToolCallback[0])) .build(); } GetMapping(/ask) public String ask(RequestParam String question) { return chatClient.prompt() .user(question) .call() .content(); } }这里有几个细节值得说明如果你的 Spring AI 版本注入的不是ListMcpToolSpecification而是ListToolCallbackProvider也不要慌注入后者然后在defaultTools里把 provider 传进去即可本质一样只是框架对你的自动装配方式不同。很多人一开始只想用“某一个” MCP 工具于是想把列表过滤一下。我的建议是先全部注册跑通之后再考虑过滤因为先看到调用日志再过滤你才知道留下哪些、去掉哪些。全部做完之后访问curl http://localhost:8080/ask?question列出/tmp/test-files目录下的文件并读取readme.txt内容如果一切正常模型会先调用 list_directory然后调用 read_file最后整理成一段自然语言回答给你。返回内容里会出现你放在 readme.txt 里的原文比如“Hello from MCP filesystem”。4. stdio 模式手把手npx 拉起子进程一条配置全自动4.1 stdio 模式的适用场景与心理预期SSE 模式跑通之后stdio 模式基本就是水到渠成。stdio 的好处是省掉了一个独立服务应用启动时自动用 npx 拉起官方 TS 版 filesystem server应用停止时子进程也会被回收。这在开发环境里体验非常好——你不用手动启动任何东西也不需要操心服务进程是不是忘关了。但你要有心理预期stdio 模式的日志观感会“乱”一些因为子进程的 stdout 与 MCP 协议共用同一条管道Spring 侧的日志里会混入一些看起来像乱码的内容这个正常不是故障。4.2 Spring AI 2 的 stdio 配置与代码复用还是用同样的spring-ai-starter-mcp-client依赖配置文件改成 stdio 类型spring: ai: mcp: client: stdio: servers: - name: filesystem command: npx args: - -y - modelcontextprotocol/server-filesystem - /tmp/test-files如果你在 Windows 环境下运行command 要改成npx.cmd否则会出现 ProcessBuilder 找不到命令的错误这个我在第 6 节还会细说。Controller 的代码和 SSE 模式完全一样一封配置切换传输方式、Java 侧零改动这就是 MCP 协议带给你的实际好处——传输层对业务代码完全透明。4.3 子进程生命周期谁启动、谁管理、谁回收很多人用 stdio 模式之后会有一个疑问这个 npx 进程是 Spring AI 拉起来的但它能活多久答案是由 Spring AI 的 MCP 客户端管理。应用启动时框架会根据配置创建子进程应用关闭时框架会发送关闭信号并回收进程。如果应用非正常 kill子进程可能会残留这时你可以在系统进程列表里搜server-filesystem关键词手动清理一次。你可能会遇到的一个小问题是npx 在首次使用某个包时需要下载这个下载过程会让子进程启动变慢Spring AI 在连接超时时间内可能判定为“启动失败”。解决办法是先手动执行一次npx -y modelcontextprotocol/server-filesystem等本地缓存放好再启动 Spring Boot 应用。4.4 跑一个真实提问验证 stdio 链路同样访问curl http://localhost:8080/ask?question帮我看看test-files目录里有什么文件如果配置没问题模型会调用list_directory工具参数是{path: /tmp/test-files}返回结果包含readme.txt。然后模型基于结果继续回答最终返回给你一段自然语言比如“目录下有一个文件 readme.txt”。这个时候 Spring AI 侧已经自动完成了一次完整的工具调用循环你全程不需要手动处理 function call 和工具返回值的拼接。5. 真调工具从日志到返回值看清楚每一次调用5.1 打开 MCP 调试日志比断点好用得多很多人在这一步卡住是因为“感觉”工具没被调用但看不到证据。实际上把日志级别调高之后整个调用链路会非常清晰。我一般会在 application.yml 里加上这一段logging: level: org.springframework.ai.mcp: DEBUG org.springframework.ai.chat: DEBUG这样启动时你能看到每个 MCP server 注册了哪些工具对话过程中也能看到模型请求了哪个工具、传了什么参数、拿到了什么结果。启动日志长这样简化版Registered MCP tool: list_directory(path) Registered MCP tool: read_file(path) Registered MCP tool: write_file(path, content)看到这几行说明工具已经成功进入模型视野。如果这里只有一个 ChatClient 的日志、看不到任何 MCP 相关注册信息优先回查配置的 url 是否正确、连接是否真的建立。5.2 观察一次完整的工具调用链当我提问“读取/tmp/test-files/readme.txt的内容”时DEBUG 日志里会出现类似这样的序列先是一次工具选择请求Tool call: read_file(path/tmp/test-files/readme.txt)紧接着是一次工具返回结果Tool result: Hello from MCP filesystem然后模型根据这个结果生成最终回答返回给前端接口。整个链路其实由三部分组成模型决定调什么工具、Spring AI 替模型执行工具调用、模型基于工具结果继续生成文本。Spring AI 2 帮你把中间这步封装好了所以你在业务代码里感知不到“工具参数解析”“返回值回填”这些动作但对排错来说看懂日志里的这三段非常重要。你可以做一个失败实验来加深理解故意问一个超出 filesystem 工具能力的问题比如“帮我把数据库连接池调大”看日志会发现模型压根没有发起任何工具调用而是直接给你一段“抱歉我无法操作”的回复。这就说明模型对工具边界是有感知的不是每个问题都会触发工具调用。5.3 流式输出时的工具调用表现为“先工具后文字”如果你把接口改成流式输出GetMapping(/ask/stream) public FluxString askStream(RequestParam String question) { return chatClient.prompt() .user(question) .stream() .content(); }你会观察到一个很有意思的现象如果模型需要调用工具流式响应一开始可能没有任何文字等工具调用完成之后才开始输出最终回答。所以在做前端流式渲染时要处理好“静默期”别让用户觉得页面卡死了配合 abort 逻辑对于长文件读取场景会更友好。这不是今天的主线但如果你已经把 MCP 跑通了值得留意。5.4 主动列出已注册工具确认连的是哪台 server有时候你同时配置了多个 MCP server或者配置了老版本的残留连接会发现工具名重复注册或注册数量不对。这时候我会直接写一个 Controller 接口把当前容器里所有工具列出来RestController public class ToolListController { private final ListMcpToolSpecification mcpTools; public ToolListController(ListMcpToolSpecification mcpTools) { this.mcpTools mcpTools; } GetMapping(/tools) public ListString tools() { return mcpTools.stream() .map(spec - spec.title() - spec.description()) .toList(); } }访问/tools你就知道当前应用到底注册了哪些 MCP 工具哪个 server 生效了。这招在多人协作、配置项混乱的时候尤其好用。6. 我踩过的坑stdout 污染、Windows 命令、端口残留与版本兼容6.1 大坑stdio 模式下子进程打日志会污染协议流这个问题我在官方 TS 版 filesystem server 上没踩到但在自定义 MCP server 上踩过而且特别隐蔽。stdio 模式下MCP 协议的数据是通过子进程的 stdout 传输的所以子进程任何“多余的打印”——比如console.log(server started)、某个库的日志输出——都会混进协议数据里导致 Spring 侧解析失败报错通常是消息格式错误或者 JSON 解析异常。排查思路很简单先在命令行手动启动子进程往 stdin 发一段 MCP 初始化 JSON看 stdout 返回的是不是干净的协议数据。如果发现 stdout 里混了日志就得改 server 代码把日志全部改到 stderr 或日志文件。千万不要把“能看到日志”当作“进程正常”在 stdio 模式下 stdout 是协议专用通道不是给你打日志的。6.2 Windows 下 npx 命令路径坑如果你在 Windows 上跑 stdio 模式command: npx经常会直接报CreateProcess error2, 系统找不到指定的文件。原因很简单Windows 下真正的可执行文件是npx.cmdProcessBuilder 默认找不到npx。解决办法就是在配置里写npx.cmdcommand: npx.cmd另外如果 args 里的路径带反斜杠和空格注意 YAML 的引号处理建议统一用正斜杠Java 侧解析 Windows 路径没问题。6.3 SSE 模式端口残留与僵尸服务SSE 模式跑的独立进程最烦的是应用重启之后端口还在被上一个实例占用。因为你是手动启动的mcp-server-fsSpring 应用退出时并不会回收它。如果你在开发机上反复重启应用过一段时间端口会越占越多。我的习惯是开发环境中尽量优先用 stdio 模式让 Spring AI 管理子进程生命周期只有在需要验证远程连接场景时才用 SSE 模式用完手动把那个 Python 进程 kill 掉。如果一定要用 SSE就在启动脚本里加一行先检查端口占用再确定要不要启动服务小心驶得万年船。6.4 版本兼容Spring AI 2.x 与 Spring Boot 的匹配关系最后说一个最容易让人心态爆炸的坑Spring AI 2 对 Spring Boot 的版本是有要求的。如果你用的 Spring Boot 是 3.2.x 而 Spring AI 是 2.0.x某些 MCP 自动配置类可能因为缺少某个依赖方法直接启动报错。我的建议是直接用 Spring Initializr 生成项目在依赖里勾选 Spring AI让脚手架帮你锁好兼容版本比手动改 pom 靠谱得多。如果必须手动引入至少确认一下 Spring AI 官方文档列出的版本矩阵。还有一点如果你之前用过 Spring AI 1.x 的 MCP 配置升级到 2.x 后旧的配置项名称可能失效代码里 import 的包路径也整体变了。别硬扛迁移直接在最新版项目里从零写一遍 MCP 配置通常比在旧项目里改来改去更快。6.5 模型不调工具不一定是集成的问题真调工具时最让人困惑的一种情况是工具注册日志明明有但模型就是不用。比如你问“读取文件内容”它偏要直接说“我无法直接访问本地文件”。这通常不是 MCP 链路的问题而是模型在提示词层面的保守选择。我的处理办法是在 system prompt 里明确写一句你有能力调用文件工具当用户询问文件相关操作时必须优先使用工具。改完之后模型就正常调了。你可以把它理解成给模型一个“使用工具授权”很多模型在没有明确指示时会倾向于不调用工具。7. 一个小经验把两种模式做进同一套配置按环境切换整套跑通之后我最后分享一个自己的使用习惯。我在项目的 application-dev.yml 里使用 stdio 模式开发时零负担在 application-prod.yml 里使用 SSE 模式接独立的 filesystem server。实现上只需要把 MCP 客户端配置放进不同 profile 的配置文件Java 侧完全不用动。无论是 SSE 还是 stdioSpring AI 2 在你面前呈现出来的都是统一的工具装配模型注入 McpToolSpecification、把工具交给 ChatClient、让模型在对话中自动决策调用。这套抽象让后续接数据库 MCP、GitHub MCP 变得几乎没有学习成本你这次花时间搞懂 filesystem 的过程可以直接平移到其他 MCP server。真调工具这件事本质上就三板斧服务端能跑、客户端能连、模型愿意用。按今天文章的顺序走一遍这三关应该都能顺利通过。
