1. 从单端点到多端点MCP 服务在 Spring Boot 里的真实困境如果你正在用 Java 做 MCPModel Context Protocol服务端大概率会遇到这样一个场景一开始只做了一个/sse端点所有客户端都往这里连工具也全塞在一个 McpSyncServer 里。跑通 Demo 没问题但一旦业务方说“通知服务走一套工具、聊天服务走另一套工具、报表服务再单独隔离”代码就开始失控了。MCP 本身是给大模型提供工具、资源、提示的协议SSE 负责服务器到客户端的单向推送消息端点负责客户端到服务器的请求上行。两者配合客户端先通过 SSE 建立长连接“收听”再通过消息端点“打电话”下发指令。问题在于当多个业务场景需要独立会话池、独立工具集、独立鉴权时单端点架构根本撑不住。我试过把所有工具注册到一个 McpSyncServer 里结果 A 场景的客户端能看到 B 场景的工具列表会话串扰、鉴权配置散落在各个 Controller 里改一个场景要动三处代码。更麻烦的是每个场景对接的大模型客户端可能来自不同厂商API Key 管理完全失控。这篇要解决的就是这件事用 Spring Boot HttpServletSseServerTransport实现多 SSE 端点监听每个端点独立会话、独立工具同时把大模型调用通道统一收敛到 TaoToken 的 API 通道上Key 只配一次所有场景共用。适合已经跑通单端点 MCP、准备做工程化落地的 Java 后端。2. TaoToken 前置统一 Key 与 API 通道为什么必要多端点架构里每个 MCP 服务器最终都要调用大模型。如果每个场景各自配一套 Key、各自写一套 HTTP 客户端配置会迅速膨胀成灾难。TaoToken 在这里扮演的角色是统一的大模型 API 通道你只需要在控制台创建一个 API Key所有 MCP 服务器通过同一个 base URL 和同一个 Key 发起请求鉴权逻辑收敛到一处。具体来说TaoToken 提供兼容 OpenAI 风格的接口base URL 是https://taotoken.net/api模型对话、编码类请求都走这个入口。对 MCP 服务端而言这意味着工具回调函数里调用大模型时不需要关心底层是哪家模型只认一个 endpoint 和一个 Key。你需要提前准备两样东西第一一个可用的 API Key。到控制台创建路径是https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content创建后复制保存后面配置里会用到。第二确认你要用的模型名称。可以在模型对话页面先手动测一次地址是https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content选一个模型发一条消息确认通道正常。注意API Key 不要硬编码进代码或提交到 Git。本文示例用环境变量注入生产环境建议配合配置中心。如果你后续要做长期编码类 Agent 或高频工具调用可以了解 Coding Plan地址是https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content这里不展开。3. 可复制配置application.yml 与多端点注册骨架先看依赖。MCP Java SDK 的核心包是mcp-coreServlet 传输实现也在其中。Spring Boot 用 3.x因为HttpServletSseServerTransport依赖 Servlet 6.0 的异步能力。dependency groupIdio.modelcontextprotocol.sdk/groupId artifactIdmcp-core/artifactId version0.10.0/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependencyapplication.yml里把 TaoToken 通道和场景端点配置抽出来避免硬编码taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} model: gpt-4o-mini timeout: 30000 mcp: scenes: - name: notifications sse-path: /sse/notifications/* message-endpoint: /mcp/notifications/message - name: chat sse-path: /sse/chat/* message-endpoint: /mcp/chat/message - name: report sse-path: /sse/report/* message-endpoint: /mcp/report/message这里的关键设计是场景列表从配置读取而不是写死在Bean方法里。每个场景对应一个HttpServletSseServerTransport实例和一个ServletRegistrationBean两者通过 Bean 名称配对。配置类骨架如下用ConfigurationProperties绑定场景列表再动态注册Configuration EnableWebMvc public class McpServerConfig implements WebMvcConfigurer { Bean ConfigurationProperties(prefix mcp) public McpSceneProperties mcpSceneProperties() { return new McpSceneProperties(); } Bean public ObjectMapper objectMapper() { return new ObjectMapper(); } Bean public ListServletRegistrationBeanHttpServletSseServerTransport mcpServletBeans( McpSceneProperties props, ObjectMapper mapper) { ListServletRegistrationBeanHttpServletSseServerTransport beans new ArrayList(); for (McpSceneProperties.Scene scene : props.getScenes()) { HttpServletSseServerTransport transport new HttpServletSseServerTransport(mapper, scene.getMessageEndpoint()); ServletRegistrationBeanHttpServletSseServerTransport bean new ServletRegistrationBean(transport, scene.getSsePath()); bean.setName(scene.getName() SseServlet); beans.add(bean); } return beans; } }McpSceneProperties就是一个普通的 POJO字段ListScene scenesScene 里放name、ssePath、messageEndpoint。这样新增场景只改 YAML不动 Java 代码。每个HttpServletSseServerTransport实例内部维护独立的会话池。客户端连/sse/notifications时请求由 notifications 对应的 transport 处理会话存在它自己的池子里连/sse/chat则完全隔离。消息端点同理POST /mcp/notifications/message只会路由到 notifications 实例。4. 验证请求curl 测多端点连通与 TaoToken 鉴权生效配置写完启动服务用 curl 验证。先测 SSE 端点是否正常建立长连接curl -N -H Accept: text/event-stream http://localhost:8080/sse/notifications正常返回会看到event: endpoint和data: /mcp/notifications/message这样的初始事件连接保持不关闭。-N参数关闭 curl 缓冲方便实时看事件流。再开一个终端测 chat 端点curl -N -H Accept: text/event-stream http://localhost:8080/sse/chat两个连接同时存在互不干扰说明多端点监听生效。接着验证消息端点。MCP 的消息端点接收 JSON-RPC 格式请求先发一个initializecurl -X POST http://localhost:8080/mcp/notifications/message \ -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}}}返回里应该包含serverInfo和capabilities。如果返回 404说明消息端点路径和 transport 构造时传的不一致返回 500 则看日志里 transport 的异常栈。最后验证 TaoToken 鉴权。在工具回调里调用大模型时请求头带Authorization: Bearer ${TAOTOKEN_API_KEY}。你可以单独用 curl 测通道curl -X POST https://taotoken.net/api/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d {model:gpt-4o-mini,messages:[{role:user,content:ping}]}返回正常 completion 说明 Key 和通道都没问题。把这个调用封装进 MCP 工具的处理函数里所有场景共用同一个WebClient或RestTemplateBeanKey 从环境变量读。5. 本篇常见错排查错误一SSE 连接建立后立刻断开。最常见原因是 Servlet 异步支持没开。检查EnableWebMvc是否加上以及ServletRegistrationBean是否设置了setAsyncSupported(true)。Spring Boot 默认对注册的 Servlet 开启异步但如果你手动new ServletRegistrationBean后没配可能被覆盖。错误二多个端点串会话。如果你把多个场景的 transport 注册到了同一个 URL 前缀或者用了/*通配导致路径重叠Servlet 容器会按注册顺序匹配。确保每个场景的sse-path前缀唯一比如/sse/notifications/*和/sse/chat/*不会冲突但/sse/*和/sse/notifications/*会。错误三消息端点返回 405。HttpServletSseServerTransport的消息端点只接受 POST。如果你用 GET 请求会返回 405。另外确认messageEndpoint路径和客户端从 SSE 初始事件里拿到的路径一致客户端应该用服务端下发的 endpoint而不是自己拼。错误四TaoToken 调用返回 401。检查环境变量TAOTOKEN_API_KEY是否真的注入到进程里。System.getenv在 IDE 里跑和打包后跑结果可能不同。另外确认请求头格式是Bearer加 Key中间一个空格不要多也不要少。错误五动态新增场景后旧连接失效。这是 Servlet 特性决定的ServletRegistrationBean在容器启动后无法动态增删。新增场景必须重启服务。但每个 transport 内部的工具可以在运行时通过McpSyncServer的 API 动态注册配合定时任务或消息队列刷新工具列表不需要重启。提示如果你在排查 SSE 连接问题时看到AsyncContext相关异常优先检查 Tomcat 版本和 Servlet API 版本是否匹配。Spring Boot 3.2 配 Tomcat 10.1 是稳妥组合。6. 接入落地Key 管理与文档入口多端点跑通后下一步是把鉴权配置彻底收敛。所有 MCP 服务器调用大模型时统一走 TaoToken 的 API 通道Key 只在环境变量或配置中心维护一份。这样新增场景时你只需要在 YAML 里加一段不用再碰 Key。创建和管理 Key 的入口在控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI Keys 管理页可以直接生成和吊销https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。接入细节和参数说明看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。如果你用的是 Claude Code 或 Anthropic 风格的客户端对应入口在https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。实测下来把多端点注册和统一 Key 这两件事拆开处理后新增一个业务场景的成本从“改三个类 重启 配 Key”降到“改一段 YAML 重启”。工具的动态刷新则完全不用重启定时任务拉一次数据库重建SyncToolSpecification注册进去就行。唯一要记住的坑是Servlet 注册是启动期行为端点本身没法热加这是 Servlet 规范的限制不是 MCP 的问题。
