1. 为什么 SpringBoot 项目要接 MCP 的 SSE 通道
很多做后台系统的同学都遇到过类似场景:运营或业务方希望系统能"听懂一句话就干活",而不是点五层菜单填八个字段。MCP(Model Context Protocol)就是为这种"让系统长嘴长耳朵"的需求设计的协议,它把工具调用、上下文传递、结果回推标准化了。而 SSE(Server-Sent Events)模式是 MCP 里最适合服务端主动推送的一种传输方式——服务端不用等客户端轮询,有结果就直接推。
但真正落地时,问题往往不在协议本身,而在"模型从哪来、Key 怎么管、endpoint 指向谁"。如果你每个工具调用都自己拼一套模型请求,Key 散落在各个配置文件里,测试环境和生产环境混着用,很快就会乱成一锅粥。我这次的做法是把 MCP 服务端的模型调用统一收敛到 TaoToken 的 API 通道上,用一套 Key、一个 Base URL 管理所有模型请求,SpringBoot 这边只负责 SSE 连接管理和工具注册。
这篇文章面向的是已经会用 SpringBoot 写 REST 接口、但对 MCP 和 SSE 还比较陌生的后端同学。读完你能拿到三样东西:一份可直接复制的application.yml、一个能跑的 MCP 客户端 Bean 配置、以及用 curl 验证 SSE 事件流和断线重连的具体命令。全程不需要你懂模型底层,只要会改配置、会看日志就行。
先说清楚 SSE 在 MCP 里的角色。MCP 的 SSE 模式本质上是两条通道:一条是客户端用 GET 建立的持久连接(/sse或类似路径),服务端通过这条连接往下推事件;另一条是客户端用 POST 发请求(/mcp或/message),服务端收到后执行工具,再把结果通过 SSE 通道推回去。这跟传统 REST 的"请求-响应"完全不同,你得习惯"发出去的东西不一定立刻回来,回来的东西也不一定是你刚发的那条"。
TaoToken 在这里的作用是:当 MCP 服务端需要调用模型(比如解析用户自然语言、决定调哪个工具)时,不再直连各家模型厂商,而是走 TaoToken 的统一 API。这样你的application.yml里只需要维护一个base-url和一个api-key,换模型只改model字段,不用动代码。官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,API 入口是 https://taotoken.net/api ,两个地址别搞混——前者是控制台和文档,后者才是代码里填的 Base URL。
2. TaoToken 前置准备:Key、Base URL 与模型 ID 三件套
在动 SpringBoot 代码之前,先把"三件套"准备好:Base URL、API Key、Model ID。这三样东西缺一个,后面 SSE 连接建起来了也调不通模型。
Base URL 固定填https://taotoken.net/api,注意结尾不要带斜杠,也不要自己加/v1之类的路径,SDK 会自己拼。API Key 需要你去控制台生成,路径是 https://taotoken.net/console/api-keys ,登录后新建一个 Key,复制出来存好——它只显示一次,丢了只能重建。Model ID 取决于你想用哪个模型,常见的有claude-sonnet-4-5、gpt-4o这类,具体以你控制台里模型列表显示的为准,别凭记忆写。
我试过把 Key 直接写死在application.yml里,本地跑没问题,但一提交到 Git 就出事。正确做法是用环境变量注入,application.yml里只写占位符。下面这段是最终形态,你可以直接抄:
taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY:} model-id: ${TAOTOKEN_MODEL_ID:claude-sonnet-4-5} connect-timeout: 15000 read-timeout: 120000启动前在 shell 里 export 两个变量:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_MODEL_ID="claude-sonnet-4-5"如果你用 IDEA 跑,在 Run Configuration 的 Environment variables 里加这两条也一样。这样做的另一个好处是:测试环境用测试 Key,生产用生产 Key,代码零改动。
关于模型选择,如果你只是做工具调用的意图识别,claude-sonnet-4-5这类响应快、函数调用支持好的模型就够;如果要做长文档分析再触发工具,可以换上下文更长的型号。切换时只改TAOTOKEN_MODEL_ID环境变量,重启服务即可,不用重新打包。这一点在联调阶段特别省事——产品经理说"换个模型试试效果",你改个变量重启就行,不用等 CI。
还有一点容易被忽略:TaoToken 的 Key 是有权限范围的,如果你在控制台限制了某个 Key 只能调特定模型,那model-id就必须在允许列表里,否则会返回 403。排查时先确认这一点,能省很多时间。控制台地址再贴一次:https://taotoken.net/console/api-keys ,生成 Key 的时候顺手看一眼权限设置。
3. 可复制的 application.yml 与 MCP 客户端 Bean 配置
这一节是全文的核心,给你两份可直接落地的配置:一份application.yml,一份 MCP 客户端 Bean。先看完整的 YAML,注意路径和字段名要跟你的项目结构对上:
server: port: 8080 spring: application: name: mcp-sse-server mcp: sse: endpoint: /mcp/sse message-endpoint: /mcp/message heartbeat-interval: 30000 client: base-url: ${TAOTOKEN_BASE_URL:https://taotoken.net/api} api-key: ${TAOTOKEN_API_KEY:} model-id: ${TAOTOKEN_MODEL_ID:claude-sonnet-4-5} max-tokens: 4096 temperature: 0.2 taotoken: base-url: ${TAOTOKEN_BASE_URL:https://taotoken.net/api} api-key: ${TAOTOKEN_API_KEY:} model-id: ${TAOTOKEN_MODEL_ID:claude-sonnet-4-5} connect-timeout: 15000 read-timeout: 120000这里mcp.sse.endpoint是客户端建立 SSE 长连接的路径,mcp.sse.message-endpoint是客户端 POST 消息的路径,两个别写反。heartbeat-interval是心跳间隔,30 秒推一次空事件,防止中间层把空闲连接掐掉。
接下来是 MCP 客户端 Bean。我用的是 Spring 的@Configuration+@Bean方式,把 TaoToken 的 Base URL 和 Key 注入进去:
@Configuration public class McpClientConfig { @Value("${mcp.client.base-url}") private String baseUrl; @Value("${mcp.client.api-key}") private String apiKey; @Value("${mcp.client.model-id}") private String modelId; @Bean public McpSyncClient mcpSyncClient() { HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(15)) .build(); return McpClient.sync( HttpClientSseClientTransport.builder() .baseUri(baseUrl) .httpClient(httpClient) .build() ).requestTimeout(Duration.ofSeconds(120)).build(); } @Bean public ChatClient chatClient() { return ChatClient.builder() .baseUrl(baseUrl) .apiKey(apiKey) .model(modelId) .build(); } }注意HttpClientSseClientTransport的baseUri填的是 TaoToken 的 API 地址,不是你的本地服务地址。很多同学第一次配会搞混:MCP 服务端自己的 SSE 端点是给"上游客户端"连的,而这里的mcpSyncClient是"你的服务端去连模型"用的,两者方向相反。如果你只是想让外部客户端连你的 SpringBoot 服务,那mcpSyncClient可以暂时不建,但chatClient必须有,因为工具执行时要用它调模型。
如果你用的是 Cline 或 Claude Code 这类工具去连你的 SpringBoot MCP 服务,那配置要写在工具那边,格式类似:
{ "mcpServers": { "spms-server": { "url": "http://localhost:8080/mcp/sse", "headers": { "Authorization": "Bearer 你的AccessToken" } } } }这里的url是你 SpringBoot 服务的地址,Authorization是你自己系统发的 AccessToken,跟 TaoToken 的 Key 是两码事,别混。TaoToken 的 Key 只在服务端内部调模型时用。
配置写完,启动服务,看日志里有没有SSE endpoint registered at /mcp/sse这类输出。如果没有,检查@GetMapping的路径是不是跟 YAML 里一致。这一步过了,才进入下一节的验证。
4. 验证请求:curl 测 SSE 事件流与断线重连
配置对不对,curl 一测便知。先测 SSE 长连接能不能建立:
curl -N -H "Accept: text/event-stream" \ -H "Authorization: Bearer 你的AccessToken" \ http://localhost:8080/mcp/sse-N是关键,它关闭 curl 的缓冲,让你能实时看到推过来的事件。正常的话你会看到类似:
event: endpoint data: /mcp/message?sessionId=abc123 event: heartbeat data: {}第一行event: endpoint是 MCP 协议规定的,告诉客户端"你发消息要发到/mcp/message?sessionId=abc123"。拿到这个 sessionId 后,另开一个终端发 POST:
curl -X POST http://localhost:8080/mcp/message?sessionId=abc123 \ -H "Content-Type: application/json" \ -H "Authorization: Bearer 你的AccessToken" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "createOrder", "arguments": {"product": "螺丝", "qty": 100} } }'发完之后回到第一个终端,你应该能看到 SSE 通道里推回来一条event: message,data 里是工具执行结果。如果 POST 返回 200 但 SSE 通道没动静,八成是SseEmitter没跟 sessionId 绑定上,检查你的SseManager是不是用同一个 key 存的 emitter。
测断线重连,最简单的办法是手动 kill 掉 curl 再重连:
# 第一次连接 curl -N http://localhost:8080/mcp/sse # Ctrl+C 断开 # 重新连接,观察是否分配新 sessionId curl -N http://localhost:8080/mcp/sseMCP 客户端(比如 Cline)通常会自动重连,重连时会带Last-Event-ID头,你的服务端要支持从这个 ID 之后继续推。如果没实现,重连后客户端会丢事件。实现方式是在SseEmitter里维护一个事件 ID 计数器,重连时读Last-Event-ID头,从对应位置补发。这部分代码不复杂,但容易被跳过,联调时表现为"断一次网,工具调用结果就丢了"。
再给一个验证 TaoToken 通道是否通的 curl,直接打模型接口:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "'"$TAOTOKEN_MODEL_ID"'", "messages": [{"role": "user", "content": "ping"}], "max_tokens": 10 }'返回里有choices数组就说明 Key 和 Base URL 没问题。这一步单独测很重要,因为它把"模型通道"和"SSE 通道"解耦了——如果这个 curl 通、但 MCP 工具调用不通,问题一定在你的 SpringBoot 代码里,不用去怀疑 Key。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
联调阶段最容易撞的四个报错,我按出现频率排一下,每个都给判断方法和修法。
401 Unauthorized。两种可能:一是 TaoToken 的 Key 没注入成功,application.yml里${TAOTOKEN_API_KEY:}冒号后面是空的,环境变量没 export;二是 Key 对了但权限不够,比如你限制了 Key 只能调 A 模型,代码里却写了 B 模型。判断方法:先跑上一节那个直连https://taotoken.net/api/v1/chat/completions的 curl,如果它也 401,就是 Key 的问题;如果它通、MCP 调用 401,那就是你 SpringBoot 里chatClient的 apiKey 字段没绑上。修法:在 Bean 初始化后打一行日志log.info("apiKey prefix: {}", apiKey.substring(0, 8)),确认不是空串。
local proxy failed。这个报错通常出现在客户端(Cline、Claude Code)连你 SpringBoot 服务时,意思是它连不上你配的url。先确认 SpringBoot 真的在监听:netstat -an | grep 8080。如果端口对但还报,检查url是不是写成了https而你的服务是http。还有一种情况是你配了localhost但客户端跑在容器里,容器内的localhost不是宿主机,得换成宿主机 IP 或host.docker.internal。这个坑我踩过,排查了半小时才发现是网络命名空间的问题。
reading choices 相关报错。典型信息是Error reading choices: unexpected end of JSON input或choices field missing。这几乎都是模型返回体被截断或格式不对。先看max-tokens是不是设太小,比如设成 10 却让它输出工具调用 JSON,肯定截断。再看temperature是不是太高导致输出不稳定。如果用的是流式(stream=true),要确认你的解析代码是按 SSE 分块处理的,不能一次性readAll再 parse。修法:把max-tokens调到 4096,temperature降到 0.2,先用非流式跑通再开流式。
OAuth 相关报错。如果你在客户端配置里看到OAuth token expired或invalid_grant,注意这通常不是 TaoToken 的 Key 问题,而是客户端自己的认证机制。比如 Claude Code 有自己的 OAuth 流程,跟你 SpringBoot 服务的 AccessToken 是两套。判断方法:看报错里提到的域名是不是taotoken.net,如果不是,就别去动 TaoToken 的配置。修法:重新走一遍客户端的登录流程,或者改用 API Key 方式认证。
再补一个隐蔽的坑:SSE 连接建立后,如果 30 秒内没有任何事件,某些反向代理(Nginx)会主动断开。表现是客户端每隔半分钟重连一次,日志里一堆connection reset。修法是在 Nginx 配置里加proxy_read_timeout 3600s;和proxy_buffering off;,同时确保你的heartbeat-interval小于代理的超时时间。这个在本地直连时不会出现,一上测试环境就暴露,提前配好能省事。
6. 长期跑 MCP 服务,Key 和模型怎么管更省心
服务跑起来只是开始,真正麻烦的是后面几个月的维护。我自己的做法是把 TaoToken 的 Key 按环境拆开:本地开发一个 Key,测试环境一个,生产一个。每个 Key 在控制台里设不同的额度上限,这样即使本地 Key 泄露,损失也可控。控制台地址 https://taotoken.net/console/api-keys 里可以给 Key 加备注,写清楚用途,过两个月回头看不会懵。
模型 ID 也别写死在代码里。我见过有团队把claude-sonnet-4-5硬编码在 Java 常量里,后来想换模型得改代码、走发布流程。用环境变量或配置中心,改一行配置重启就行。如果你用 Nacos 或 Apollo,把TAOTOKEN_MODEL_ID放进去,还能做到不重启热更新。
对于需要长期跑 Agent 任务、工具调用量比较大的场景,可以关注一下 Coding Plan 这类按量方案,比单次调用更划算。入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,具体额度以页面显示为准。如果你的 MCP 服务只是偶尔被调几次,用普通 API Key 就够,不用上套餐。
最后说个实用技巧:在 SpringBoot 里加一个/actuator/health的自定义检查,启动时 ping 一下 TaoToken 的模型接口,通了才把服务标记为健康。这样 K8s 的 readiness probe 能拦住"服务起来了但模型通道不通"的情况,避免流量打进来才发现 401。代码就是在HealthIndicator里发一个max_tokens=1的请求,超时设 3 秒,失败就返回down。这个检查我加了之后,至少三次在发布阶段就发现了 Key 配错的问题,比等用户报障强得多。
工具注册那块,建议给每个@McpMethod加一个enabled开关,从配置里读。这样某个工具出问题时,不用改代码,配置里关掉就行,SSE 通道不受影响。配合 TaoToken 的 Key 权限控制,能做到"工具级"的灰度——新工具只对测试 Key 开放,生产 Key 看不到,联调时特别方便。