资讯详情

实战:用 Spring Boot 搭建 Model Context Protocol (MCP) 服务并接入 TaoToken 统一 Key

📅 2026/9/25 12:58:11 | 华诺云谱 👁 阅读
实战:用 Spring Boot 搭建 Model Context Protocol (MCP) 服务并接入 TaoToken 统一 Key
1. 为什么要在 Spring Boot 里手搓一个 MCP 服务Model Context ProtocolMCP这两年被讨论得很多简单说它就是给大模型和外部工具、数据源之间定的一套通用插口。你可以把它理解成 AI 世界的 USB-C模型这边只要支持 MCP就能用同一套协议去调用你写的工具而不用为每个模型单独适配一遍 Function Calling 的格式。那为什么选 Spring Boot 来做 MCP 服务端因为大部分公司的业务系统、内部工具、数据库访问层都是 Java 写的Spring Boot 又是 Java 生态里最顺手的 Web 框架。用 Spring Boot 暴露一个 SSE 长连接加一个 JSON-RPC 的 POST 端点就能把已有的 Java 能力包装成 MCP 工具让支持 MCP 的客户端直接调用。这篇要交付的东西很具体一个能跑起来的 MCP 服务骨架包含 SSE 握手、JSON-RPC 请求分发、tools/list 和 tools/call 两个核心方法再配上 TaoToken 的统一 Key 和 API 通道让模型侧调用走同一个入口。适合已经会写 Spring Boot、但还没接触过 MCP 协议的同学跟着敲一遍就能拿到可验证的结果。需要提前说清楚一点MCP 协议本身还在演进不同客户端对 protocolVersion 的要求可能不一样本文以当前常见的 2024-11-05 版本为基准遇到版本不匹配时改一个字符串即可。2. TaoToken 前置准备统一 Key 与 API 通道在写代码之前先把模型侧的调用通道准备好。TaoToken 在这里扮演的角色是统一入口你不需要为每个模型厂商单独维护一套 Key 和 Base URL而是拿一个统一 Key通过同一个 API 地址去访问不同模型。对于 MCP 这种要频繁调用模型的场景统一 Key 能省掉很多配置切换的麻烦。具体操作分三步。第一步打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册并登录账号。这一步只是拿到账号不涉及任何复杂配置。第二步进入控制台创建 API Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面点新建复制生成的 Key 并保存好。这个 Key 就是后面 application.yml 里要填的值。API Keys 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 后续要轮换或删除 Key 也在这里操作。第三步确认 API 通道地址。TaoToken 的 API 基础地址是 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为 base_url 使用即可。如果你用的是 OpenAI 兼容的客户端通常填到 /v1 这一层具体以文档为准接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。注意API Key 属于敏感凭证不要写进前端代码或提交到公开仓库。本文示例里用环境变量占位实际部署时通过配置中心或环境变量注入。如果你只是想先验证模型通道是否通可以打开模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 直接发一条消息确认 Key 有效、额度正常再回来写 MCP 服务。这样能把「模型通道问题」和「MCP 代码问题」分开排查省很多时间。3. 可复制配置application.yml 与 MCP 服务端骨架3.1 项目依赖与 application.yml先建一个标准的 Spring Boot 项目JDK 17 起步。pom.xml 里核心依赖只有两个Web Starter 负责 SSE 和 REST 端点Jackson 随 Web Starter 一起进来用来处理 JSON-RPC 的序列化。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesapplication.yml 里把 TaoToken 的通道配置和 MCP 服务自身的参数分开写方便后面替换。server: port: 8080 taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY:sk-please-replace-me} default-model: gpt-4o-mini timeout-seconds: 60 mcp: server: name: SpringBoot-MCP-Demo version: 1.0.0 protocol-version: 2024-11-05 sse: timeout-ms: 0这里timeout-ms: 0表示 SSE 连接不主动超时适合长连接场景。生产环境建议设一个合理值比如 3000005 分钟配合客户端重连。3.2 JSON-RPC 请求与响应模型MCP 基于 JSON-RPC 2.0请求和响应的结构要严格对齐。请求里有 jsonrpc、method、params、id 四个字段响应里对应 jsonrpc、result 或 error、id。package com.example.mcp.model; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; Data public class JsonRpcRequest { private String jsonrpc 2.0; private String method; private JsonNode params; private Object id; }package com.example.mcp.model; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; Data AllArgsConstructor NoArgsConstructor public class JsonRpcResponse { private String jsonrpc 2.0; private Object result; private Object error; private Object id; public static JsonRpcResponse success(Object id, Object result) { return new JsonRpcResponse(2.0, result, null, id); } public static JsonRpcResponse error(Object id, int code, String message) { return new JsonRpcResponse(2.0, null, java.util.Map.of(code, code, message, message), id); } }把 error 也做成静态工厂方法后面在 Service 层抛异常时统一走这里响应格式不会乱。3.3 核心服务层initialize、tools/list、tools/callMCP 服务端要处理的方法不多最核心的是三个initialize 告诉客户端我是谁、支持什么能力tools/list 列出可用工具tools/call 执行具体工具。另外 notifications/initialized 是客户端确认初始化完成的通知不需要返回内容但连接要保持。package com.example.mcp.service; import com.example.mcp.model.JsonRpcRequest; import com.example.mcp.model.JsonRpcResponse; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.List; import java.util.Map; Service public class McpService { private final ObjectMapper objectMapper new ObjectMapper(); Value(${mcp.server.name}) private String serverName; Value(${mcp.server.version}) private String serverVersion; Value(${mcp.server.protocol-version}) private String protocolVersion; public JsonRpcResponse handleRequest(JsonRpcRequest request) { String method request.getMethod(); try { switch (method) { case initialize: return handleInitialize(request); case tools/list: return handleListTools(request); case tools/call: return handleCallTool(request); case notifications/initialized: return null; default: return JsonRpcResponse.error(request.getId(), -32601, Method not found: method); } } catch (Exception e) { return JsonRpcResponse.error(request.getId(), -32603, e.getMessage()); } } private JsonRpcResponse handleInitialize(JsonRpcRequest request) { MapString, Object result new HashMap(); result.put(protocolVersion, protocolVersion); result.put(capabilities, Map.of(tools, Map.of())); result.put(serverInfo, Map.of(name, serverName, version, serverVersion)); return JsonRpcResponse.success(request.getId(), result); } private JsonRpcResponse handleListTools(JsonRpcRequest request) { MapString, Object addTool Map.of( name, calculate_sum, description, 计算两个数字的和, inputSchema, Map.of( type, object, properties, Map.of( a, Map.of(type, number, description, 第一个数字), b, Map.of(type, number, description, 第二个数字) ), required, List.of(a, b) ) ); MapString, Object sysInfoTool Map.of( name, get_system_info, description, 获取当前服务器运行环境信息, inputSchema, Map.of(type, object, properties, Map.of()) ); return JsonRpcResponse.success(request.getId(), Map.of(tools, List.of(addTool, sysInfoTool))); } private JsonRpcResponse handleCallTool(JsonRpcRequest request) { String name request.getParams().get(name).asText(); MapString, Object arguments objectMapper.convertValue( request.getParams().get(arguments), Map.class); String resultText; if (calculate_sum.equals(name)) { double a Double.parseDouble(arguments.get(a).toString()); double b Double.parseDouble(arguments.get(b).toString()); resultText String.valueOf(a b); } else if (get_system_info.equals(name)) { resultText System.getProperty(os.name) - Java System.getProperty(java.version); } else { return JsonRpcResponse.error(request.getId(), -32602, Unknown tool: name); } MapString, Object content Map.of( content, List.of(Map.of(type, text, text, resultText)) ); return JsonRpcResponse.success(request.getId(), content); } }注意 tools/call 的返回格式MCP 要求 result 里包一层 content 数组每个元素有 type 和 text。这个格式和普通 REST 接口不一样写错了客户端会解析失败。3.4 Controller 层SSE 握手与 POST 消息端点MCP over HTTP 有两个端点。SSE 端点负责建立长连接服务端通过它推送事件POST 端点负责接收客户端的 JSON-RPC 请求。SSE 建立后服务端要先发一个 endpoint 事件告诉客户端往哪个地址发 POST。package com.example.mcp.controller; import com.example.mcp.model.JsonRpcRequest; import com.example.mcp.model.JsonRpcResponse; import com.example.mcp.service.McpService; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; RestController RequestMapping(/mcp) public class McpController { private final McpService mcpService; private final ConcurrentHashMapString, SseEmitter emitters new ConcurrentHashMap(); Value(${mcp.sse.timeout-ms:0}) private long sseTimeoutMs; public McpController(McpService mcpService) { this.mcpService mcpService; } GetMapping(/sse) public SseEmitter handleSse() { SseEmitter emitter new SseEmitter(sseTimeoutMs); String sessionId UUID.randomUUID().toString(); emitters.put(sessionId, emitter); emitter.onCompletion(() - emitters.remove(sessionId)); emitter.onTimeout(() - emitters.remove(sessionId)); emitter.onError(e - emitters.remove(sessionId)); try { String endpointUrl /mcp/messages?sessionId sessionId; emitter.send(SseEmitter.event().name(endpoint).data(endpointUrl)); } catch (IOException e) { emitters.remove(sessionId); } return emitter; } PostMapping(/messages) public JsonRpcResponse handleMessage( RequestParam(required false) String sessionId, RequestBody JsonRpcRequest request) { return mcpService.handleRequest(request); } }这里 sessionId 目前只是记录连接没有做严格的会话绑定。生产环境建议把 sessionId 和 emitter 关联起来在 tools/call 执行时间较长时通过 SSE 推送进度事件而不是让 POST 一直阻塞。4. 验证请求SSE 握手与 JSON-RPC 调用代码写完启动 Spring Boot 应用默认监听 8080。接下来用 curl 分三步验证。4.1 验证 SSE 握手打开一个终端窗口执行curl -N http://localhost:8080/mcp/sse预期输出类似event:endpoint data:/mcp/messages?sessionId8f3a1c2e-...-N参数关闭 curl 的缓冲能实时看到 SSE 事件。这个窗口保持打开记下 sessionId。如果没有任何输出检查 Controller 是否被扫描到、端口是否被占用。4.2 验证 initialize另开一个终端发送初始化请求curl -X POST http://localhost:8080/mcp/messages \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, method: initialize, id: 1, params: { protocolVersion: 2024-11-05, capabilities: {}, clientInfo: {name: curl-client, version: 1.0} } }预期返回里包含 serverInfo 和 capabilities{ jsonrpc: 2.0, result: { protocolVersion: 2024-11-05, capabilities: {tools: {}}, serverInfo: {name: SpringBoot-MCP-Demo, version: 1.0.0} }, error: null, id: 1 }4.3 验证 tools/list 与 tools/call列出工具curl -X POST http://localhost:8080/mcp/messages \ -H Content-Type: application/json \ -d {jsonrpc:2.0,method:tools/list,id:2,params:{}}调用加法工具curl -X POST http://localhost:8080/mcp/messages \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, method: tools/call, id: 3, params: { name: calculate_sum, arguments: {a: 10, b: 25.5} } }预期返回{ jsonrpc: 2.0, result: { content: [{type: text, text: 35.5}] }, error: null, id: 3 }到这里一个最小可用的 MCP 服务就跑通了。模型侧只要支持 MCP配置上这个 SSE 地址就能发现并调用 calculate_sum 工具。5. 本篇常见错排查5.1 SSE 连接建立后立刻断开最常见的原因是 SseEmitter 超时时间设成了默认值。Spring 的 SseEmitter 默认超时较短长连接场景要显式设成 0 或一个较大值。检查 application.yml 里的mcp.sse.timeout-ms是否生效以及 Controller 里是否用了Value注入。另一个原因是反向代理层有超时限制。如果你在 Nginx 后面跑需要给 SSE 路径单独配置proxy_read_timeout和proxy_buffering off否则事件会被缓冲住客户端看不到实时推送。5.2 JSON-RPC 返回 400 或反序列化失败先看请求体是不是合法 JSON。curl 里用单引号包 JSON 时如果 JSON 内部有单引号会出问题。另外Content-Type必须是application/json少了这个头 Spring 不会走 Jackson 反序列化。如果报的是Cannot construct instance of JsonRpcRequest检查 JsonRpcRequest 是否有无参构造。Lombok 的Data默认会生成无参构造但如果你手动加了带参构造就要补上NoArgsConstructor。5.3 tools/call 返回 Unknown tool这个错误说明request.getParams().get(name)拿到的工具名和 handleListTools 里注册的不一致。注意 MCP 客户端有时会把工具名做大小写转换或加前缀建议在 handleCallTool 里先打印一下实际收到的 name再对照注册列表。还有一种情况是 params 结构不对。tools/call 的 params 必须是{name: ..., arguments: {...}}如果客户端把 arguments 直接平铺到 params 里就会取不到。5.4 TaoToken 通道调用超时MCP 服务本身不直接调模型但如果你在工具实现里调用了 TaoToken 的 API超时通常来自两处一是taotoken.timeout-seconds设得太短长文本生成容易超二是网络出口不稳定。建议把超时设到 60 秒以上并在工具实现里加一层重试。如果返回 401检查 API Key 是否复制完整、有没有多余空格。如果返回 404检查 base-url 是否写成了https://taotoken.net/api而不是带/v1的地址具体以接入文档为准。5.5 多客户端并发时 session 串了当前示例里 emitters 用 ConcurrentHashMap 存了 sessionId 到 emitter 的映射但 POST 端点没有校验 sessionId 是否有效。多客户端并发时如果客户端传了错误的 sessionId消息可能推错连接。生产环境要在 handleMessage 里校验 sessionId 存在性不存在就返回错误而不是静默处理。6. 接入与后续把 MCP 服务接到真实模型骨架跑通之后下一步是让真实模型用上这个 MCP 服务。如果你只是想在对话里验证模型能不能正确调用工具可以打开模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 把 MCP 服务的 SSE 地址配进去发一条「帮我算一下 10 加 25.5」看模型是否触发 calculate_sum。如果你要做的是长期编码或 Agent 场景比如让模型在 IDE 里持续调用本地工具建议用 Coding Plan 通道 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它在长会话和工具调用频率上更稳。Claude Code 相关的接入配置在 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-code-anthropicutm_campaignrewrite 按文档填 base_url 和 Key 即可。接入过程中如果遇到 401、404 或 SSE 握手失败优先去 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 确认 Key 状态再对照接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 检查参数。大部分问题出在 base_url 多写或少写了路径段以及 Key 复制时带了换行。最后提醒一个实际踩过的坑MCP 的 protocolVersion 在不同客户端之间可能不一致如果客户端 initialize 时传的版本和服务端返回的版本对不上有些客户端会直接断开。稳妥做法是服务端在 handleInitialize 里回显客户端传来的版本而不是硬编码一个固定值。这个改动很小但能避免很多莫名其妙的连接失败。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑