1. 为什么要在 Spring AI 里折腾 MCP Streamable HTTP
如果你正在用 Spring AI 做 MCP 服务端,大概率踩过这样的坑:1.0.x 版本只支持 SSE 协议,客户端一断线就得重建连接,上下文全丢,用户问了一半的问题得从头再来。更难受的是每个客户端都要挂一条 SSE 长连接,并发一上来服务器 TCP 连接数直接飙红,横向扩容也麻烦。
MCP 规范后来把默认传输方式切到了 Streamable HTTP,Spring AI 从 1.1.0-M1 开始跟进,到 1.1.0-M3 已经能比较顺手地用了。它保留了 SSE 的流式推送能力,同时把通信整合到统一端点,支持会话状态管理和断线重连,服务器不用再为每个客户端维持长连接。对用 WebFlux 或 WebMVC 的 Java 开发者来说,这意味着你可以用熟悉的 Spring Boot 那套东西,把 MCP 服务端和客户端都跑起来。
这篇是番外篇,重点不在讲原理,而是把 MCP Streamable HTTP 模式接入 TaoToken 统一 Key 的完整配置走一遍。我会给出 application.yml、config.toml、settings.json 三份骨架,再附上启动验证和请求连通性检查的步骤。适合已经写过 Spring Boot、想快速把 MCP 服务端到客户端联调跑通的人。TaoToken 在这里的角色是统一 API 通道,你只需要维护一个 Key,就能让 MCP 客户端背后的模型调用走同一条路,省得在多个平台之间来回切配置。
2. TaoToken 前置准备:统一 Key 与 API 通道
在动手改代码之前,先把 TaoToken 这边的准备工作做完。它的官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api ,注意 API 地址不带 UTM 参数,配置里填这个就行。
你需要拿到一个统一 Key。登录后进控制台,在 API Keys 页面创建一个,复制出来先存好。这个 Key 后面会同时出现在 MCP 客户端的 settings.json 和 Spring AI 的 application.yml 里,所以别弄丢。如果你还没建过 Key,直接去 https://taotoken.net/console/api-keys 操作,创建时给个容易认的名字,比如 spring-ai-mcp。
TaoToken 的定位是统一 API 通道,不是让你替换掉 Spring AI 或 MCP 本身,而是把模型调用的出口收敛到一处。MCP 服务端负责暴露工具,MCP 客户端负责调用工具并驱动模型,模型这一层的请求就走 TaoToken。这样你在 config.toml 和 settings.json 里配一次,后面换模型或者加通道都不用改业务代码。
有一点要提醒:TaoToken 不是编辑器插件,也不是 MCP 直连生产库的工具。它只处理 API 通道这一层,你的数据库连接、业务逻辑还是在自己代码里。配置的时候把 Key 放在环境变量或本地配置文件里,别硬编码进 Git 仓库。
3. 可复制配置:application.yml、config.toml、settings.json 三件套
先看 MCP 服务端。父项目里引入 spring-ai-bom,版本用 1.1.0-M3,这样依赖版本统一,不会出现 starter 和核心包对不上的情况。
<!-- extra01/pom.xml --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.1.0-M3</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>服务端模块 extra-mcp-server 引入 WebMVC 版的 MCP starter。如果你用 WebFlux,把 artifactId 换成 spring-ai-starter-mcp-server-webflux 即可,配置项基本一致。
<!-- extra01/extra-mcp-server/pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> </dependency>写一个简单的 Service,用 @Tool 注解暴露工具。这里模拟通过城市名字获取温度,实际项目里换成你的业务方法就行。
// extra01/extra-mcp-server/src/main/java/com/kaifamiao/extra01/service/WeatherService.java @Service public class WeatherService { @Tool(description = "通过城市名字获取温度") public String getWeatherByCity(@ToolParam(description = "城市名称") String cityName) { return cityName + "今天的温度是" + (new java.util.Random().nextInt(9) + 1) * 6; } }注册工具的时候加 @Primary,避免多个 ToolCallbackProvider 冲突。
// extra01/extra-mcp-server/src/main/java/com/kaifamiao/extra01/configuration/McpServerConfig.java @Configuration public class McpServerConfig { @Bean @Primary public ToolCallbackProvider toolProvider(WeatherService weatherService) { return MethodToolCallbackProvider.builder().toolObjects(weatherService).build(); } }服务端的 application.yml 是核心。protocol 填 streamable,type 填 sync,streamable-http 下面指定 mcp-endpoint 和 keep-alive-interval。端口我用了 8081,你按自己环境改。
# extra01/extra-mcp-server/src/main/resources/application.yml spring: ai: mcp: server: name: streamable-weather-server version: 0.0.1 protocol: streamable type: sync streamable-http: mcp-endpoint: /mcp keep-alive-interval: 30s server: port: 8081在 IDEA 里敲 protocol 的时候,如果补全能提示 streamable,说明依赖版本对了。1.0.0 版本是没有这个选项的,这也是判断版本是否升级成功的一个小技巧。
客户端这边,extra-mcp-client 引入 dashscope starter 和 mcp-client starter。dashscope 负责模型调用,mcp-client 负责连服务端。
<!-- extra01/extra-mcp-client/pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>客户端的 application.yml 里,mcp.client.streamable-http.connections 下面配服务端 URL。这里我把 dashscope 的 api-key 用环境变量占位,实际值从 TaoToken 那边拿。
# extra01/extra-mcp-client/src/main/resources/application.yml spring: ai: dashscope: api-key: ${TAOTOKEN_API_KEY} chat: options: model: qwen-plus temperature: 0.7 mcp: client: streamable-http: connections: server1: url: http://127.0.0.1:8081/mcp如果你用的是 Claude Code 或类似客户端,settings.json 的骨架长这样。把 base_url 指向 TaoToken 的 API 入口,api_key 填你的统一 Key。
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "你的统一Key" }, "mcpServers": { "streamable-weather": { "url": "http://127.0.0.1:8081/mcp" } } }config.toml 这边,如果你用支持 TOML 配置的客户端,结构类似,重点是 base_url 和 api_key 两项。
# config.toml [api] base_url = "https://taotoken.net/api" api_key = "你的统一Key" [mcp.servers.streamable-weather] url = "http://127.0.0.1:8081/mcp"三份配置里,TaoToken 的 Key 只出现一次,其他客户端都引用同一个值。这就是统一 Key 的好处,改一处全生效。
4. 启动验证与请求连通性检查
配置写完,先启动服务端。在 extra-mcp-server 目录下执行:
mvn spring-boot:run看到日志里出现 MCP server started on /mcp 之类的字样,说明服务端起来了。如果端口被占用,改 application.yml 里的 server.port 再试。
服务端起来后,先用 curl 探一下端点是否可达。Streamable HTTP 模式下,MCP 端点接受 POST 请求,返回可能是 JSON 也可能是 SSE 流。
curl -i -X POST http://127.0.0.1:8081/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl-test","version":"1.0"}}}'如果返回 200 并且 body 里有 serverInfo 或 capabilities 字段,说明服务端正常。返回 404 的话检查 mcp-endpoint 是不是 /mcp,返回 415 检查 Content-Type 头。
接着跑客户端的测试用例。这个测试注入 ChatClient.Builder 和 SyncMcpToolCallbackProvider,让模型带着 MCP 工具回调去回答天气问题。
// extra01/extra-mcp-client/src/test/java/com/kaifamiao/extra01/StreamableWeatherServerTest.java @SpringBootTest @Slf4j public class StreamableWeatherServerTest { @Test void testMcpServer(@Autowired ChatClient.Builder chatClientBuilder, @Autowired SyncMcpToolCallbackProvider syncMcpToolCallbackProvider) { ChatClient chatClient = chatClientBuilder.build(); String response = chatClient.prompt() .toolCallbacks(syncMcpToolCallbackProvider) .user("北京现在的天气如何") .call() .content(); log.info("response: {}", response); } }运行测试前,确保环境变量 TAOTOKEN_API_KEY 已经设置。Linux 或 macOS 下:
export TAOTOKEN_API_KEY="你的统一Key" mvn test -Dtest=StreamableWeatherServerTest控制台如果输出类似「北京今天的温度是36℃。请注意防暑降温!」的内容,说明整条链路通了:客户端连上 MCP 服务端,模型通过 TaoToken 通道调用,工具回调正常执行。如果输出里没有温度数字,可能是模型没触发工具调用,检查 @Tool 的 description 是否清晰,或者把 temperature 调低一点让模型更倾向走工具。
想单独验证模型通道是否通,可以打开模型对话页面发一条简单消息,确认 Key 和 base_url 没问题。这一步能帮你把「模型通道问题」和「MCP 连接问题」分开排查。
5. 本篇常见错排查
启动报 protocol 不识别。多半是 spring-ai-bom 版本没到 1.1.0-M1 以上。检查父项目 dependencyManagement 里的版本号,1.0.x 是不支持 streamable 的。改完记得 mvn clean,让依赖重新解析。
客户端连不上服务端,报 Connection refused。先确认服务端端口和 URL 里的端口一致,再看服务端是否真的启动完成。Streamable HTTP 模式下,客户端连的是 /mcp 这个统一端点,不是以前的 /sse。如果你从旧配置迁移过来,把 url 里的 /sse 改成 /mcp。
工具回调不触发,模型直接编答案。这是最常见的问题。检查三点:Service 类是否被 Spring 扫描到,McpServerConfig 里的 @Primary 是否加上,@Tool 的 description 是否写清楚。description 太模糊,模型不知道什么时候该调这个工具。另外客户端测试里要确保 .toolCallbacks(syncMcpToolCallbackProvider) 这行没漏。
TaoToken Key 报 401 或 403。先确认 Key 是从控制台复制的完整字符串,没有多余空格。再确认 base_url 填的是 https://taotoken.net/api ,不要带 UTM 参数。如果用的是环境变量,检查 export 是否在当前 shell 生效,IDEA 里跑测试的话要在 Run Configuration 里单独配环境变量。
keep-alive-interval 设了但连接还是断。Streamable HTTP 的保活机制依赖服务端和客户端都支持。如果中间有反向代理,检查代理的超时时间是否小于 keep-alive-interval。一般把 keep-alive-interval 设成 30s,代理超时设成 60s 以上比较稳。
WebFlux 和 WebMVC 混用导致冲突。服务端和客户端可以用不同的编程模型,但同一个模块里别同时引 web 和 webflux starter。如果你服务端用 WebMVC,客户端也用 WebMVC,那就都引 spring-boot-starter-web。要换 WebFlux 的话,把 starter 换成对应的 webflux 版本,配置项不用大改。
6. 接入之后怎么继续往下走
把上面这套跑通之后,你手里就有了一个能用的 MCP Streamable HTTP 服务端和客户端,模型调用统一走 TaoToken。接下来可以做的事:把 WeatherService 换成你真实的业务工具,比如查订单、查库存、调内部 API;在客户端加更多 MCP 连接,让一个模型同时挂多个工具服务;或者把配置抽到配置中心,让 Key 和 URL 支持动态刷新。
如果你在排障阶段卡住了,优先去看接入文档,里面有针对 Streamable HTTP 的端点说明和错误码解释。验证模型通道是否正常,直接开模型对话发一条消息最快。要是你打算长期跑编码类或 Agent 类任务,Coding Plan 那边有更完整的通道配置建议,适合把 MCP 和日常开发流串起来。
我自己的习惯是,每加一个新 MCP 工具,先用 curl 打一次 initialize,确认服务端活着,再跑客户端测试。这样出问题的时候,能立刻判断是服务端没起来还是客户端配置错了,省得两边瞎猜。