)
1. 为什么我要用 Spring Boot 手搓一个 MCP 服务MCPModel Context Protocol是 Anthropic 推出的开放协议用来把 AI 模型和外部工具、数据源连起来。你可以把它理解成 AI 世界的 USB 接口AI 客户端是电脑MCP 服务是打印机或鼠标工具Tool就是设备上的具体按钮。我手上有几个跑了好几年的 Spring Boot 业务系统一直想让它们被 AI 直接调用比如查订单、算价格、拉库存而不是每次都在对话框里手动贴数据。MCP 正好解决这个问题——把现有接口包装成标准工具AI 客户端就能自动发现并调用。这篇面向的是有 Java 基础、想快速跑通 MCP 端到端链路的开发者。我会用 Spring Boot 3.2 MCP Java SDK 0.10.0 从零搭一个服务端注册两个工具计算器和天气查询然后用三种方式验证调用链路最后接入 TaoToken 统一 Key 通道让模型侧也能直接消费这些工具。整套代码可以直接复制运行不需要你提前理解协议细节。需要说明的是MCP 服务端本身不依赖大模型它只负责暴露工具真正调用工具的是 AI 客户端。所以本文分两条线一条是 Spring Boot 服务端怎么建另一条是通过 TaoToken 的 API 通道让模型客户端能连上这个服务。两条线都跑通才算端到端。2. TaoToken 前置准备统一 Key 与 API 通道在写代码之前先把模型侧的通道准备好。TaoToken 提供统一的 API Key 和兼容接口省去你分别对接多家模型的时间。注册和拿 Key 的流程很快这里只讲关键步骤。打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后进入控制台。在「API Keys」页面创建一个新 Key复制保存。这个 Key 后面会用在模型客户端的配置里用来调用模型对话能力。TaoToken 的 API 基地址是 https://taotoken.net/api 兼容常见的对话补全格式。如果你只是想让模型能对话用这个地址加 Key 就够了如果你要跑长期编码任务或 Agent 流程建议看一下 Coding Plan额度更划算。模型对话的入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。注意MCP 服务端和 TaoToken 是两套独立的东西。服务端负责暴露工具TaoToken 负责提供模型通道。两者通过 AI 客户端比如 Claude Desktop 或你自己写的客户端串起来。3. 可复制配置pom.xml 与 MCP 服务端骨架3.1 环境与依赖技术栈要求 JDK 17MCP SDK 最低要求、Spring Boot 3.2.0、MCP Java SDK 0.10.0、Maven 3.6。在 pom.xml 里加依赖properties mcp.sdk.version0.10.0/mcp.sdk.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdio.modelcontextprotocol.sdk/groupId artifactIdmcp-spring-webmvc/artifactId version${mcp.sdk.version}/version /dependency dependency groupIdio.modelcontextprotocol.sdk/groupId artifactIdmcp/artifactId version${mcp.sdk.version}/version /dependency /dependenciesmcp-spring-webmvc 是服务端传输层mcp 是客户端依赖后者用来写测试客户端。3.2 application.ymlserver: port: 8088 spring: application: name: boot-mcp-simpleDemo端口用 8088避免和常见服务冲突。3.3 MCP 核心配置类创建 McpServerConfig.java这是整个服务的核心。它分三层传输层、服务器层、工具层。package com.demo.mcp.config; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider; import io.modelcontextprotocol.spec.McpSchema; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.function.RouterFunction; import org.springframework.web.servlet.function.ServerResponse; import org.springframework.web.servlet.function.support.RouterFunctionMapping; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Random; Configuration public class McpServerConfig { private final Random random new Random(); Bean public WebMvcSseServerTransportProvider mcpTransportProvider(ObjectMapper objectMapper) { return new WebMvcSseServerTransportProvider( objectMapper, /mcp/sse, /mcp/message ); } Bean public RouterFunctionServerResponse mcpRouterFunction( WebMvcSseServerTransportProvider transportProvider) { return transportProvider.getRouterFunction(); } Bean public RouterFunctionMapping mcpRouterFunctionMapping( RouterFunctionServerResponse routerFunction) { return new RouterFunctionMapping(routerFunction); } Bean public McpSyncServer mcpServer(WebMvcSseServerTransportProvider transportProvider) { return McpServer.sync(transportProvider) .serverInfo(boot-mcp-simpleDemo, 1.0.0) .capabilities(McpSchema.ServerCapabilities.builder() .tools(true) .build()) .tool(createCalculatorTool(), (exchange, args) - handleCalculation(args)) .tool(createWeatherTool(), (exchange, args) - handleWeather(args)) .build(); } private McpSchema.Tool createCalculatorTool() { McpSchema.JsonSchema schema new McpSchema.JsonSchema( object, Map.of( operation, Map.of( type, string, description, 操作类型, enum, Arrays.asList(add, subtract, multiply, divide) ), a, Map.of(type, number, description, 第一个数字), b, Map.of(type, number, description, 第二个数字) ), Arrays.asList(operation, a, b), null, null, null ); return new McpSchema.Tool(calculator, 基础计算器支持加减乘除运算, schema); } private McpSchema.CallToolResult handleCalculation(MapString, Object args) { try { String operation (String) args.get(operation); Number a (Number) args.get(a); Number b (Number) args.get(b); double result switch (operation) { case add - a.doubleValue() b.doubleValue(); case subtract - a.doubleValue() - b.doubleValue(); case multiply - a.doubleValue() * b.doubleValue(); case divide - { if (b.doubleValue() 0) throw new ArithmeticException(除数不能为零); yield a.doubleValue() / b.doubleValue(); } default - throw new IllegalArgumentException(未知操作: operation); }; return McpSchema.CallToolResult.builder() .content(List.of(new McpSchema.TextContent(计算结果: result))) .build(); } catch (Exception e) { return McpSchema.CallToolResult.builder() .content(List.of(new McpSchema.TextContent(错误: e.getMessage()))) .isError(true) .build(); } } private McpSchema.Tool createWeatherTool() { McpSchema.JsonSchema schema new McpSchema.JsonSchema( object, Map.of(city, Map.of(type, string, description, 城市名称例如: 北京、上海)), Arrays.asList(city), null, null, null ); return new McpSchema.Tool(getWeather, 获取指定城市的天气信息, schema); } private McpSchema.CallToolResult handleWeather(MapString, Object args) { try { String city (String) args.get(city); String[] conditions {晴, 多云, 阴, 小雨, 中雨}; String condition conditions[random.nextInt(conditions.length)]; int temperature random.nextInt(35) - 5; String weatherInfo String.format(【%s 天气】%s温度: %d°C, city, condition, temperature); return McpSchema.CallToolResult.builder() .content(List.of(new McpSchema.TextContent(weatherInfo))) .build(); } catch (Exception e) { return McpSchema.CallToolResult.builder() .content(List.of(new McpSchema.TextContent(获取天气失败: e.getMessage()))) .isError(true) .build(); } } }每个 MCP 工具由两部分组成Tool Definition 告诉 AI 怎么用工具名、描述、参数 SchemaTool Handler 是实际执行的业务逻辑。参数 Schema 用 JSON Schema 描述AI 会根据它自动填充参数。3.4 启动类package com.demo.mcp; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class McpSimpleDemoApplication { public static void main(String[] args) { SpringApplication.run(McpSimpleDemoApplication.class, args); } }4. 验证请求三种方式跑通工具调用链路4.1 启动服务mvn spring-boot:run启动后日志会输出路由映射和启动耗时。看到Started McpSimpleDemoApplication就说明服务起来了。4.2 方式一Java 客户端测试创建 McpClientDemo.javapackage com.demo.mcp.client; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.spec.McpSchema; import java.util.HashMap; import java.util.Map; public class McpClientDemo { public static void main(String[] args) { String mcpServerUrl http://localhost:8088; ObjectMapper objectMapper new ObjectMapper(); HttpClientSseClientTransport transport HttpClientSseClientTransport.builder(mcpServerUrl).build(); McpSyncClient client McpClient.sync(transport).build(); try { McpSchema.InitializeResult initResult client.initialize(); System.out.println(连接成功: initResult.serverInfo().name()); McpSchema.ListToolsResult toolsResult client.listTools(); for (McpSchema.Tool tool : toolsResult.tools()) { System.out.println( 工具: tool.name() - tool.description()); } MapString, Object args new HashMap(); args.put(operation, add); args.put(a, 10); args.put(b, 5); MapString, Object requestMap new HashMap(); requestMap.put(name, calculator); requestMap.put(arguments, args); McpSchema.CallToolRequest request objectMapper.convertValue(requestMap, McpSchema.CallToolRequest.class); McpSchema.CallToolResult result client.callTool(request); for (var content : result.content()) { if (content instanceof McpSchema.TextContent textContent) { System.out.println(结果: textContent.text()); } } } finally { client.closeGracefully(); } } }运行后输出连接成功: boot-mcp-simpleDemo 工具: calculator - 基础计算器支持加减乘除运算 工具: getWeather - 获取指定城市的天气信息 结果: 计算结果: 15.04.3 方式二Python 脚本验证 SSE 协议如果你不想写 Java 客户端用 Python 直接发 JSON-RPC 请求也能验证。核心是先用 GET 建立 SSE 连接拿到 sessionId再往 message 端点 POST 请求。import requests import json import threading import queue import time BASE_URL http://localhost:8088 def test_mcp_sse(): session_id None message_endpoint None sse_events queue.Queue() sse_connected threading.Event() def sse_listener(): nonlocal session_id, message_endpoint try: sse_response requests.get(f{BASE_URL}/mcp/message, streamTrue, timeout120) if sse_response.status_code 200: print(SSE 连接成功!) for line in sse_response.iter_lines(): if line: decoded_line line.decode(utf-8) sse_events.put(decoded_line) if id: in decoded_line and session_id is None: session_id decoded_line.replace(id:, ).strip() if data: in decoded_line and message_endpoint is None: endpoint decoded_line.replace(data:, ).strip() message_endpoint f{BASE_URL}{endpoint} sse_connected.set() except Exception as e: print(fSSE 错误: {e}) sse_thread threading.Thread(targetsse_listener, daemonTrue) sse_thread.start() sse_connected.wait(timeout5) if not session_id or not message_endpoint: print(未能获取 Session ID) return print(fSession ID: {session_id}) def send_request(request, label): print(f\n[{label}] 发送请求...) response requests.post( message_endpoint, jsonrequest, headers{Content-Type: application/json}, timeout5 ) print(fHTTP 状态: {response.status_code}) timeout time.time() 5 while time.time() timeout: try: line sse_events.get(timeout0.5) if result in line or error in line: json_str line.replace(data:, ).strip() if data: in line else line try: parsed json.loads(json_str) print(f解析结果: {json.dumps(parsed, indent2, ensure_asciiFalse)}) except: pass break except queue.Empty: continue init_request { jsonrpc: 2.0, id: 1, method: initialize, params: { protocolVersion: 2024-11-05, capabilities: {}, clientInfo: {name: test-client, version: 1.0} } } send_request(init_request, 初始化) send_request({jsonrpc: 2.0, method: notifications/initialized}, 已初始化) send_request({jsonrpc: 2.0, id: 2, method: tools/list, params: {}}, 列出工具) calc_request { jsonrpc: 2.0, id: 3, method: tools/call, params: {name: calculator, arguments: {operation: add, a: 10, b: 5}} } send_request(calc_request, 计算器) weather_request { jsonrpc: 2.0, id: 4, method: tools/call, params: {name: getWeather, arguments: {city: 深圳}} } send_request(weather_request, 天气) if __name__ __main__: test_mcp_sse()运行python test_mcp.py你会看到 SSE 事件流和解析后的 JSON-RPC 响应。计算器返回计算结果: 15.0天气返回类似【深圳 天气】多云温度: 23°C。4.4 方式三接入 AI 客户端以 Claude Desktop 为例编辑配置文件{ mcpServers: { boot-mcp-simpleDemo: { type: sse, url: http://localhost:8088/mcp/message } } }配置后重启客户端它会自动发现你的工具。用户说「帮我计算 10 5」客户端会自动调用 calculator 工具并返回结果。Trae 的配置类似类型选 SSEURL 填同样的地址。5. 本篇常见错排查5.1 启动报端口占用netstat -ano | findstr :8088 taskkill /PID 占用的PID /F5.2 SSE 连接 404检查是否注册了 RouterFunctionMapping。这个 Bean 负责把 MCP 端点映射到 Spring MVC漏了它请求会直接 404。5.3 客户端调用失败按顺序检查MCP 服务是否已启动、URL 是否正确http://localhost:8088/mcp/message、服务端日志有没有异常堆栈。如果服务端日志显示收到请求但没响应多半是工具 Handler 里抛了异常被吞掉在 Handler 里加日志确认。5.4 工具参数填错AI 客户端根据 JSON Schema 填参数如果 Schema 里 required 字段和 Handler 里读取的 key 不一致会报参数缺失。检查Arrays.asList(operation, a, b)和args.get(operation)是否对应。5.5 模型侧连不上如果你用 TaoToken 的 API 通道调模型确认 Key 是从控制台复制的完整字符串请求头格式是Authorization: Bearer 你的Key。模型对话入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的请求示例。6. 扩展工具与生产级结构建议当工具数量变多时建议把工具定义和业务逻辑拆到独立的 Handler 类里Config 只负责注册。这样每个工具可以单独测试也方便后续加权限校验和日志。Component public class CalculatorToolHandler { public McpSchema.Tool getTool() { // 工具定义 } public McpSchema.CallToolResult handle(MapString, Object args) { return calculatorService.calculate(args); } }然后在 Config 里注册.tool(calculatorHandler.getTool(), (exchange, args) - calculatorHandler.handle(args))新增工具只需三步定义 Tool工具名、描述、参数 Schema、实现 Handler 业务逻辑、在 mcpServer() 里注册。整个过程不需要改传输层代码。如果你要跑长期编码任务或 Agent 流程建议用 TaoToken 的 Coding Plan额度更充足适合持续调用。模型对话和 API Key 管理都在控制台完成接入文档里有各语言的调用示例。整套链路跑通后你的 Spring Boot 业务系统就正式变成了 AI 可调用的工具集。