首页 / 资讯中心 / 文章详情

SpringBoot3 + OpenSpec 实现 MCP 服务器实践:把 SSE 与 JSON-RPC 端点改到 TaoToken

SpringBoot3 + OpenSpec 实现 MCP 服务器实践:把 SSE 与 JSON-RPC 端点改到 TaoToken ★ FEATURED ARTICLE
1. 为什么要在 SpringBoot3 里自己搭一个 MCP 服务器如果你最近在折腾 AI 编程工具大概率听过 MCPModel Context Protocol。简单说它是一套让 AI 应用和外部数据源对话的开放协议Anthropic 提出来的核心目标是把「AI 调用外部能力」这件事标准化。以前你想让模型查数据库、读文件、调第三方接口每接一个数据源就得写一套认证、解析、错误处理的对接代码重复劳动多、维护成本高。MCP 把这些统一成 Tools、Resources、Prompts 三类能力底层用 JSON-RPC 2.0 通信传输层可以是 Stdio、SSE 或者 Streamable HTTP。那为什么要在 SpringBoot3 里自己实现一个 MCP 服务器因为很多团队的后端本来就是 Java 技术栈业务逻辑、权限体系、数据访问都沉淀在 Spring 生态里。与其另起一个 Node 或 Python 服务去桥接不如直接在现有 SpringBoot3 工程里暴露 MCP 端点让 Claude Code、Cline 这类客户端直接调用你已有的服务能力。这篇就聚焦两个关键环节SSE 长连接的握手以及 JSON-RPC 消息的分发路由同时把模型调用入口统一到 TaoToken 上避免在多个客户端里散落不同的 Key 和 Base URL。适合谁看有 SpringBoot 基础、想在本地跑通一个 MCP 服务、并且希望把模型调用统一管理的开发者。我会给出可复制的application.yml、OpenSpec 工件配置片段以及启动后用 curl 验证 SSE 握手和 JSON-RPC 请求响应的完整动作。整个过程不需要你懂前端跟着敲命令就能确认端点切换后服务可用。先说清楚一个容易混淆的点MCP 服务器本身不负责「调用大模型」它负责的是「把工具能力暴露给 AI 客户端」。真正发起模型推理的是客户端比如 Claude Code。所以我们的 SpringBoot 服务扮演的是工具提供方而模型调用入口则通过 TaoToken 统一配置在客户端侧。这两件事分开理解后面配置就不会乱。我试过把工具逻辑直接塞进 Controller 里结果新增一个工具就要改路由代码非常难受。后来用注册中心 接口抽象的方式重构新增工具只需要写一个类这也是下面设计部分会重点讲的。2. TaoToken 前置准备统一模型调用入口与 Key 获取在动手写 MCP 服务器之前先把模型调用入口这件事理清楚。因为后面你要在 Claude Code 或 Cline 里测试工具调用这些客户端都需要一个能访问模型的通道。TaoToken 在这里的作用就是提供统一的 API 入口让你不用在多个客户端里分别配置不同的供应商信息。你需要先拿到一个 API Key。访问控制台地址https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_springboot_consoleutm_campaignrewrite登录后在 API Keys 页面创建一个新的 Key。建议按用途命名比如mcp-local-dev方便后面区分。创建后立刻复制保存页面刷新后就看不到完整 Key 了。拿到 Key 之后记下两个关键信息Base URL 是https://taotoken.net/api以及你刚创建的 Key。这两个值后面会出现在客户端的配置里。如果你用的是 Claude Code它的配置方式是通过claude mcp add命令或者 settings 文件如果用的是 Cline则是在 MCP 配置 JSON 里填写。这里要强调一个概念MCP 服务器我们的 SpringBoot 应用和模型调用入口TaoToken是两条独立的链路。MCP 服务器监听localhost:8080/mcp负责接收客户端的 JSON-RPC 请求并返回工具结果而客户端调用模型时走的是 TaoToken 的 API。两者不要混在一起配置否则排查问题时会很痛苦。如果你还想在浏览器里直接验证模型是否可用可以打开模型对话页面https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_springboot_chatutm_campaignrewrite发一条简单消息确认通道正常。这一步不是必须的但能帮你提前排除 Key 无效或额度问题。对于需要长期跑编码 Agent 的场景可以了解一下 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_springboot_planutm_campaignrewrite它更适合高频调用。不过本篇的重点还是本地 MCP 服务的搭建模型入口配置到位即可。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_springboot_docutm_campaignrewrite里面有各客户端的详细配置示例遇到不确定的字段可以去对照。API Keys 管理页再贴一次方便你找https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_springboot_keysutm_campaignrewrite。3. 可复制配置application.yml 与 OpenSpec 工件片段这一节给出可以直接抄的配置。先看 SpringBoot3 的application.yml重点是 SSE 相关的超时设置和 JSON 序列化配置。server: port: 8080 servlet: encoding: charset: UTF-8 force: true spring: application: name: mcp-server-demo mvc: async: request-timeout: 60000 jackson: default-property-inclusion: non_null serialization: write-dates-as-timestamps: false mcp: server: name: calendar-mcp version: 1.0.0 protocol-version: 2024-11-05 sse-timeout-ms: 60000spring.mvc.async.request-timeout设成 60000 是为了配合 SSE 的 60 秒超时避免异步请求被容器提前掐断。mcp.server.protocol-version是握手时协商的协议版本客户端会拿这个值做兼容判断。接下来是 OpenSpec 的工件配置。OpenSpec 是工件驱动的开发方法通过结构化文档把需求、设计、实现、验证串起来。下面是一个精简的openspec.yaml片段描述本次 MCP 服务器的工件清单。change: springboot-mcp-sse artifacts: - name: proposal path: openspec/changes/springboot-mcp-sse/proposal.md description: 技术选型与架构决策 - name: tasks path: openspec/changes/springboot-mcp-sse/tasks.md description: 任务拆分与依赖 - name: design path: openspec/changes/springboot-mcp-sse/design.md description: 类结构与接口定义 - name: implementation path: openspec/changes/springboot-mcp-sse/implementation.md description: 核心代码实现 - name: verification path: openspec/changes/springboot-mcp-sse/verification.md description: 测试结果与归档如果你用 Claude Code 作为客户端MCP 配置可以写成这样注意这里同时出现了 Base URL、Key、Model ID 三件套缺一不可{ mcpServers: { calendar-mcp: { type: http, url: http://localhost:8080/mcp } }, env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的Key, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }注意mcpServers里配置的是我们的 SpringBoot 服务地址而env里配置的是模型调用入口。两者分开职责清晰。如果你用 ClineMCP 配置 JSON 结构类似把type改成sse或http按客户端要求来。OpenSpec 的 proposal 工件里建议把传输层选型的决策写清楚。比如为什么选 SSE 而不是 StdioStdio 最简单官方示例常用但它是进程级通信Web 应用集成不自然WebSocket 双向能力强但实现复杂SSE 基于 HTTP 长连接实现简单也符合 MCP 服务端推送的模式。这个决策写进 proposal后面回看时能省很多解释成本。4. 验证请求curl 测 SSE 握手与 JSON-RPC 响应配置写完启动服务。用 Maven 或 Gradle 都行./gradlew bootRun看到控制台输出Started McpServerApplication in x seconds就说明起来了。接下来用 curl 验证两个动作SSE 握手和 JSON-RPC 请求。先测initialize握手curl -N -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -H Accept: text/event-stream \ -d {jsonrpc:2.0,method:initialize,params:{protocolVersion:2024-11-05,clientInfo:{name:curl-test,version:1.0}},id:1}-N关闭 curl 的缓冲这样你能实时看到 SSE 事件流。正常返回类似event: message data: {jsonrpc:2.0,result:{protocolVersion:2024-11-05,serverInfo:{name:calendar-mcp,version:1.0.0},capabilities:{tools:{}}},id:1}接着测tools/listcurl -N -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -H Accept: text/event-stream \ -d {jsonrpc:2.0,method:tools/list,id:2}返回里应该能看到你注册的工具比如get_calendar_events包含 name、description 和 inputSchema。最后测tools/callcurl -N -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -H Accept: text/event-stream \ -d {jsonrpc:2.0,method:tools/call,params:{name:get_calendar_events,arguments:{date:2026-02-10}},id:3}预期返回event: message data: {jsonrpc:2.0,result:{content:[{type:text,text:2026-02-10 有 3 个会议}],isError:false},id:3}再测一个异常路径确认错误码正确curl -N -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -H Accept: text/event-stream \ -d {jsonrpc:2.0,method:unknown_method,id:4}应该返回{jsonrpc:2.0,error:{code:-32601,message:方法不存在},id:4}。JSON-RPC 规范里 -32601 就是 Method not found这个别写错。如果三个正向请求都返回了预期结果说明 SSE 传输层和 JSON-RPC 路由都通了。这时候再去 Claude Code 里配置 MCP 服务器就能直接调用工具了。配置命令claude mcp add --transport http --scope user calendar-mcp http://localhost:8080/mcp然后在对话里说「列出所有可用工具」或「查询 2026-02-10 的日程」客户端会自动发现并调用。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth实际跑的时候报错基本集中在这几类。逐个说清楚原因和改法。401 Unauthorized这个通常不是 MCP 服务器的问题而是模型调用入口的 Key 配错了。检查客户端env里的ANTHROPIC_API_KEY是不是完整、有没有多余空格Base URL 是不是https://taotoken.net/api。注意 Base URL 末尾不要多加/v1之类的路径除非文档明确要求。如果 Key 刚创建就报 401去控制台确认一下 Key 状态是否启用。local proxy failed这个报错一般出现在客户端尝试连接本地 MCP 服务时。先确认 SpringBoot 服务真的在跑curl http://localhost:8080/mcp能通。然后检查客户端配置里的 URL 是不是写成了https本地服务通常是http。还有一种情况是端口被占用换个端口重启服务同步改客户端配置。reading choices 相关报错这类错误通常出现在模型返回结构不符合预期时根源往往是 Model ID 写错或者模型不支持当前请求格式。检查ANTHROPIC_MODEL是否填了有效值别填一个不存在的模型名。如果客户端日志里出现reading choices说明它在按 OpenAI 格式解析响应但你配的可能是 Anthropic 格式的端点两者要对齐。OAuth 相关报错有些客户端默认走 OAuth 流程但我们的本地 MCP 服务没有实现 OAuth。这时候要么在客户端配置里显式指定type: http跳过 OAuth要么确认客户端版本支持无认证的本地端点。如果报错里出现OAuth字样先看是不是客户端把 MCP 服务器当成了需要授权的远程服务。再补一个容易忽略的点SSE 连接建立后如果长时间没有事件某些客户端会主动断开。我们的sse-timeout-ms设成 60000如果工具执行时间可能超过这个值要么调大超时要么在工具里做异步处理并先返回一个「处理中」事件。别让连接静默超时否则客户端会报连接中断。排查顺序建议先 curl 确认服务本身正常再看客户端配置的 URL 和 type最后查模型入口的 Key 和 Model ID。分层排查比一上来就改代码高效得多。6. 把工具接入做扎实从本地验证到长期使用走到这里你已经有了一个能响应 SSE 握手和 JSON-RPC 请求的 SpringBoot3 MCP 服务器也验证了工具列表和调用。接下来真正影响体验的是工具设计的细节。第一inputSchema要写清楚。AI 客户端靠这个 schema 决定怎么填参数。如果你只写{type:object}而不定义具体字段模型可能会瞎猜参数名。建议把每个参数的 type、description、是否 required 都写全。比如日期参数写成{type:string,description:日期格式 YYYY-MM-DD}模型填错的概率会明显下降。第二工具描述要面向「什么时候用」而不是「这是什么」。getDescription()返回的内容会进入模型的上下文写「查询指定日期的日历事件」比写「日历工具」有用得多。如果工具有使用限制比如只支持未来 30 天也写进描述里。第三错误处理要返回结构化信息。工具执行失败时别直接抛异常让连接断掉而是返回isError: true加上错误说明。这样模型能理解失败原因甚至自己重试或换参数。第四注册中心用 Spring 的ListTool自动注入是个很省事的做法。新增工具只需要写一个Component类实现接口不用改任何配置。但要注意工具名不能重复否则后注册的会覆盖先注册的。可以在注册时加个校验重名直接启动失败早发现早改。关于模型调用入口如果你后面要跑长时间的编码 Agent建议把 Key 和 Base URL 统一放在客户端的环境变量或 settings 文件里不要硬编码在代码中。TaoToken 的接入文档里有各客户端的推荐配置方式照着填能少踩坑。需要新建 Key 时去https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_springboot_keys2utm_campaignrewrite需要查配置示例去https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_springboot_doc2utm_campaignrewrite。最后留一个实用技巧在本地开发阶段把 MCP 服务器的日志级别调到 DEBUG把每个进来的 JSON-RPC 请求和出去的响应都打出来。这样客户端报错时你能立刻判断是请求没到、路由没匹配还是工具执行出错。等稳定后再调回 INFO避免日志刷屏。工具接入这件事验证一次不难难的是长期维护时不混乱配置分层和日志清晰是底线。
阅读完成 · 觉得有帮助?
咨询建站