MCP 技术原理与 Java 项目落地:用 TaoToken 统一 Key 打通 JSON-RPC 调用链
1. 为什么 Java 项目需要 MCP从 JSON-RPC 到 Spring Boot 的落地场景MCPModel Context Protocol在 Java 项目里到底是个什么东西一句话说清楚它是一套基于 JSON-RPC 2.0 的标准化协议让大模型能以一种统一的方式去调用你项目里的工具和数据源而不是每接一个模型就写一套定制接口。适合谁适合手上已经有 Spring Boot 项目、有 MySQL 数据、想让 AI 助手直接查库出报表但又不想把核心业务代码改得面目全非的后端同学。我先把原理拆开讲不然后面配置会像抄作业。MCP 的架构分三层正好和 Java 的分层设计对得上应用层是 Host也就是你的 Java 业务系统。它接收用户指令调用大模型解析模型返回的 MCP 调用意图再转发给客户端。协议层是 Client负责把指令封装成 JSON-RPC 2.0 格式和 MCP Server 建立通信处理请求和响应的序列化。工具适配层是 Server它去适配具体的工具比如 MySQL、Redis、本地文件执行实际操作后按 MCP 标准返回结果。一次完整的调用链是这样的用户在 Host 里说「查询上月华东区销售数据并生成报表」Host 把这句话交给大模型模型识别出需要调用 MySQL 查询返回一个 MCP 调用指令。Java MCP Client 把指令封装成 JSON-RPC 格式比如{ jsonrpc: 2.0, method: mysql/query, params: { sql: SELECT region, SUM(amount) FROM sales WHERE ..., dataSourceName: sales-ds }, id: a1b2c3d4 }Client 通过 HTTP 把这段发给 MCP ServerServer 执行 SQL把结果按 MCP 标准返回结构化 JSONClient 解析后回传 HostHost 再让大模型生成报表文案最终返回给用户。为什么 Java 生态适配起来很顺因为 JSON-RPC 用 Jackson 或 Gson 就能序列化通信层可以复用 Spring Boot 的 RestTemplate 或 Netty工具适配层可以直接封装成 Spring Bean安全层还能挂 Spring Security 做权限控制。这意味着你不需要推翻现有架构MCP 是「贴」上去的不是「换」上去的。但这里有个现实问题调用链里每一环都要跟模型服务通信如果每个模块各自维护一套 Key 和 Base URL配置会散得到处都是排查问题时要翻好几个文件。这就是为什么我在落地时选择用 TaoToken 统一 Key——一个 Key 管住所有模型调用入口Client 和 Host 层不用各自记一套凭证。下面从环境准备开始一步步把这条链路跑通。2. TaoToken 前置准备统一 Key 与 Base URL 配置在写代码之前先把「钥匙」准备好。MCP 调用链里Host 层需要调用大模型来解析用户意图、生成 SQL这一步是要走模型 API 的。如果你项目里同时有多个模块要调模型每个模块都配一套 Key后面换模型或者加额度时会很痛苦。TaoToken 的作用就是把这些调用统一到一个入口。你需要准备三样东西Base URL、API Key、Model ID。这三件套在后面的 Spring Boot 配置里会反复出现先记牢。Base URL 用https://taotoken.net/api注意这个地址不带任何查询参数直接作为根路径使用。API Key 需要你去控制台生成路径是 API Keys 页面生成后复制保存它只会完整显示一次。Model ID 根据你实际要用的模型填比如做 SQL 生成这种结构化输出任务选一个指令跟随能力强的模型即可。具体操作步骤打开 https://taotoken.net/api-keys 登录后点创建 Key给它起个名字比如mcp-java-demo然后复制那串以sk-开头的字符串。接着确认你要用的模型 ID可以在模型对话页面先试一下输入一句「把这句话转成 MySQL SELECT 语句查询上月华东区销售总额」看返回是否符合预期。确认没问题后把 Model ID 记下来。这里有个容易踩的坑很多人把 Base URL 写成带/v1或者带具体路径的形式结果请求 404。正确做法是 Base URL 只写到/api具体的路径由 SDK 或你的 HTTP 客户端拼接。比如 OpenAI 兼容的调用方式是{Base URL}/v1/chat/completions所以最终请求地址是https://taotoken.net/api/v1/chat/completions。如果你用的是 Claude Code 这类编码工具配置方式略有不同需要填 Anthropic 风格的 Base URL 和 Key可以参考接入文档里的说明。文档地址是 https://taotoken.net/doc 里面有各语言的示例。准备好这三件套后把它们写进 Spring Boot 的配置文件。我建议不要硬编码在 Java 代码里而是放在application.yml中通过ConfigurationProperties注入。这样本地、测试、生产环境可以切换不同的 Key也方便做密钥轮换。taotoken: base-url: https://taotoken.net/api api-key: sk-你的实际Key model-id: 你的模型ID注意api-key这一行在实际项目里不要提交到 Git用环境变量覆盖比如TAOTOKEN_API_KEY。Spring Boot 支持${TAOTOKEN_API_KEY}这种占位符写法。到这里前置准备就完成了。接下来进入代码环节我会给出完整的 MCP Server 配置片段、Spring Boot 依赖和 Bean 注册代码你可以直接复制到项目里改改就能跑。3. 可复制配置MCP Server 与 Spring Boot 集成片段这一节是全文的核心我会把 MCP Server 的配置、Spring Boot 依赖、Bean 注册、以及调用 TaoToken 的客户端代码全部给出来。你按顺序复制注意路径和包名改成你自己的。先看pom.xml依赖。除了 Spring Boot Web 和 MySQL 驱动还需要 JSON-RPC 库和 Jackson。这里我用jsonrpc4j做协议封装用spring-boot-starter-web提供 HTTP 容器。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.github.briandilley.jsonrpc4j/groupId artifactIdjsonrpc4j/artifactId version1.6.0/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependency /dependencies然后是 MCP 的核心模型类。MCP 请求体遵循 JSON-RPC 2.0加上 MCP 自己的扩展字段。我定义三个类MCPRequest、MCPParams、MCPResponse。public class MCPRequest { private String jsonrpc 2.0; private String id; private String method; private MCPParams params; // getter/setter 省略 } public class MCPParams { private String sql; private String dataSourceName; private ListString allowedOperations; // getter/setter 省略 } public class MCPResponse { private String jsonrpc 2.0; private String id; private Object result; private MCPError error; // getter/setter 省略 }接下来是 MCP Server它负责接收 JSON-RPC 请求、校验权限、执行 SQL、返回结构化结果。注意权限校验这一段只允许 SELECT这是防止 AI 误删数据的关键。RestController RequestMapping(/mcp/server/mysql) public class MysqlMCPServer { Autowired private DataSource dataSource; PostMapping(/execute) public ResponseEntityMCPResponse executeMysqlQuery(RequestBody MCPRequest request) { MCPResponse response new MCPResponse(); response.setId(request.getId()); try { MCPParams params request.getParams(); if (params null || !params.getSql().trim().toUpperCase().startsWith(SELECT)) { MCPError error new MCPError(); error.setCode(-32602); error.setMessage(AI仅允许执行SELECT查询); response.setError(error); return ResponseEntity.badRequest().body(response); } try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(params.getSql()); ResultSet rs ps.executeQuery()) { ListMapString, Object resultList new ArrayList(); ResultSetMetaData metaData rs.getMetaData(); int columnCount metaData.getColumnCount(); while (rs.next()) { MapString, Object row new HashMap(); for (int i 1; i columnCount; i) { row.put(metaData.getColumnName(i), rs.getObject(i)); } resultList.add(row); } response.setResult(resultList); return ResponseEntity.ok(response); } } catch (Exception e) { MCPError error new MCPError(); error.setCode(-32603); error.setMessage(执行SQL失败 e.getMessage()); response.setError(error); return ResponseEntity.internalServerError().body(response); } } }然后是 MCP Client它负责把 Host 的调用意图封装成 JSON-RPC 请求发给 MCP Server。这里用 RestTemplate 发 HTTP 请求。Component public class MCPClient { Autowired private RestTemplate restTemplate; private static final String MYSQL_MCP_SERVER_URL http://localhost:8080/mcp/server/mysql/execute; public ListMapString, Object callMysqlQuery(String sql) { MCPRequest request new MCPRequest(); request.setId(UUID.randomUUID().toString()); request.setMethod(mysql/query); MCPParams params new MCPParams(); params.setSql(sql); params.setDataSourceName(sales-ds); params.setAllowedOperations(Collections.singletonList(SELECT)); request.setParams(params); MCPResponse response restTemplate.postForObject( MYSQL_MCP_SERVER_URL, request, MCPResponse.class); if (response null || response.getError() ! null) { String errorMsg response ! null ? response.getError().getMessage() : MCP Server无响应; throw new RuntimeException(MCP调用失败 errorMsg); } return (ListMapString, Object) response.getResult(); } }最后是 Host 层它调用 TaoToken 的模型接口把自然语言转成 SQL再通过 MCP Client 查库。这里我用 RestTemplate 直接调 OpenAI 兼容接口Base URL 和 Key 从配置文件读。RestController RequestMapping(/ai/assistant) public class AIAssistantController { Autowired private MCPClient mcpClient; Value(${taotoken.base-url}) private String baseUrl; Value(${taotoken.api-key}) private String apiKey; Value(${taotoken.model-id}) private String modelId; Autowired private RestTemplate restTemplate; PostMapping(/sales-report) public ResponseEntityString generateSalesReport(RequestParam String userQuery) { try { String sql generateSqlFromLLM(userQuery); ListMapString, Object salesData mcpClient.callMysqlQuery(sql); return ResponseEntity.ok(查询到 salesData.size() 条数据); } catch (Exception e) { return ResponseEntity.internalServerError().body(失败 e.getMessage()); } } private String generateSqlFromLLM(String userQuery) { String prompt 你是SQL生成助手仅生成SELECT语句表sales字段id, region, amount, sale_date。 用户需求 userQuery 只返回SQL。; HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(apiKey); MapString, Object body new HashMap(); body.put(model, modelId); body.put(messages, List.of( Map.of(role, user, content, prompt) )); HttpEntityMapString, Object entity new HttpEntity(body, headers); ResponseEntityMap resp restTemplate.postForEntity( baseUrl /v1/chat/completions, entity, Map.class); ListMapString, Object choices (ListMapString, Object) resp.getBody().get(choices); MapString, Object message (MapString, Object) choices.get(0).get(message); return message.get(content).toString().trim(); } }Bean 注册部分把 RestTemplate 和数据源配好Configuration public class MCPConfig { Bean public RestTemplate restTemplate() { return new RestTemplate(); } Bean ConfigurationProperties(prefix spring.datasource) public DataSource dataSource() { return DataSourceBuilder.create().build(); } }配置文件application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/sales_db?useUnicodetruecharacterEncodingutf8useSSLfalse username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} model-id: 你的模型ID server: port: 8080这套配置里Base URL、Key、Model ID 三件套都齐了。注意api-key用环境变量注入不要写死在文件里。到这里MCP Server、Client、Host 三层就都注册好了。4. 验证请求跑通 JSON-RPC 调用链并看到成功结果配置写完后别急着上生产先在本地把链路跑通。验证分三步先单独测 MCP Server再测模型调用最后测完整链路。第一步启动 Spring Boot 项目确认 MySQL 里sales表有数据。你可以先手动插几条INSERT INTO sales (region, amount, sale_date) VALUES (华东区, 12000.50, 2025-08-15), (华东区, 8000.00, 2025-08-20), (华南区, 9500.00, 2025-08-18);第二步用 curl 直接打 MCP Server 的接口验证 JSON-RPC 格式是否正确。这一步不经过模型纯测协议层。curl -X POST http://localhost:8080/mcp/server/mysql/execute \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, id: test-001, method: mysql/query, params: { sql: SELECT region, SUM(amount) AS total FROM sales GROUP BY region, dataSourceName: sales-ds, allowedOperations: [SELECT] } }如果返回类似下面的结构说明 MCP Server 正常{ jsonrpc: 2.0, id: test-001, result: [ {region: 华东区, total: 20000.50}, {region: 华南区, total: 9500.00} ], error: null }再测一下权限拦截把 SQL 换成 DELETEcurl -X POST http://localhost:8080/mcp/server/mysql/execute \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, id: test-002, method: mysql/query, params: {sql: DELETE FROM sales, dataSourceName: sales-ds} }应该返回-32602错误提示只允许 SELECT。这一步过了说明安全控制生效。第三步测完整链路。调用 Host 层接口curl -X POST http://localhost:8080/ai/assistant/sales-report?userQuery查询各区域销售总额预期结果是返回「查询到 N 条数据」。如果这一步报错大概率是模型调用出了问题往下看第五节排查。第四步验证模型调用是否真的走了 TaoToken。你可以在generateSqlFromLLM里加一行日志打印baseUrl和modelId确认请求地址是https://taotoken.net/api/v1/chat/completions。也可以去模型对话页面手动发一句同样的话对比返回的 SQL 是否一致。实测下来整条链路跑通后从用户输入自然语言到拿到数据库结果耗时主要花在模型生成 SQL 上本地网络下大概 1 到 3 秒。MCP Server 执行 SQL 本身很快几十毫秒级别。如果你想让链路更完整可以在 Host 层加上 Excel 生成逻辑用 POI 把salesData写成 xlsx 文件。这部分代码和 MCP 无关我就不展开了你项目里应该已经有现成的工具类。5. 常见报错排查401、local proxy failed、reading choices 与 OAuth链路跑不通时报错信息往往很模糊。我把几个高频错误和对应排查路径列出来你对照着看。401 Unauthorized这个最常见说明 Key 没传对或者失效了。先检查application.yml里的api-key是否被环境变量正确覆盖打印一下实际值的前几位。然后确认请求头是Authorization: Bearer sk-xxx注意 Bearer 后面有一个空格。如果 Key 是从控制台复制的确认没有多余换行。还有一种情况是 Base URL 写错了比如写成了https://taotoken.net/api/带尾斜杠拼接后变成双斜杠某些网关会拒绝。正确写法是https://taotoken.net/api不带尾斜杠。local proxy failed这个报错通常出现在你本地网络环境有代理设置但请求没走通。先检查系统环境变量HTTP_PROXY和HTTPS_PROXY是否指向了一个不可用的地址。如果你不需要代理把它们清空。另外检查 Spring Boot 的 RestTemplate 是否被全局代理配置影响可以在 Bean 里显式设置SimpleClientHttpRequestFactory不继承系统代理。reading choices 报错 / NullPointerException这个错误说明模型返回的 JSON 结构和你解析的字段对不上。常见原因是模型返回了错误信息而不是正常响应比如额度不足、模型 ID 不存在。你可以在解析前先打印完整的resp.getBody()看看里面是choices还是error。如果是error里面会有message字段说明原因。另一个原因是choices数组为空比如模型被内容安全策略拦截了这时候要检查你的 prompt 是否包含敏感词。OAuth 相关报错如果你用的是 Claude Code 或者某些需要 OAuth 授权的工具报错可能提示 token 过期或授权失败。这类工具不走简单的 API Key而是走 OAuth 流程。排查方法是确认你的授权是否还在有效期内必要时重新走一遍授权。如果你在 MCP 配置里同时填了 API Key 和 OAuth 凭证可能会冲突建议只保留一种。MCP Server 返回 -32603这是 SQL 执行失败。先看错误信息里的具体原因常见的是表名写错、字段不存在、或者数据源连接不上。你可以在MysqlMCPServer的 catch 块里把e.printStackTrace()打出来定位到具体哪一行。如果是连接池耗尽检查spring.datasource的maximum-pool-size配置。模型生成的 SQL 语法错误这不是 MCP 的问题是 prompt 没写好。你可以在 prompt 里加一句「只返回 SQL不要 markdown 代码块标记」因为有些模型会返回 sql 包裹的内容直接拿去执行会报语法错误。另外在generateSqlFromLLM里加一层清洗去掉首尾的代码块标记。排查时有个通用技巧把链路拆开先单独测 MCP Server再单独测模型调用最后合起来测。这样能快速定位是哪一层的问题。我试过把三层混在一起调报错信息互相干扰拆开测效率高很多。6. 从本地跑通到长期使用MCP 在 Java 项目里的接入建议链路跑通只是第一步真正要长期用起来还得考虑几个工程问题。第一把 MCP 相关代码封装成独立模块。不要把MysqlMCPServer、MCPClient直接塞进主业务包而是建一个mcp-starter模块通过 Spring Boot 自动配置引入。这样其他项目要接 MCP加个依赖就行不用复制粘贴。模块里用ConditionalOnProperty控制是否启用避免不需要的项目被强制加载。第二权限控制要细粒度。现在只做了「只允许 SELECT」这一层实际企业场景可能需要按用户、按 AI 应用限制可访问的表和字段。你可以在MCPParams里加一个userId字段在 Server 层查权限表动态拼接 WHERE 条件。审计日志也要加上记录每次 MCP 调用的 SQL、调用方、时间戳方便追溯。第三限流和降级。AI 调用可能突发流量如果直接打到数据库容易把连接池打满。用 Resilience4j 或 Sentinel 在 MCP Server 层加限流超过阈值直接返回错误保护后端。数据库查询本身也要加超时避免慢 SQL 拖垮整个链路。第四Key 管理要规范。TaoToken 的 Key 不要写死在代码或配置文件里用环境变量或配置中心注入。如果团队多人协作每个人用不同的 Key方便按人统计用量和排查问题。Key 要定期轮换轮换时通过配置中心热更新不用重启服务。第五扩展其他工具适配。MySQL 只是第一个场景你可以照着MysqlMCPServer的结构写RedisMCPServer、FileMCPServer、HttpApiMCPServer。每个 Server 只负责一种工具通过不同的method前缀区分比如redis/get、file/read、api/call。Client 层根据 method 路由到对应的 Server 地址。如果你后面要接编码类 Agent比如让 AI 直接改代码、跑测试那调用量会比查数据库大很多建议单独用 Coding Plan 的额度和日常查询的 Key 分开避免互相影响。配置方式在 https://taotoken.net/coding-plan 有说明。最后说一个实际经验MCP 的价值不在于协议本身多复杂而在于它把「模型调用工具」这件事标准化了。你项目里原来可能有一堆为 AI 定制的接口每个接口的入参出参都不一样维护起来很累。换成 MCP 后所有工具调用都走同一套 JSON-RPC 格式Client 和 Server 解耦加新工具只需要加一个 Server 实现Client 不用改。这是它值得在 Java 项目里落地的根本原因。