
1. 为什么要把 MCP 服务端改成 Stateless Streamable-HTTP如果你正在用 Spring AI 写 MCP 服务端大概率已经踩过 STDIO 和 SSE 的坑STDIO 只能本地进程通信SSE 在容器里长连接容易被网关掐断多副本部署时还会因为会话粘性问题导致工具调用随机失败。Stateless Streamable-HTTP 就是来解决这类问题的——它把每次请求都当成独立事务处理服务端不保存会话状态天然适配 Kubernetes 多副本、Serverless 和云原生网关。我这次的目标很明确把 Spring AI MCP 服务端的 endpoint 从默认路径改到统一 API 通道让本地联调时不用再维护一堆散落的 Key同时保留 Stateless 模式的无状态特性。具体做法是把 MCP 服务端的模型调用出口指向 TaoToken 的统一 Key/API 通道这样工具回调里如果需要调用大模型走的是同一套鉴权和计费调试时只改一个 Base URL 就能切换环境。适合谁看已经跑通过 Spring AI MCP 基础示例、想进一步做无状态部署的 Java 后端正在用 Cline、Claude Code 这类客户端连自建 MCP 服务端、被会话状态搞烦的开发者以及想把 MCP 服务端塞进微服务架构、需要水平扩容的团队。核心检索词先摆出来Spring AI MCP 服务端 Stateless Streamable-HTTP 配置本质是spring.ai.mcp.server.protocolSTATELESS加上spring-ai-starter-mcp-server-webmvc或 webflux依赖再配合mcp-endpoint自定义路径。下面从依赖、配置、验证到排错一步步来。2. TaoToken 前置准备与 MCP 服务端依赖选型在动手改 endpoint 之前先把两件事理清楚一是 TaoToken 这边需要拿到什么二是 Spring AI MCP 服务端该选哪个 starter。TaoToken 的作用是提供统一的模型 API 通道。你注册后在控制台创建一个 API Key后续 MCP 服务端里如果工具需要调用大模型比如让工具内部做一次摘要、分类、代码补全就用这个 Key 和对应的 Base URL。官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 路径不带 UTM 参数配置里写干净的这个就行。Key 的创建在控制台的 API Keys 页面模型对话调试可以用模型对话页长期跑编码类 Agent 任务可以看 Coding Plan。依赖选型这块Stateless 模式支持两种传输starter传输层适用场景spring-ai-starter-mcp-server-webmvcSpring MVC传统阻塞式、团队熟悉 Servlet 栈spring-ai-starter-mcp-server-webfluxWebFlux高吞吐、非阻塞、响应式栈两者都通过spring.ai.mcp.server.protocolSTATELESS开启无状态。我这次用 WebMVC因为本地联调时排查请求更直观日志里能看到完整的 HTTP 请求链路。如果你线上是 WebFlux 网关换成 webflux starter 即可配置项基本一致。pom.xml 里加上dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server-webmvc/artifactId /dependencySpring AI 的版本管理建议用 BOM 统一避免 starter 和核心包版本错位。如果你用的是 Spring Boot 3.3.x对应 Spring AI 1.0.x 系列MCP starter 的坐标在 1.0 之后从spring-ai-mcp-server-spring-boot-starter迁移到了spring-ai-starter-mcp-server-webmvc老教程里的坐标会报找不到类这点后面排错会细说。还有一点要提前确认Stateless 服务端不支持向客户端反向发消息也就是没有 sampling、没有 elicitation、没有心跳。如果你的工具逻辑依赖「服务端主动问客户端要输入」那 Stateless 模式不适用得回到有状态模式。这个限制在选型阶段就要想清楚否则写到一半发现架构不匹配返工成本很高。3. 可复制的 application.yml 与 MCP 客户端配置片段这一节是全文最核心的部分直接给可复制的配置。先看服务端的 application.ymlserver: port: 8080 spring: ai: mcp: server: enabled: true protocol: STATELESS name: stateless-mcp-server version: 1.0.0 type: SYNC instructions: Stateless MCP server for local dev, endpoint routed via unified API channel request-timeout: 30s capabilities: tool: true resource: true prompt: true completion: true annotation-scanner: enabled: true stateless: mcp-endpoint: /api/mcp disallow-delete: false几个关键点解释一下。protocol: STATELESS是开关不写这个默认是有状态。mcp-endpoint: /api/mcp就是你要改的 endpoint 路径客户端连接时用的就是这个。type: SYNC表示同步处理如果你工具里有大量 IO 想用异步改成ASYNC但注意 Stateless 下异步规范要用McpStatelessServerFeatures.AsyncToolSpecification。然后是模型出口的配置也就是把工具内部调用大模型的那条链路指向 TaoTokenspring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: claude-sonnet-4-20250514 temperature: 0.7这里base-url写 https://taotoken.net/api api-key从环境变量注入不要硬编码进 yml。Model ID 按你实际要用的填TaoToken 支持多种模型具体在模型对话页能看到可用列表。注意 OpenAI 兼容协议下 base-url 通常不带/v1Spring AI 的 OpenAI starter 会自己拼/v1/chat/completions如果你写成了https://taotoken.net/api/v1反而会 404这个坑后面排错会讲。服务端工具类这样写Service public class WeatherService { Tool(description Get weather information by city name) public String getWeather(String cityName) { return Sunny in cityName , 25C; } }注册 ToolCallbackProviderSpringBootApplication public class McpServerApplication { public static void main(String[] args) { SpringApplication.run(McpServerApplication.class, args); } Bean public ToolCallbackProvider weatherTools(WeatherService weatherService) { return MethodToolCallbackProvider.builder() .toolObjects(weatherService) .build(); } }客户端这边如果你用 Cline 或 Claude Code 连这个 Stateless 服务端配置里要写全三件套Base URL、Key、Model ID。以 Cline 的 MCP 配置为例settings JSON 片段{ mcpServers: { stateless-weather: { url: http://localhost:8080/api/mcp, transport: streamable-http, headers: { Authorization: Bearer ${TAOTOKEN_API_KEY} } } } }注意transport要写streamable-http不是sse。Stateless 服务端只认 Streamable-HTTP 客户端用 SSE 客户端连会握手失败。url里的路径就是你 yml 里配的mcp-endpoint。如果你用的是 Codex 的 auth.json 体系配置思路类似把 base_url 指向 https://taotoken.net/api api_key 填 TaoToken 的 Keymodel 填对应 Model ID。三件套缺一不可尤其是 Model ID写错了会报 model not found。4. 验证请求与成功结果curl 实测与响应解读配置写完先别急着上客户端用 curl 直接打服务端确认 Stateless 端点活着。启动 Spring Boot 应用后先看日志里有没有Registered tools和MCP endpoint: /api/mcp这类输出。第一步验证端点可达curl -i -X POST http://localhost:8080/api/mcp \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -d { jsonrpc: 2.0, id: 1, method: initialize, params: { protocolVersion: 2024-11-05, capabilities: {}, clientInfo: {name: curl-test, version: 1.0} } }成功的话你会看到 HTTP 200响应体是 JSON-RPC 格式里面包含serverInfo和capabilities。Stateless 模式下initialize 不会返回Mcp-Session-Id头这是和无状态模式最直观的区别——有状态模式会给你一个 session id后续请求要带上Stateless 则每次请求都是独立的。第二步列出工具curl -s -X POST http://localhost:8080/api/mcp \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -d { jsonrpc: 2.0, id: 2, method: tools/list, params: {} }响应里应该能看到getWeather这个工具带 description 和 inputSchema。如果这里返回空数组说明 ToolCallbackProvider 没被扫描到检查tool-callback-converter是不是被设成了 false或者 WeatherService 有没有加Service。第三步实际调用工具curl -s -X POST http://localhost:8080/api/mcp \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -d { jsonrpc: 2.0, id: 3, method: tools/call, params: { name: getWeather, arguments: {cityName: Hangzhou} } }预期返回Sunny in Hangzhou, 25C。到这一步Stateless Streamable-HTTP 服务端就算跑通了。第四步验证模型出口是否走通 TaoToken。如果你的工具内部调用了 ChatClient可以加一个测试工具Tool(description Summarize text using LLM) public String summarize(String text) { return chatClient.prompt() .user(Summarize: text) .call() .content(); }调用这个工具时观察日志里请求的 URL 是不是 https://taotoken.net/api/v1/chat/completions 返回 200 就说明统一通道生效了。这一步很关键很多人 MCP 端点通了但模型出口还是指向默认地址结果工具一调用就超时。实测下来Stateless 模式下连续发 10 次 tools/call每次都是独立请求服务端内存里不会累积 session 对象用jcmd看堆内存很平稳。这就是无状态的价值多副本部署时不需要 sticky session。5. 本篇常见错误排查401、local proxy failed、reading choices、OAuth这一节按真实报错来都是我踩过的。401 Unauthorized。最常见的原因是 API Key 没注入或者写错了。检查环境变量TAOTOKEN_API_KEY是否在启动时传进去了IDEA 里跑的话在 Run Configuration 的 Environment variables 里加。如果是 Docker-e TAOTOKEN_API_KEYxxx。还有一种情况是 Key 前面多了Bearer前缀Spring AI 的 OpenAI starter 会自己加你手动加了就变成Bearer Bearer xxx直接 401。local proxy failed / connection refused。这个报错通常出现在客户端连 MCP 服务端时。先确认服务端端口和mcp-endpoint路径对得上http://localhost:8080/api/mcp里的/api/mcp必须和 yml 里stateless.mcp-endpoint完全一致大小写敏感。如果服务端在容器里localhost 要换成容器 IP 或服务名。另外检查Accept头Streamable-HTTP 要求同时接受application/json和text/event-stream只写一个可能被服务端拒绝。reading choices / choices is null。这是模型出口的问题说明请求发出去了但响应体里没有choices字段。原因一般是 base-url 写错比如写成了https://taotoken.net/api/v1Spring AI 又拼了一次/v1变成/api/v1/v1/chat/completions返回的是 404 页面而不是 JSON解析时自然拿不到 choices。正确写法是 base-url 只到 https://taotoken.net/api 。另一个原因是 Model ID 写错模型不存在时有些网关会返回错误结构也会导致 choices 为空。OAuth / unauthorized_client。如果你在客户端配置里用了 OAuth 流程连 MCP 服务端但服务端是 Stateless 且没配安全模块会报这个。Stateless 模式下建议先用简单的 Bearer Token 鉴权别急着上 OAuth。等 MCP 安全模块配好了再切。另外 Claude Code 连远程 MCP 时如果走 OAuth回调地址要和服务端注册的一致本地联调阶段直接用 header 传 Key 最省事。No tool named xxx found。工具名对不上。Tool注解里的 name 默认取方法名但如果你显式写了Tool(name weather)客户端调用时就得用weather。还有工具去重逻辑同名工具只保留第一个如果你有两个 Bean 都注册了getWeather第二个会被丢弃日志里会有 warning。Stateless 模式下 sampling 报错。如果你在工具里试图调用McpSyncServerExchange的 sampling 能力会抛异常因为 Stateless 不支持服务端向客户端发请求。解决办法是把需要采样的逻辑改成工具内部直接调模型走 TaoToken 通道而不是依赖客户端采样。排错时建议把日志级别调到 DEBUGlogging: level: org.springframework.ai.mcp: DEBUG org.springframework.web: DEBUG这样能看到完整的 JSON-RPC 请求和响应定位问题快很多。6. 把 endpoint 稳定跑在统一通道上的几个实用建议配置跑通之后有几个细节能让它更稳。第一request-timeout别用默认的 20 秒。Stateless 模式下如果工具内部要调模型模型响应可能超过 20 秒尤其是长文本生成。设成 30s 到 60s 比较稳妥具体看你工具的耗时分布。第二多副本部署时mcp-endpoint路径在所有副本上保持一致网关层做负载均衡不需要 sticky session这正是 Stateless 的优势。但要注意如果你的工具依赖本地文件或内存缓存多副本下每个实例状态不同得把状态外置到 Redis 或数据库。第三Key 的管理。本地开发用环境变量CI/CD 用 Secret 管理别把 Key 提交到 Git。TaoToken 控制台可以创建多个 Key按环境区分出问题能单独吊销。第四客户端配置里 Base URL 和 MCP 服务端 URL 是两个概念别混。MCP 服务端 URL 是你自己服务的地址http://localhost:8080/api/mcpBase URL 是模型通道地址 https://taotoken.net/api 。前者是客户端连你后者是你连模型。第五如果你要从 Stateless 切回有状态做对比测试只改protocol一个值就行但客户端也要相应调整有状态模式需要处理 session id。建议用 Spring Profile 隔离两套配置application-stateless.yml和application-stateful.yml启动时--spring.profiles.activestateless切换。最后验证模型通道是否真的走了统一出口可以在 TaoToken 控制台的用量页面看请求记录每次工具调用模型都会有一条记录时间戳和你的 curl 调用对得上就说明链路正确。模型对话页也能直接测 Model ID 是否可用省得在代码里反复试。到这里Spring AI MCP 服务端 Stateless Streamable-HTTP 的 endpoint 改造和统一通道接入就完整了。核心就三件事protocol: STATELESS开无状态mcp-endpoint定路径base-url指向 https://taotoken.net/api 让模型出口走统一 Key。剩下的就是按 curl 三步验证遇到 401 查 Key、遇到 choices 为空查 base-url、遇到连接失败查路径和 Accept 头。