1. 为什么要在 Spring AI 里接入 MCP:一个真实的后端痛点
如果你正在用 Spring Boot 写业务系统,最近又被要求“给系统加个 AI 助手”,大概率会卡在同一个地方:模型能聊天,但拿不到你系统里的真实数据。比如用户问“帮我查一下订单 A123 的物流状态”,模型只能礼貌地回复“我无法访问实时数据”。这不是模型不行,而是它缺少一个标准化的工具调用通道。
MCP(Model Context Protocol,模型上下文协议)就是来解决这件事的。它由 Anthropic 在 2024 年底提出,本质是一套开源的通信标准,规定了 AI 应用(Host)和外部工具服务(Server)之间怎么交换上下文。你可以把它理解成“大模型的 USB-C 接口”:以前每个工具都要为不同模型写一套适配,现在只要工具实现了 MCP Server,任何支持 MCP 的客户端都能即插即用。
那 Spring AI 在这里扮演什么角色?Spring AI 是 Spring 官方推出的 AI 应用开发框架,它把 MCP 的 Client 和 Server 能力都封装成了 Starter。也就是说,你不需要手写 JSON-RPC 的握手、能力协商、工具列表拉取,只要加依赖、写配置、打上@Tool注解,一个能被大模型调用的工具服务就成型了。这对 Java 后端来说门槛非常低,因为整套东西还是你熟悉的 Spring 那套 Bean、配置、注解。
但工程落地时还有第二个坑:模型通道。MCP 解决的是“工具怎么被调用”,可模型本身从哪来、Key 怎么管、多个项目怎么统一计费和切换,这些 MCP 不管。我试过在几个 Spring AI 项目里各配一套模型 Key,结果就是密钥散落、模型版本不一致、换模型要改一堆 yml。所以这篇会把两件事串起来讲:用 Spring AI 落地 MCP 的完整流程,以及用 TaoToken 统一模型通道,让 MCP Client 侧的 LLM 配置收敛成一份可复制的片段。
适合谁看?有 Spring Boot 基础、想给现有系统加 AI 工具调用能力的后端;正在评估 MCP 工程化方案、需要可复制配置的架构同学;以及被多项目模型 Key 管理折磨过的人。下面从原理快速过一遍,重点放在能直接抄的配置和验证步骤上。
2. MCP Server 原理与 Spring AI 的 Tool 回调机制
先把原理讲清楚,不然后面配置里那些type: SYNC、toolcallback.enabled你会不知道在调什么。
MCP 采用 C/S 架构,角色分四个:Host 是接收用户提问、和大模型交互的主机(比如你的 Spring AI 应用、Cline、Cherry Studio);MCP Client 负责按 MCP 协议和 Server 通信,通常内置在 Host 里;MCP Server 是提供具体能力的轻量程序;再往外是本地数据源或远程服务。一次完整调用是这样的:用户提问 → Host 把问题和可用工具清单给 LLM → LLM 决定用哪个工具、给什么参数 → Host 启动 MCP Client → Client 按 MCP 协议请求 Server → Server 访问数据源返回结果 → 结果回传 Host → Host 连同上下文再给 LLM → LLM 整理成最终答案。
这里有个容易被误解的点:真正执行动作的是 Host,不是 LLM。LLM 只负责“决策用哪个工具”,编排是 Host 干的。而且 MCP 协议只规定 Client 和 Server 之间怎么交互,跟 LLM 怎么交互无关。抓包会发现,很多 Host 是把工具说明书直接塞进 system prompt 里发给模型的,长度可能到几万字符。这也解释了为什么工具描述(description)写得好不好,直接影响模型选工具的准确率。
传输机制目前主流两种:Stdio 和 HTTP with SSE,消息格式都是 JSON-RPC。Stdio 下 Client 把 Server 当子进程启动,通过标准输入输出通信,适合本地工具、IDE 插件,单客户端、低延迟,但要求 Server 绝对不能往 stdout 写非协议数据,否则解析直接崩。SSE 下 Server 是独立进程,提供 SSE 端点做服务端推送、HTTP POST 端点做客户端上行,支持多客户端、可远程部署,代价是要处理网络开销和安全措施。选哪个看场景:本地单机调试用 Stdio 最省事,要中心化部署给多个客户端用就上 SSE。
再说 Spring AI 的 Tool 回调机制。核心是@Tool注解,把它打在一个方法上,这个方法就成了可被 LLM 调用的工具,注解里的description会被传给模型,所以一定要写清楚用途和参数格式。光有注解还不够,得通过ToolCallbackProvider把这些方法注册进去,Spring AI 提供MethodToolCallbackProvider来扫描带@Tool的对象。注册完成后,MCP Server 启动时会把这些工具的能力清单暴露出去,Client 拉取后交给 LLM 决策。整个链路里,Spring AI 帮你屏蔽了 JSON-RPC 的序列化和握手细节,你只管写业务方法。
理解了这层,后面的配置就不是死记硬背了:Server 侧配的是“我用什么传输方式、叫什么名字”,Client 侧配的是“我去连哪个 Server、用哪个模型”。
3. 可复制配置:application.yml 与 MCP Client 接入片段
这一节是重点,配置直接给全,路径和字段保持和 Spring AI 一致,你复制后改路径和 Key 就能跑。环境基线:JDK 17、Spring Boot 3.4.x、Spring AI 1.0.0-M7(版本可按需换),构建工具 Maven。
先看 MCP Server 侧依赖,三选一。Stdio 用spring-ai-starter-mcp-server;SSE 基于 Spring MVC 用spring-ai-starter-mcp-server-webmvc,基于 WebFlux 用spring-ai-starter-mcp-server-webflux。Stdio 的 Server 配置如下:
spring: main: banner-mode: off ai: mcp: server: name: mcp-server version: 1.0.0 type: SYNC logging: pattern: console: level: root: off注意banner-mode: off和root: off这两项,Stdio 模式下必须关,否则 Spring Boot 启动横幅和日志会写进 stdout,Client 解析协议时直接报错。SSE 模式则不需要关日志,配置改成:
spring: ai: mcp: server: name: mcp-server version: 1.0.0 type: SYNC sse-message-endpoint: /mcp/messages工具方法用@Tool声明,再注册成 Bean:
@Service public class DateTimeTools { @Tool(description = "获取当前时间") String getCurrentDateTime() { return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString(); } @Tool(description = "设置闹钟,需要提供ISO-8601格式的时间") void setAlarm(String time) { LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME); } } @Configuration public class ToolsConfig { @Bean public ToolCallbackProvider tools(DateTimeTools dateTimeTools) { return MethodToolCallbackProvider.builder().toolObjects(dateTimeTools).build(); } }Stdio 模式下记得把工具方法里的System.out.println注释掉,任何标准输出都会污染协议流。
再看 MCP Client 侧。依赖除了spring-ai-starter-mcp-client(或spring-ai-starter-mcp-client-webflux),还要加模型依赖spring-ai-starter-model-openai和spring-boot-starter-web,少了 web 依赖启动会报错。Stdio 连接配置:
server: port: 8081 spring: ai: mcp: client: toolcallback: enabled: true stdio: root-change-notification: true connections: server1: command: java args: - -jar - /your/path/mcp-server-0.0.1-SNAPSHOT.jarSSE 连接配置换成:
spring: ai: mcp: client: toolcallback: enabled: true sse: connections: server1: url: http://localhost:8080然后是模型通道,这里用 TaoToken 统一接入。TaoToken 提供统一的 Key 和 API 通道,兼容 OpenAI 协议,所以 Spring AI 的 OpenAI Starter 直接指向它即可,Base URL 填https://taotoken.net/api,Key 在控制台生成:
spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: gpt-4o-mini把 Key 放环境变量而不是硬编码,多项目共用一份通道,换模型只改model一行。模型 ID 要和你账号下可用的保持一致,不确定就去模型对话页确认。这样 MCP Client 侧的三件套就齐了:Base URL、Key、Model ID。
4. 本地启动验证:从 MCP 工具调用到成功返回
配置写完,跑起来验证。分两条路,Stdio 和 SSE 各走一遍,你按自己选的传输方式对照。
先验证 MCP Server 本身。Stdio 模式下不需要手动启动 Server,Client 会自己拉起子进程,但你要先用 Maven 把 Server 打成 jar:mvn clean package -DskipTests,产物在target/下。SSE 模式则要先手动启动 Server,java -jar mcp-server-0.0.1-SNAPSHOT.jar,看到端口监听日志即可。
接着写 Client 的 ChatClient 和 Controller:
@Configuration public class ChatClientConfig { @Bean public ChatClient initChatClient(ChatClient.Builder builder, ToolCallbackProvider mcpTools) { return builder.defaultTools(mcpTools).build(); } } @RestController public class DateTimeController { @Autowired private ChatClient chatClient; @GetMapping("/chat") public String chat(String input) { return chatClient.prompt().user(input).call().content(); } @GetMapping("/chat/stream") public Flux<String> streamChat(HttpServletResponse response, String input) { response.setCharacterEncoding("UTF-8"); return chatClient.prompt().user(input).stream().content(); } }启动 Client 服务,Stdio 模式下观察日志,应该能看到它拉起 Server 子进程并完成工具列表拉取。然后在浏览器访问:
http://localhost:8081/chat?input=现在几点了,顺便帮我设一个明天早上8点的闹钟预期结果是模型先调用getCurrentDateTime拿到当前时间,再调用setAlarm传入 ISO-8601 格式的明天 8 点。返回内容里会包含当前时间,并且 Server 侧(SSE 模式)控制台能看到工具被调用的日志。如果走 SSE,启动 Client 后 Server 控制台会打印客户端连接信息,再发同样的请求,同样能看到工具调用记录。
流式接口/chat/stream用来验证逐字返回,浏览器里能看到内容分段吐出。这一步成功,说明整条链路通了:Client 拿到工具清单 → 交给模型决策 → 模型返回工具名和参数 → Client 通过 MCP 协议请求 Server → Server 执行并返回 → 模型整理成自然语言。
验证时建议先问一个只触发单个工具的问题,比如“现在几点”,确认基础调用没问题,再问组合问题。这样出问题时容易定位是工具注册、传输还是模型决策的环节。
5. 常见报错排查:401、local proxy failed 与 reading choices
跑不通是常态,这一节按真实报错对照排查,都是我在接入过程中踩过的。
401 Unauthorized。出现在 Client 调用模型时,说明 Key 或 Base URL 有问题。先确认spring.ai.openai.api-key是否真的读到了环境变量,Spring 里${TAOTOKEN_API_KEY}如果环境变量没设会直接解析失败或传空。再确认base-url是https://taotoken.net/api,注意结尾不要多加/v1之类的路径,OpenAI Starter 会自己拼。如果 Key 是从控制台复制的,检查有没有多余空格。401 基本就是这两处,跟 MCP 无关。
local proxy failed / connection refused。这个多出现在 SSE 模式,Client 连不上 Server。先确认 Server 真的启动了、端口对得上,url: http://localhost:8080里的端口要和 Server 的server.port一致。如果 Server 和 Client 不在同一台机器,localhost 要换成实际地址,同时确认防火墙放行。Stdio 模式下如果报子进程启动失败,检查args里的 jar 路径是不是绝对路径、文件是否存在,相对路径在不同工作目录下会找不到。
Error reading choices / 解析响应失败。这个报错通常意味着模型返回的 JSON 结构不符合 OpenAI 协议预期。常见原因是 Base URL 指向了一个不兼容 OpenAI 格式的端点,或者模型 ID 填错导致返回了错误结构。确认model字段是你账号下真实可用的模型 ID,去模型对话页核对。另外如果用了自定义的 HTTP 客户端或拦截器改写了响应体,也会触发这个错,先去掉自定义逻辑用默认配置验证。
Stdio 模式启动即崩、协议解析错误。九成是 Server 往 stdout 写了非协议数据。检查banner-mode: off和root: off是否生效,工具方法里有没有残留的System.out.println,第三方库有没有往控制台打印。Stdio 下 stdout 是协议专用通道,任何多余输出都是致命的。
工具没被调用、模型直接回答。这不是报错但很常见。原因通常是@Tool的 description 写得太模糊,模型不知道什么时候该用;或者toolcallback.enabled没开;或者ToolCallbackProvider没注册进 ChatClient 的defaultTools。先确认配置项,再把 description 写具体,比如“获取当前系统时间,无需参数”比“获取时间”更好。
排查顺序建议:先确认模型通道通(单独发个不带工具的请求),再确认 MCP 连接通(看工具列表是否拉取成功),最后确认工具被调用(看 Server 日志)。分层定位比一股脑改配置快得多。
6. 把模型通道收敛成一份配置:TaoToken 接入与后续扩展
MCP 的工程价值在于它把工具调用标准化了,但一个完整的 AI 应用还需要稳定的模型通道。把这两件事分开看:MCP 管工具,TaoToken 管模型,各司其职,配置就不会互相纠缠。
TaoToken 在这里的作用是统一 Key 和 API 通道。你可以在控制台生成 Key,所有 Spring AI 项目共用同一个 Base URLhttps://taotoken.net/api,换模型只改model字段,不用动 Key 和地址。对于多项目、多环境的团队来说,这比每个项目各配一套模型密钥要省心得多。Key 建议放环境变量或配置中心,别提交到仓库。
如果你后续要长期做编码类或 Agent 类应用,可以了解下 Coding Plan,它面向的就是这类持续调用场景。需要管理多个 Key 或查看用量,去 API Keys 页面;想先确认某个模型 ID 是否可用,直接在模型对话里试一句最快。接入过程中遇到协议或配置问题,接入文档里有更细的字段说明。
回到工程本身,MCP 目前还在快速演进,框架版本、传输机制、工具描述规范都可能有变化。落地时建议把 MCP Server 和业务系统解耦,Server 只暴露工具能力,业务逻辑还是留在原有服务里,这样协议升级时影响面可控。另外工具 description 要当成接口文档来写,它是模型决策的唯一依据,写得好坏直接决定调用准确率。我自己的习惯是每加一个工具,先用自然语言问一遍模型,看它选不选得对,选错了就回去改 description,比读文档管用。