1. 从 STDIO/SSE 迁移到 Streamable HTTP,到底解决了什么问题
如果你正在用 Spring AI 做 MCP 工具接入,大概率踩过这两个坑:STDIO 模式下 MCP Server 必须和 Client 跑在同一台机器、同一个进程组里,一旦想把工具服务拆出去给多个应用复用,进程管理立刻变成噩梦;SSE 模式虽然能跨网络,但它是单向推送,客户端发请求还得另开一条 POST 通道,连接状态在网关、负载均衡层经常被掐断,日志里动不动就是connection reset。
Streamable HTTP 传输的 MCP 就是冲着这些痛点来的。它把请求和响应都收敛到标准 HTTP 语义上,服务端可以像普通 Web 服务一样部署,客户端用一次 HTTP 调用就能完成工具发现和工具执行,流式响应通过 chunked 传输或 SSE 帧在同一个端点上完成。说白了,MCP Server 从"必须贴着客户端跑的附属进程"变成了"可以独立部署、独立扩缩容的后端服务"。
这篇文章面向的是已经在用 Spring AI、想把 MCP 从本地 STDIO 或 SSE 迁移到 Streamable HTTP 的开发者。我会给出一套可复制的 Server/Client 配置、Streamable HTTP 端点声明、以及用 TaoToken 统一 Key 接入模型调用的完整示例,最后附上启动日志和调用回显的验证动作。你跟着走一遍,链路能一次跑通。
核心检索词先明确:Spring AI Streamable HTTP MCP 配置、MCP STDIO 迁移、SSE 转 Streamable HTTP、Spring AI MCP Server WebFlux。这几个词贯穿全文,你搜的时候也能对上。
先说清楚迁移的收益边界。Streamable HTTP 不是银弹,它适合的是"工具服务需要被多个客户端共享""工具服务要独立部署和监控""需要通过标准网关做鉴权和限流"这三类场景。如果你的工具就是本地读个文件、跑个脚本,STDIO 反而更省事。但只要你开始考虑多工具协作、远程服务调用、或者把 MCP 能力暴露给团队里其他应用,Streamable HTTP 就是该走的路。
我试过把一套用户管理工具从 STDIO 迁到 Streamable HTTP,最直观的变化是:Server 可以单独mvn spring-boot:run起来挂在 8080,Client 在 8081 通过一个 URL 就能连上,中间还能塞个 Nginx 做转发。调试的时候直接 curl 打端点就能看工具列表,不用再去翻子进程的 stdout。
下面按"原问题与场景 → TaoToken 前置 → 可复制配置 → 验证请求 → 常见错排查 → CTA"的顺序展开。每一段都给可执行的东西,不空谈概念。
2. TaoToken 统一 Key 前置准备与模型接入配置
在动手改 MCP 传输之前,先把模型调用这一层理顺。MCP 负责的是"工具怎么被调用",但工具调用完的结果最终要交给 LLM 去理解和生成回答,所以模型接入的 Key 管理不能乱。我建议用 TaoToken 做统一 Key 接入,原因是它把 OpenAI 兼容协议封装好了,Spring AI 的spring-ai-starter-model-openai直接改base-url和api-key就能用,不用为每个模型供应商写一套适配。
先拿 Key。打开 TaoToken 控制台,在 API Keys 页面创建一个新 Key,复制出来。这个 Key 后面会同时用在 MCP Client 的模型调用配置里。控制台地址是 https://taotoken.net/console ,创建 Key 的入口在 https://taotoken.net/api-keys 。如果你还没注册,官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 可以先看一眼整体能力。
拿到 Key 之后,模型调用的 Base URL 用 https://taotoken.net/api ,注意这个地址后面不加任何 UTM 参数,直接写进配置。Model ID 按你实际要用的填,比如claude-3-7-sonnet-latest或者gpt-4o这类 OpenAI 兼容命名。这三个要素——Base URL、API Key、Model ID——在 Spring AI 的application.yml里对应spring.ai.openai.base-url、spring.ai.openai.api-key、spring.ai.openai.chat.options.model。
这里有个容易忽略的点:MCP Client 和 MCP Server 是两个独立进程,模型调用只发生在 Client 侧。Server 侧不需要配模型 Key,它只负责暴露工具。所以 TaoToken 的 Key 只需要出现在mcp-client模块的配置里。如果你把 Key 写到了 Server 的配置里,那是多余的,而且增加了泄露面。
再确认一下依赖。Client 侧需要spring-ai-starter-model-openai来驱动模型调用,Server 侧只需要spring-ai-starter-mcp-server-webflux。两者都依赖 Spring AI 的 BOM 来统一版本。我用的版本组合是 Spring Boot 3.5.10 + Spring AI 1.1.0-SNAPSHOT,JDK 25。如果你用的是稳定版 Spring AI,把版本号换成对应的 release 即可,配置结构不变。
关于 Key 的安全存放,别直接硬编码在application.yml里提交到仓库。用环境变量注入,配置里写${TAOTOKEN_API_KEY},启动时通过export TAOTOKEN_API_KEY=你的Key传入。这样本地调试和 CI 都能复用同一份配置。
如果你更习惯用命令行工具做模型对话验证,TaoToken 的模型对话入口在 https://taotoken.net/models ,可以先用它确认 Key 和 Model ID 是通的,再去配 Spring AI。这一步能帮你排除掉"到底是 Key 错了还是 MCP 配置错了"的干扰。长期做编码和 Agent 场景的话,Coding Plan 页面 https://taotoken.net/coding-plan 有对应的套餐说明,按需看就行。
前置准备清单:JDK 25、Maven 3.8+、PostgreSQL(本文示例用 R2DBC 连 PostgreSQL,你也可以换成 H2 先跑通)、TaoToken API Key、一个能访问的 Model ID。这些齐了,下面直接进配置。
3. 可复制的 MCP Server/Client 配置与 Streamable HTTP 端点声明
这一节是全文的核心,所有配置都可以直接复制。我按"父 POM → Server 配置 → Client 配置"的顺序给,路径和原文保持一致。
3.1 父 POM 统一版本管理
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>spring-ai-mcp-demo</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <properties> <maven.compiler.source>25</maven.compiler.source> <maven.compiler.target>25</maven.compiler.target> <spring-boot.version>3.5.10</spring-boot.version> <spring-ai.version>1.1.0-SNAPSHOT</spring-ai.version> </properties> <modules> <module>mcp-server</module> <module>mcp-client</module> </modules> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> </repository> </repositories> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project>3.2 MCP Server 的 Streamable HTTP 配置
Server 侧的关键是spring.ai.mcp.server.protocol: STREAMABLE和streamable-http.mcp-endpoint。前者声明传输协议,后者声明端点路径。完整application.yml:
spring: application: name: mcp-user-server http: codecs: max-in-memory-size: 10MB ai: mcp: server: protocol: STREAMABLE enabled: true name: webflux-mcp-server version: 1.0.0 type: ASYNC instructions: "This reactive server provides user management tools and resources" annotation-scanner: enabled: true capabilities: tool: true resource: true prompt: true streamable-http: mcp-endpoint: /api/mcp keep-alive-interval: 30s r2dbc: url: r2dbc:postgresql://localhost:5432/chatdb username: postgres password: root pool: enabled: true initial-size: 5 max-size: 20 sql: init: mode: always server: port: 8080Server 的依赖只需要三块:WebFlux、MCP Server WebFlux starter、R2DBC PostgreSQL。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-r2dbc</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>r2dbc-postgresql</artifactId> </dependency> </dependencies>工具定义用@McpTool和@McpToolParam注解,方法返回Mono<String>以适配响应式链路。下面是一个创建用户的工具示例:
@Component public class UserTools { private final UserRepository userRepository; public UserTools(UserRepository userRepository) { this.userRepository = userRepository; } @McpTool( name = "createUser", description = "创建新用户,需要用户名、邮箱、手机号和年龄" ) public Mono<String> createUser( @McpToolParam(description = "用户名,必填", required = true) String username, @McpToolParam(description = "邮箱地址,必填", required = true) String email, @McpToolParam(description = "手机号", required = false) String phone, @McpToolParam(description = "年龄", required = false) Integer age) { return userRepository.findByUsername(username) .flatMap(existing -> Mono.just("创建失败:用户名 '" + username + "' 已存在")) .switchIfEmpty( userRepository.findByEmail(email) .flatMap(existing -> Mono.just("创建失败:邮箱 '" + email + "' 已被使用")) .switchIfEmpty(Mono.defer(() -> { User newUser = new User(username, email, phone, age); return userRepository.save(newUser) .map(saved -> "用户创建成功:" + saved.getUsername()); })) ); } }3.3 MCP Client 的 Streamable HTTP 连接配置
Client 侧要同时配模型调用和 MCP 连接。模型调用走 TaoToken,MCP 连接指向 Server 的 Streamable HTTP 端点。完整application.yml:
spring: application: name: mcp-user-client http: codecs: max-in-memory-size: 10MB ai: openai: api-key: ${TAOTOKEN_API_KEY} base-url: https://taotoken.net/api chat: options: model: claude-3-7-sonnet-latest retry: max-attempts: 5 backoff: initial-interval: 2000 multiplier: 2 mcp: client: name: user-server version: 1.0.0 enabled: true type: ASYNC request-timeout: 30s streamable-http: connections: user-server: url: http://localhost:8080 endpoint: /api/mcp server: port: 8081 logging: level: io.modelcontextprotocol: DEBUG org.springframework.ai.mcp: DEBUGClient 依赖:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> </dependencies>注意streamable-http.connections下面的 key(这里是user-server)要和spring.ai.mcp.client.name对应上,否则注入McpAsyncClient时筛选会失败。Client 侧调用工具的核心代码:
@Service public class UserMcpService { private final McpAsyncClient mcpAsyncClient; private final ChatClient.Builder chatClientBuilder; public UserMcpService(List<McpAsyncClient> clients, ChatClient.Builder builder) { this.mcpAsyncClient = clients.stream() .filter(c -> c.getClientInfo().name().equals("user-server - user-server")) .findFirst() .orElseThrow(() -> new IllegalArgumentException("未找到 user-server 客户端")); this.chatClientBuilder = builder; } public Mono<String> createUser(String username, String email, String phone, Integer age) { Map<String, Object> params = new HashMap<>(); params.put("username", username); params.put("email", email); if (phone != null) params.put("phone", phone); if (age != null) params.put("age", age); return mcpAsyncClient.callTool(new McpSchema.CallToolRequest("createUser", params)) .map(this::extractResult); } private String extractResult(McpSchema.CallToolResult result) { if (result.isError()) return "调用出错:" + result.content(); return result.content().stream() .filter(c -> c instanceof McpSchema.TextContent) .map(c -> ((McpSchema.TextContent) c).text()) .findFirst() .orElse("无结果"); } }三件套在这里体现得很清楚:Base URL 是https://taotoken.net/api,Key 是${TAOTOKEN_API_KEY}环境变量,Model ID 是claude-3-7-sonnet-latest。MCP 连接则是url: http://localhost:8080+endpoint: /api/mcp。这两组配置各管各的,别混在一起。
4. 验证请求与成功结果:启动日志与调用回显
配置写完,跑起来验证。先起 Server,再起 Client,顺序不能反,因为 Client 启动时会去连 Server 拉工具列表。
启动 Server:
cd mcp-server mvn spring-boot:run看到类似下面的日志说明 Streamable HTTP 端点已经挂上:
MCP 服务器已启动! Streamable HTTP endpoint registered at /api/mcp Netty started on port 8080启动 Client:
export TAOTOKEN_API_KEY=你的Key cd mcp-client mvn spring-boot:runClient 启动时会打印工具发现结果:
MCP 客户端已启动! API 地址: http://localhost:8081 MCP 客户端已连接,可用工具:[createUser, getUserById, getAllUsers, updateUser, deleteUser, countUsers]这行可用工具:[...]是链路通了的第一个信号。如果这里为空或者报连接失败,直接跳到第 5 节排查。
接下来用 curl 打 Client 的 REST 接口,验证工具调用回显。创建用户:
curl -X POST http://localhost:8081/api/users \ -H "Content-Type: application/json" \ -d '{ "username": "testuser", "email": "test@example.com", "phone": "13800138000", "age": 25 }'预期返回:
{ "success": true, "message": "用户创建成功:testuser" }查询所有用户:
curl http://localhost:8081/api/users预期返回带用户列表的文本,包含刚创建的testuser。按 ID 查询:
curl http://localhost:8081/api/users/1更新用户:
curl -X PUT http://localhost:8081/api/users/1 \ -H "Content-Type: application/json" \ -d '{"phone": "13900139000", "age": 26}'删除用户:
curl -X DELETE http://localhost:8081/api/users/1最后验证模型调用链路,走流式问答:
curl -X POST http://localhost:8081/api/users/ask/stream \ -H "Content-Type: application/json" \ -d '{"question": "统计一下用户情况,并给出分析建议"}'这个请求会先通过 MCP 工具拉取用户数据,再把数据塞进 prompt 交给 TaoToken 的模型生成回答,响应以text/event-stream流式返回。如果你看到一段段文字陆续吐出来,说明 MCP 工具调用 + 模型调用两条链路都通了。
验证成功的三个标志:Client 启动日志里工具列表非空、curl 创建用户返回success: true、流式问答有内容输出。三个都满足,迁移就算完成。任何一个不满足,对照下一节的报错排查。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
迁移过程中最容易撞上的几类报错,我按实际遇到的频率排一下,每个都给定位方法和修复动作。
5.1 401 Unauthorized
这个报错基本都出在模型调用侧,也就是 TaoToken 的 Key 没配对。典型日志:
401 Unauthorized: {"error":{"message":"Invalid API key"}}排查顺序:先确认spring.ai.openai.api-key读到的环境变量TAOTOKEN_API_KEY确实有值,用echo $TAOTOKEN_API_KEY看一眼;再确认base-url是https://taotoken.net/api,末尾不要多斜杠也不要少路径;最后确认 Model ID 是账号有权限的模型。三件套里任何一个错了都会 401。注意 MCP Server 侧不需要这个 Key,如果 Server 日志里出现 401,那是你误把模型调用配到了 Server 上。
5.2 local proxy failed
这个报错通常出现在 Client 连 MCP Server 的阶段,日志类似:
local proxy failed: connection refused to http://localhost:8080/api/mcp意思是 Client 按配置去连 Server 的 Streamable HTTP 端点,但连不上。先确认 Server 真的起来了、端口是 8080;再确认streamable-http.connections.user-server.url和endpoint拼起来正好是http://localhost:8080/api/mcp;如果 Server 换了端口,Client 配置要同步改。还有一种情况是 Server 的mcp-endpoint写成了别的路径,两边对不上。
5.3 reading choices 相关报错
流式问答时如果模型返回体解析失败,会看到类似:
Error reading choices from response这多半是模型返回格式和 Spring AI 预期的不一致。检查spring.ai.openai.chat.options.model填的 Model ID 是否在 TaoToken 侧支持 OpenAI 兼容的 chat completions 格式。有些模型只支持特定接口形态,换一个兼容性更好的 Model ID 通常能解决。另外确认base-url没有指向一个返回 HTML 错误页的地址,那样解析必然失败。
5.4 OAuth 相关报错
如果你在 MCP 连接上启用了鉴权,或者 Server 侧加了 OAuth 保护,Client 没带 token 就会报:
OAuth authentication failed: missing bearer token本文的示例没有启用 MCP 层鉴权,所以正常不该出现这个错。如果你自己加了鉴权,确认 Client 的streamable-http.connections下配了对应的 header 或 token。生产环境建议在网关层做鉴权,而不是在 MCP 协议层硬塞 OAuth,这样 Client 配置更干净。
5.5 工具列表为空
Client 启动日志里可用工具:[],但没报错。这通常是annotation-scanner.enabled没开,或者工具类没被 Spring 扫描到。确认 Server 配置里spring.ai.mcp.server.annotation-scanner.enabled: true,工具类上有@Component,方法上有@McpTool。还有一种可能是 Client 筛选McpAsyncClient时名字对不上,检查getClientInfo().name()的实际值,用日志打出来看。
5.6 R2DBC 连接池耗尽
高并发下如果看到:
R2DBC pool exhausted, unable to acquire connection调大spring.r2dbc.pool.max-size,并加上max-idle-time: 30m。同时检查工具方法里有没有阻塞调用,响应式链路里混入阻塞 IO 会拖垮连接池。
排查的核心思路是分层:模型调用报错看 TaoToken 三件套,MCP 连接报错看 url + endpoint,工具发现报错看注解扫描。把这三层分开定位,大部分问题五分钟内能锁定。
6. 语义一致的 CTA 与后续接入路径
链路跑通之后,接下来该往哪走,取决于你的实际场景。
如果你是在做工具接入和排障,下一步是把 MCP Server 部署到能被团队访问的环境,然后在 TaoToken 的 API Keys 页面 https://taotoken.net/api-keys 管理你的 Key,接入文档在 https://taotoken.net/doc 有更细的协议说明。排障阶段遇到模型侧的问题,用模型对话入口 https://taotoken.net/models 单独验证 Key 和 Model ID,能快速区分是模型问题还是 MCP 问题。
如果你是要长期做编码和 Agent 场景,MCP 工具会越接越多,模型调用量也会上来,Coding Plan https://taotoken.net/coding-plan 的套餐更适合这种持续使用的形态。控制台 https://taotoken.net/console 可以统一看用量和 Key 状态。
Claude Code 相关的接入,Anthropic 兼容入口在 https://taotoken.net/claudecode-anthropic ,如果你同时用 Claude Code 和 Spring AI 两套工具链,可以共用同一个 TaoToken Key,省去多套凭证管理的麻烦。
回到技术本身,Streamable HTTP 迁移完成后,我建议你做的第一件事是把 MCP Server 的端点加上健康检查和基础监控。工具调用是有状态的业务操作,出问题时你需要知道是网络层断了、工具执行超时了、还是模型返回异常。在 Server 侧加一个/actuator/health,在 Client 侧记录每次callTool的耗时和结果状态,这两条信息在排查线上问题时比日志管用得多。
第二件事是给工具方法加幂等设计。Streamable HTTP 下客户端可能因为超时重试,createUser这类写操作如果没有唯一约束保护,会插入重复数据。本文示例里用findByUsername和findByEmail做了前置检查,但高并发下仍有竞态窗口,数据库层的唯一索引才是最终防线。
第三件事是把request-timeout和模型调用的retry策略对齐。MCP 工具调用超时设 30s,模型调用重试 5 次、退避 2s 起,这两个参数要匹配你的实际业务耗时。工具执行慢但模型重试快,会导致重复调用;反过来则会让用户等太久。按你的 P99 耗时来定这两个值。
迁移不是终点,把链路跑稳、把边界情况处理好,才是 Streamable HTTP 真正发挥价值的地方。