1. 为什么你的 Spring AI 项目需要一个 MCP 工具调用链路
如果你正在用 Spring AI 做 AI 应用,大概率遇到过这个尴尬:模型能聊天,但一让它「读一下我桌面的文件」「查一下本机配置」,它就开始一本正经地胡说。原因很简单——大模型本身没有手脚,它只能生成文本,真正去执行文件读写、系统查询、调用外部接口的,得靠工具(Tool / Function Calling)。
MCP(Model Context Protocol)就是把这个「给模型装手脚」的过程标准化了。它定义了一套服务端和客户端之间的通信协议,服务端负责暴露工具能力,客户端负责把工具注册给模型,模型在对话中自主决定调哪个工具、传什么参数。Spring AI 从 1.0 版本开始对 MCP 做了比较完整的支持,提供了spring-ai-mcp-server-spring-boot-starter和spring-ai-mcp-client-webflux-spring-boot-starter两个核心依赖,让你用注解和配置就能把链路搭起来。
这篇文章面向的是想真正把 MCP 跑通的 Java 开发者。我会用一个本地 Java 项目做场景,拆成两条线:一条是「消费别人的 MCP 服务」(比如 filesystem 这种现成的),一条是「自己写一个 MCP 服务」(获取电脑配置),最后把两条线接在一起,做一次端到端验证。全程给出可复制的 pom、JSON 配置、@Tool注册代码和测试类,你跟着敲就能跑。
适合谁看:有 Spring Boot 基础、用过 Spring AI 的 ChatClient、但还没把 MCP 从概念落到工程里的人。如果你连 ChatClient 都没用过,建议先把官方 quickstart 跑一遍再回来,不然配置部分会有点懵。
先说清楚一个容易混淆的点:MCP 服务端和客户端是两个独立进程。服务端可以是 Java、Python、Node.js 写的,只要遵循 MCP 协议;客户端(你的 Spring AI 应用)通过 stdio 或 SSE 跟它通信。所以「搭建可复用链路」的本质,是把服务端的工具能力,通过一份配置,稳定地注入到客户端的 ChatClient 里。下面分步来。
2. TaoToken 前置准备:给 ChatClient 配一个稳定的模型入口
在写 MCP 代码之前,得先解决模型从哪来的问题。Spring AI 的 ChatClient 需要一个ChatModel实现,你可以接官方 OpenAI,也可以接兼容 OpenAI 协议的网关。我这边习惯用 TaoToken 做统一入口,原因是它同时提供 OpenAI 兼容接口和 Claude Code 这类编码场景的接入方式,切换模型只改一个 model 字段,不用动代码结构。
你需要准备三样东西,我把它叫做「三件套」,后面所有配置都围绕它展开:
- Base URL:
https://taotoken.net/api,这是 OpenAI 兼容协议的根地址,Spring AI 的OpenAiApi会往这个地址拼/v1/chat/completions。 - API Key:去控制台创建一个,格式通常是
sk-开头。创建入口在 API Keys,建议按项目建独立的 key,方便后面排查是哪个应用在调。 - Model ID:比如
gpt-4o、claude-sonnet-4-5这类。具体支持哪些,可以在 模型对话 页面直接试,确认能出结果再写进配置。
把这三个值写进application.yml,Spring AI 的 OpenAI starter 会自动读取:
spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: gpt-4o temperature: 0.7这里用${TAOTOKEN_API_KEY}从环境变量读,别把 key 硬编码进仓库。本地跑的时候在 IDE 的 Run Configuration 里加环境变量,或者用.env配合 spring-dotenv。
如果你更习惯用 Claude Code 做编码辅助,TaoToken 也提供了对应的接入方式,配置逻辑和上面一致,只是走的协议不同。文档在 接入文档,里面有 ClaudeCodeAnthropic 的完整参数说明。我建议先把 OpenAI 兼容这条线跑通,因为 Spring AI 对它的支持最成熟,MCP 的工具调用也是基于这条线验证的。
有一点要提醒:MCP 的工具调用会显著增加 token 消耗,因为每次对话都要把工具定义(name、description、参数 schema)塞进上下文。所以模型选择上,优先选支持 function calling 且上下文够大的。如果你只是本地测试,用便宜的小模型也能验证链路是否通,等确认没问题再换大模型。
3. 可复制配置:MCP 客户端与服务端的最小闭环
这一节是全文的核心,我会给出两份配置:一份是客户端引入外部 MCP 服务(filesystem),一份是服务端自己暴露工具(电脑配置)。两份配置的路径和字段都按实际项目写,你直接复制改路径即可。
3.1 客户端 pom 与 mcp-servers-config.json
先建一个 Spring Boot 项目,pom 里加两个依赖。注意 Spring AI 的版本要用 1.0.0 及以上,MCP 的 starter 才稳定:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency>然后在src/main/resources下建mcp-servers-config.json,内容如下。这个文件是 Spring AI 约定的 MCP 服务清单,客户端启动时会读取并建立连接:
{ "mcpServers": { "filesystem": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-filesystem", "/Users/root/Desktop", "/Users/root/Desktop" ] } } }逐字段解释一下,这几个参数配错了是最常见的坑:
filesystem:服务名称,客户端里用它区分不同 MCP 服务,日志里也会打这个名字。command: "npx":用 Node.js 的 npx 来拉起这个 npm 包。前提是你本机装了 Node.js,npx -v能出版本号。args里的-y:表示包不存在时自动同意安装,避免卡在交互式确认。- 后面两个路径:分别是允许读和允许写的目录。Windows 用户必须写绝对路径,比如
D:\\workspace\\demo,而且 JSON 里反斜杠要转义成\\。Mac/Linux 用/Users/xxx/Desktop这种。
如果你不想用 filesystem,想找更多现成的 MCP 服务,可以去 smithery.ai 或 glama.ai/mcp/servers 逛,上面有大量社区维护的服务端,覆盖数据库、浏览器、Git 等场景。选的时候注意看它支持的传输方式(stdio 还是 SSE),stdio 的配置就是上面这种 command + args 的形式。
3.2 服务端 pom 与 @Tool 注册
现在写自己的 MCP 服务端。新建一个 Spring Boot 项目,pom 加:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId> </dependency>服务实现类用@Tool注解暴露方法。这个注解是 Spring AI 识别工具的关键,description 会作为工具描述传给模型,模型靠它判断什么时候调用:
@Slf4j @Service public class ComputerService { @Tool(description = "获取电脑配置") public ComputerFunctionResponse queryConfig(ComputerFunctionRequest request) { log.info("获取电脑配置信息 {}", request.getComputer()); Properties properties = System.getProperties(); String osName = properties.getProperty("os.name"); String osVersion = properties.getProperty("os.version"); String osArch = properties.getProperty("os.arch"); String userName = properties.getProperty("user.name"); String userHome = properties.getProperty("user.home"); String userDir = properties.getProperty("user.dir"); String javaVersion = properties.getProperty("java.version"); ComputerFunctionResponse response = new ComputerFunctionResponse(); response.setOsName(osName); response.setOsVersion(osVersion); response.setOsArch(osArch); response.setUserName(userName); response.setUserHome(userHome); response.setUserDir(userDir); response.setJavaVersion(javaVersion); return response; } }然后在启动类里把工具注册成ToolCallbackProvider,这一步是把@Tool方法真正挂到 MCP 服务端的工具列表上:
@Slf4j @SpringBootApplication public class McpServerComputerApplication implements CommandLineRunner { public static void main(String[] args) { SpringApplication.run(McpServerComputerApplication.class, args); } @Bean public ToolCallbackProvider computerTools(ComputerService computerService) { return MethodToolCallbackProvider.builder() .toolObjects(computerService) .build(); } @Override public void run(String... args) { log.info("mcp server computer success!"); } }toolObjects可以传多个 service,比如ComputerService、FileService一起塞进去,它们的所有@Tool方法都会暴露。打包用mvn clean package,产物是target/mcp-server-computer-1.0.0.jar。
3.3 把自建服务接回客户端
服务端 jar 打出来后,回到客户端的mcp-servers-config.json,追加一段:
{ "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/root/Desktop", "/Users/root/Desktop"] }, "mcp-server-computer": { "command": "java", "args": [ "-Dspring.ai.mcp.server.stdio=true", "-jar", "/Users/fuzhengwei/Applications/apache-maven-3.8.4/repository/cn/bugstack/mcp/mcp-server-computer/1.0.0/mcp-server-computer-1.0.0.jar" ] } } }关键参数是-Dspring.ai.mcp.server.stdio=true,它告诉服务端用 stdio 传输,而不是默认的 SSE。客户端通过标准输入输出跟这个 jar 进程通信,所以 jar 路径必须是绝对路径,且文件真实存在。到这里,客户端同时挂了两个 MCP 服务:filesystem 负责文件操作,mcp-server-computer 负责系统信息查询。
4. 验证请求:一次端到端跑通工具调用
配置写完,得验证链路真的通了。写一个测试类,注入ChatClient.Builder和ToolCallbackProvider,把工具挂到 ChatClient 上:
@Slf4j @SpringBootTest public class MCPTest { @Resource private ChatClient.Builder chatClientBuilder; @Autowired private ToolCallbackProvider tools; @Test public void test_tool() { String userInput = "有哪些工具可以使用"; var chatClient = chatClientBuilder .defaultTools(tools) .defaultOptions(OpenAiChatOptions.builder() .model("gpt-4o") .build()) .build(); System.out.println(">>> QUESTION: " + userInput); System.out.println(">>> ASSISTANT: " + chatClient.prompt(userInput).call().content()); } }先跑test_tool,问模型「有哪些工具可以使用」。如果链路正常,模型会列出 filesystem 和 computer 相关的工具名。这一步验证的是工具注册是否成功——如果模型说「我没有工具」,说明defaultTools(tools)没生效,或者 MCP 服务没连上。
接着跑工作流测试,让模型组合两个工具完成一个任务:
@Test public void test_workflow() { String userInput = "获取电脑配置,在 /Users/root/Desktop 文件夹下创建 电脑.txt,把电脑配置写入 电脑.txt"; var chatClient = chatClientBuilder .defaultTools(tools) .defaultOptions(OpenAiChatOptions.builder() .model("gpt-4o") .build()) .build(); System.out.println(">>> QUESTION: " + userInput); System.out.println(">>> ASSISTANT: " + chatClient.prompt(userInput).call().content()); }这个请求会触发多轮工具调用:模型先调queryConfig拿到系统信息,再调 filesystem 的写文件工具创建电脑.txt并写入内容。跑完后去桌面看,文件应该真实存在,内容就是本机的 OS、Java 版本等信息。控制台日志里能看到获取电脑配置信息这行,说明服务端被真正调用了。
实测下来,第一次跑最容易卡在 npx 下载 filesystem 包上,网络慢的话会等十几秒。如果超时,可以提前手动npx -y @modelcontextprotocol/server-filesystem跑一次把包缓存下来。另外,模型有时候会「偷懒」,不调工具直接编答案,这时候把 temperature 调低一点,或者在 prompt 里明确说「必须调用工具获取真实数据」,命中率会高很多。
5. 本篇常见错排查:401、local proxy failed 与 reading choices
MCP 链路涉及进程间通信和模型调用,报错点比较分散。我把几个高频错误和对应解法列出来,你对照日志定位。
401 Unauthorized:模型调用被拒。先检查spring.ai.openai.api-key是否读到了环境变量,echo $TAOTOKEN_API_KEY确认非空。再确认 base-url 是https://taotoken.net/api,末尾不要多加/v1,Spring AI 会自己拼。如果 key 没问题还是 401,去 API Keys 看这个 key 是否被禁用或额度耗尽。
local proxy failed / Connection refused:客户端连不上 MCP 服务端。stdio 模式下,检查command指向的可执行文件在 PATH 里,npx -v、java -version都能正常输出。jar 路径写错是最常见的,日志里会明确说Cannot run program。Windows 用户特别注意 JSON 里的路径转义,D:\a\b.jar要写成D:\\a\\b.jar。
reading choices 相关报错:通常是模型返回体解析失败。可能是模型 ID 写错,或者该模型不支持 function calling。换一个明确支持工具调用的模型再试。也有可能是响应被截断,检查max-tokens是否设得太小。
OAuth / 认证失败:如果你接的是需要 OAuth 的 MCP 服务(部分远程 SSE 服务),需要在配置里加 token 字段。stdio 的本地服务一般不需要。遇到这类报错,先确认服务端的认证方式,别盲目加参数。
工具被调用但结果为空:检查@Tool方法的返回对象是否可序列化,字段有没有 getter。Spring AI 会把返回值转成 JSON 塞回模型,如果序列化失败,模型收到的是空对象,就会说「没拿到数据」。
排查顺序建议:先看模型调用是否通(401 类),再看 MCP 进程是否起来(proxy 类),最后看工具执行结果(序列化类)。日志级别调到 DEBUG,logging.level.org.springframework.ai=DEBUG,能看到完整的请求和响应体,定位快很多。
6. 把链路变成可复用资产:从单次验证到长期编码
跑通一次不代表能长期用。要让 MCP 链路变成可复用的资产,有几个工程化动作值得做。
第一,把mcp-servers-config.json里的路径参数外置成环境变量。Spring AI 支持在 JSON 里用${VAR}占位,这样同一份配置在 Mac、Windows、CI 上都能用,不用改文件。第二,给每个 MCP 服务加健康检查,客户端启动时如果某个服务连不上,日志要明确报出来,而不是静默失败——静默失败会导致模型「假装」有工具,排查起来很痛苦。第三,工具描述(@Tool的 description)要写清楚输入输出,模型靠它决策,描述模糊会导致误调用。
如果你打算把 MCP 用在日常编码或 Agent 场景,比如让模型自动读代码库、跑测试、查文档,那模型调用量会比较大,建议用 Coding Plan 这类面向长期编码的套餐,比按次调用划算。配置方式跟前面一样,只是 key 和 model 换成套餐对应的值。
最后说个我踩过的坑:MCP 服务端的 jar 更新后,客户端不会自动重连,得重启客户端进程。所以本地开发时,改完服务端代码重新打包后,记得把客户端也重启一遍,否则你调的还是旧工具。这个坑不报错,只是行为不符合预期,很容易怀疑人生。
链路搭好之后,你可以继续往上叠:把 filesystem 换成数据库 MCP,让模型查真实数据;或者把多个自建服务组合成一个工具集,做成团队内部的 MCP 网关。核心结构不变——服务端暴露@Tool,客户端用 JSON 挂载,ChatClient 用defaultTools注入。把这三步固化成模板,下一个 MCP 服务接入就是十分钟的事。