:把 MCP Server 改成无状态(文末附github源码链接))
MCP 实战手记系列三系列一留了个钩子“无状态改造具体怎么改系列三实测”。这篇来兑现。面向一线开发者重实操每篇都有我亲手跑过的东西。引子我手上正好有个反面教材系列一写完 MCP Live我回头看了眼自己 2025 年的一个项目——里面有个 Spring AI 写的 MCP Server17 个工具跑得好好的。然后发现三件事用的是SSE 传输配置里写着sse-message-endpoint依赖是Spring AI1.0.0-M7里程碑版本还挂着Redisson Redis 做会话存储对照 2026-07-28 新规范——去 session、无状态、Streamable HTTP——每一条都踩在即将过时那一边。那就迁吧。业务细节不便公开场景已换为通用机房环境监控温湿度查询 设备控制技术骨架与改造步骤一致。一、旧版长什么样典型 Spring AI MCP Server 三件套依赖spring-ai-starter-mcp-server-webmvc:1.0.0-M7配置spring.ai.mcp.server.sse-message-endpoint: /mcp/message工具ToolToolParam标注普通Service方法再用MethodToolCallbackProvider注册成 Bean工具定义这块我很喜欢——业务方法加个注解就变成 MCP 工具不用实现任何接口Tool(namecontrol_device,description启动或关闭指定设备)publicvoidcontrolDevice(ToolParam(description设备名称)StringdeviceName,ToolParam(description1:启动 0:关闭)intoperateType){...}运行模型客户端建 SSE 长连接 → 服务端分配 session 存状态 → 后续请求复用会话。问题也在这服务端存状态就要共享存储、要粘性会话水平扩展麻烦。二、新规范改了什么变化影响去掉initialize握手和 session不再需要会话存储每请求自带协议版本、客户端身份、能力打到哪个实例都能处理SSE → Streamable HTTP传输配置要改elicitation 双向流 →input_required补参逻辑要改Mcp-Method/Mcp-Name请求头网关不再解析 body一句话服务端不再需要记住任何东西。三、动手迁移4 步步骤 1升级依赖1.0.0-M7→ 支持新规范的正式版。里程碑版跑生产本来就有风险顺手解决。步骤 2改传输配置核心改动就这几行spring:ai:mcp:server:name:device-mcp-serverprotocol:STATELESS# 替代 SSESSE/session 配置删除步骤 3清理状态确认业务逻辑不依赖 session 后把 Redisson / Redis 会话存储整套删掉。看着 pom 瘦了一圈。如果 session 里存了业务上下文如多轮对话中间态要外移到客户端传参或落到独立存储按 ID 查——不能继续挂在 MCP session 上。步骤 4补参改造旧规范用 elicitation 双向流新规范改为工具返回input_requiredif(operateTypenull){returnDeviceControlResult.inputRequired(operateType,需要确认1 启动 / 0 关闭);}四、最省心的一点工具代码几乎没动我这个项目里整个迁移90% 的改动集中在配置和依赖业务工具代码基本没碰。Tool/ToolParam的声明式写法在新旧规范之间保持稳定——这是 Spring AI 设计到位的地方协议演进不该波及业务代码。我那个 17 个工具的项目真正要改的只有1 个依赖版本、2 行配置、删掉 Redis 会话、少数几个有补参逻辑的工具。犹豫要不要迁成本可以放心——声明式写法稳定业务代码基本不用动。五、怎么验证真的无状态了多实例测试起两个实例同客户端请求交替打过去。旧版因 session 不共享会报错新版完全正常重启不中断重启服务端客户端不需重新建连下次请求直接成功抓包看请求每请求自带协议版本/身份/能力不应再有initialize握手和 session ID六、踩坑记录实跑记录报错原文保留搜索价值最高的部分。① 依赖坑ToolCallback.getName()被移除升级 Spring AI 2.0.x 后编译挂[ERROR] McpToolConfig.java:[29,22] 找不到符号方法 getName()接口ToolCallback。工具名挪进了ToolDefinition。解法callback.getName()→callback.getToolDefinition().name()。② 配置坑聚合 POM 的 properties 不传给子模块父 POM 若是纯聚合器子模块拿不到project.build.sourceEncodingWindows 按 GBK 解码中文注释搅乱注释块报出[ERROR] McpToolConfig.java:[5,1] 需要 class、interface、enum 或 record。解法每个模块 pom 显式声明 UTF-8源文件无 BOM。③ 客户端坑PowerShell 的curl -d {...}会被转义搞坏一直返回Invalid message format (-32600)不是协议问题是引号被转义。解法改用curl -d payload.json文件传参。④ 行为差异坑ToolParam默认必填补参分支永远走不到不传operateType时拿到框架报错Tool (control_device) input validation failed: 未找到所需属性 operateType而非我写的补参提示——因为ToolParam默认requiredtrue框架在调方法前就拦截了。解法补参加required false请求才会进方法体实测拿到{inputRequired:true,requiredParam:operateType,message:需要确认1 启动 / 0 关闭}⑤ 无状态的直接证据initialize响应不再返回Mcp-Session-Id。七、意外收获迁移时发现的安全问题清理代码时顺手做了一次检查发现两个问题系列七安全篇展开1. MCP 端点没有鉴权—— SSE 端点直接暴露、无 token 校验而同项目 Web 端反而接了 Sa-Token且这个有写工具控制设备开关风险完全不同。2. 凭证明文硬编码—— 密码、API Key 明文写在配置和代码里配置类管 API Key 却在 Service 里又硬编码一份根本没生效。小结成本比想象低—— 声明式写法稳定业务代码零改动改的是配置和依赖收益很直接—— 去掉 Redis 会话服务无状态扩展不再需要粘性会话顺手发现安全问题—— 端点无鉴权 明文凭证这比要不要迁重要得多手上还有 2025 年那批 MCP Server 的建议抽半天做一次迁移 安全检查。前半天成本换后面少踩很多坑。本文完整可运行代码github.com/ethanliang2016/mcp-in-actiontag:v03旧版在03-stateless-migration/legacy-sse新版在stateless/可对照运行。MCP 实战手记系列路线图#篇目状态1总纲篇MCP 到哪一步了✅2跑通第一个 MCP Server即将3把 MCP Server 改成无状态本篇✅4-7CIMD 授权 / MCP Apps / 自建网关 / 安全篇规划后面几篇逐步放出关注我更新第一时间能看到。迁移 Spring AI MCP Server 时踩过什么坑评论区补充有价值的我会整理进后续篇目。参考MCP 2026-07-28 规范更新MCP 路线图Spring AI MCP Server 文档