MCP SDK client 开发:这次用 TaoToken 让 Codex 把 io.modelcontextprotocol.sdk 的初始化顺序走通
2026/9/14 20:09:48 网站建设 项目流程

1. 排障现场:让 Codex 用 TaoToken 读懂 MCP Client 的初始化死角

先交代一个很现实的背景:用io.modelcontextprotocol.sdk写 MCP Client,报错最多的地方往往不在callTool,而在最前面那几步。你按原文搭好了McpSyncClientStdioClientTransport,也照着把security-ai-mcp-demo构建出来,但在 IDEA 里一跑,要么Server jar 不存在,要么school_list返回的 content 里success:falseisError()却是 false。这时候如果只盯着McpSyncClient的 API 看,很容易绕进去:明明连接建立成功,工具列表也能列出 15 个,为什么业务结果还是失败?

我自己踩过这个坑之后,调整了排障方式:先让 Codex 把io.modelcontextprotocol.sdk 0.12.1的初始化顺序逐行讲清楚,再对照日志排查。而要让 Codex 稳定可用,我选择在 TaoToken 创建一把 Key,把模型接口 Base URL 配成 https://taotoken.net/api。这样既不用同时维护多个厂商的 Key,也能在排障时把日志直接丢给模型分析。

TaoToken 在这里不是替你运行 Java 进程,它只是让 Codex 拥有稳定的模型通道。MCP Client 的启动、握手、调用,仍然发生在你的本地 JVM 里。等配通了之后,Codex 请求日志里的 Token 消耗就是你验证这把 Key 可用的直接证据。

2. 准备材料:TaoToken Key 与本地 MCP 项目

写代码前先准备好三样东西:

  1. 一个能通过模型接口「看懂日志并给出排查建议」的 Codex 配置;
  2. 一个可构建的 MCP Client 工程,pom 里引入io.modelcontextprotocol.sdk0.12.1;
  3. 一个事先构建好的 security-ai-mcp-demo jar,否则StdioClientTransport启动的是一个不存在的命令。

先解决第一样。打开 TaoToken 注册并创建 API Key。拿到 Key 之后,在 Codex 的配置里把模型接口 Base URL 填成https://taotoken.net/api,注意末尾不要加/v1,也不要把带 UTM 的官网地址填进去。TaoToken 在这里的角色是「统一 API 兼容通道」:Codex 通过这个地址访问模型,你不需要维护多个厂商的 Key,也不用担心某个模型额度用完导致整个排查中断。

第二样直接看下面的 Maven 配置。原文用的是 Spring Boot 3.2.5 + Java 17,MCP SDK 版本固定在 0.12.1,我用 mcp-bom 统一管理版本,避免mcpmcp-spec等子模块版本漂移:

<properties> <java.version>17</java.version> <mcp.version>0.12.1</mcp.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp-bom</artifactId> <version>${mcp.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp</artifactId> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.9</version> </dependency> </dependencies>

第三样是 security-ai-mcp-demo 的 jar。原文路径是../security-ai-mcp-demo/target/security-ai-mcp-demo-1.0-SNAPSHOT.jar,下面的代码保留同样路径,并支持用-Dmcp.server.jar=绝对路径覆盖。这个设计不是可有可无:多人协作时,每个人的工作目录可能不同,写死相对路径早晚会踩 jar 不存在的坑。

2.1 关键依赖版本说明

MCP Java SDK 在 0.12.1 里,McpClient.sync(...)返回的是McpSyncClient,支持同步调用。这个版本的特点是初始化必须显式调用initialize(),不会在build()时自动握手。很多人把build()当成连接完成,结果调用listTools()拿到空列表,其实协议根本没握手。

另一个观察点是StdioClientTransport的日志都打到 STDERR。MCP Client 自己的业务日志走 stdout,Server 的启动日志走 stderr。排障时不要只看 IDEA 控制台最后的输出,要同时看 STDERR 段有没有 Spring Boot 启动日志。原文那段STDERR Message received里就藏着 Server 是否成功启动、是否打印Server is ready的关键信息。

3. Codex 接入 TaoToken:在 config.toml 里写对 Base URL

Codex 的接入方式不止一种。如果你用的是 OpenAI Codex CLI,通常在~/.codex/config.toml里配置model_provider。把 Base URL 指向 TaoToken 的兼容通道,Key 用刚创建的 TaoToken Key。

# ~/.codex/config.toml model = "以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 模型广场当时列表为准" model_provider = "taotoken" [model_providers.taotoken] name = "taotoken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"

配置里用到的TAOTOKEN_API_KEY环境变量,值就是 YOUR_API_KEY。这个 Key 同样从 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 创建。注意:base_url填的是 https://taotoken.net/api,不是官网地址,末尾也不加 /v1。

配好后,可以用一个最简单的提示词验证连通性:让 Codex 解释下面这个StdioClientTransport日志里Server is ready的含义。如果 Codex 能正常回答,说明模型通道已经通了,接下来才能真正帮你排障。

3.1 为什么不让 Codex 直接启动你的本地 Java 进程

严格说,Codex 是命令行编程助手,它可以读取项目里的 pom.xml、Java 源文件和日志文件,也可以给出修改建议,但「启动一个会与本地 stdin/stdout 交互的 Java 子进程」这种操作,不同环境的行为差异很大。更稳妥的做法是:你负责在本地执行mvn packagejava -jar,Codex 负责读日志、判断 jar 路径对不对、检查school_list的入参结构。TaoToken 在这里只负责让 Codex 有模型可用,不负责替你执行本地命令。

4. 初始化顺序排障:从 StdioClientTransport 到 callTool

Codex 接入后,让它对照下面这段代码逐行走流程。这是按照原文SecurityAiMcpClientDemo改写的可运行版本,初始化顺序保持不变,但把 jar 判断、错误输出、结果解析拆得更清楚,方便和 STDERR 日志一一对应:

package com.demo.mcp.client; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.transport.ServerParameters; import io.modelcontextprotocol.client.transport.StdioClientTransport; import io.modelcontextprotocol.spec.McpSchema; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.util.List; import java.util.Map; public class SecurityAiMcpClientDemo { private static final String DEFAULT_SERVER_JAR = "../security-ai-mcp-demo/target/security-ai-mcp-demo-1.0-SNAPSHOT.jar"; public static void main(String[] args) throws Exception { String serverJar = System.getProperty("mcp.server.jar", DEFAULT_SERVER_JAR); Path jarPath = Paths.get(serverJar).toAbsolutePath().normalize(); if (!jarPath.toFile().exists()) { System.err.println("Server jar 不存在: " + jarPath); System.err.println("请先构建 security-ai-mcp-demo,或使用 -Dmcp.server.jar=绝对路径"); System.exit(1); } ServerParameters params = ServerParameters.builder("java") .args("-jar", jarPath.toString()) .build(); McpSyncClient client = McpClient.sync(new StdioClientTransport(params)) .requestTimeout(Duration.ofSeconds(30)) .build(); // 1. 初始化:必须等待 Server 返回协议版本 client.initialize(); System.out.println("已连接 MCP Server,协议已初始化。"); // 2. 列出工具 McpSchema.ListToolsResult listResult = client.listTools(); List<McpSchema.Tool> tools = listResult != null && listResult.tools() != null ? listResult.tools() : List.of(); System.out.println("工具数量: " + tools.size()); tools.forEach(t -> System.out.println(" - " + t.name() + ": " + t.description())); // 3. 调用 school_list McpSchema.CallToolRequest callReq = McpSchema.CallToolRequest.builder() .name("school_list") .arguments(Map.of( "appKey", "demo-key-001", "pageNum", 1, "pageSize", 5)) .build(); McpSchema.CallToolResult callResult = client.callTool(callReq); System.out.println("school_list 调用结果:"); System.out.println(" isError: " + callResult.isError()); if (callResult.content() != null && !callResult.content().isEmpty()) { callResult.content().forEach(c -> { if (c instanceof McpSchema.TextContent) { McpSchema.TextContent tc = (McpSchema.TextContent) c; System.out.println(" content: " + tc.text()); } }); } else if (callResult.isError()) { System.out.println(" error: " + callResult); } } }

这段代码的每一步都对应一条日志:

  • ServerParameters拼出java -jar 绝对路径.jar,如果 jar 路径不对,STDERR 会直接出现找不到文件的报错;
  • initialize()之前,McpSyncClient 不会知道 Server 支持哪些工具;
  • listTools()返回 15 个工具,说明协议握手成功;
  • callTool返回的isError只代表 JSON-RPC 层是否有异常,不代表业务成功。

4.1 school_list 返回 success:false 的排查方法

把下面这段真实日志贴给 Codex,请它判断问题出在哪一层:

school_list 调用结果: isError: false content: {"data":{"records":[...]},"code":200,"success":false}

这里最容易误导人:isError: false,HTTP 那层code也是 200,但业务字段success是 false。Codex 会提醒你,这是 security-ai-mcp-demo 的业务规则,不是 MCP 协议错误。你需要去 Server 端确认appKey是否有效,以及school_list在什么条件下会返回success:false

我实际遇到的情况是,appKey传了demo-key-001,但 Server 端校验规则要求 appKey 必须与已注册的密钥一致,日志里却没有打印校验失败的明细。后来让 Codex 对照 Server 源码里的StdioMcpConfigschool_list实现,才定位到是配置类没有加载校验规则。

如果你也想用同样思路排查,不用自己死磕日志。把上面的代码和日志整理成一个 Markdown 文档,让 Codex 扮演一个熟悉io.modelcontextprotocol.sdk的同事,请它逐行解释初始化顺序,并指出哪个环节会产生Server is ready日志。Codex 的回答会直接引用McpAsyncServerStdioClientTransport的行为,比搜索引擎翻帖子快。

5. 把 Codex 请求日志里的 Token 消耗当作连通性验证

跑通一次school_list之后,另一个容易被忽略的动作是:检查 Codex 请求日志里的 Token 消耗。这一步不是为了看费用,而是验证「TaoToken Key -> Codex -> 模型」整条链路确实在工作。

具体做法很简单:在 Codex 里问一个与 MCP Client 相关的问题,比如「请检查我的 ServerParameters 是否缺少环境变量传递」,然后到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 控制台查看用量记录。如果这次请求产生了 Token 记录,说明 Key 已经真实生效,Base URL 和模型 ID 都没配错。

这里有一个常见误区:有人把官网地址当作 Base URL 填进 Codex,结果请求打到了网页而不是接口。TaoToken 的官网落地页只用于注册、创建 Key、查看模型广场和用量统计;真正填进 Codex 的必须是 https://taotoken.net/api,后面不带 /v1。

控制台用量记录里能看到每次请求的模型、Token 数和时间。建议跑完 MCP Client 后养成习惯:先看 Codex 有没有正常回答,再看请求日志里有没有新增 Token 消耗。只回答不记账,说明走的不是这把 Key;只记账不回答,说明模型通道有问题。两个都正常,才能放心让 Codex 继续参与后续排障。

5.1 验证时顺手检查的四个点

  1. ~/.codex/config.tomlenv_key是否指向了包含 TaoToken Key 的环境变量;
  2. 环境变量是否在启动 Codex 的终端里导出,别配在.env里但忘了 source;
  3. Base URL 是否误写成带/v1或带 UTM 参数的官网地址;
  4. 模型 ID 是否与 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 模型广场当时的列表一致。

前三点出错时会表现为 401、404 或连接超时,最后一点出错时可能表现为模型不存在。排障时把报错原文发给 Codex,它能很快定位到是哪一类问题。

6. 常见排障场景对照:jar 不存在、success:false、STDERR 静默

整理三个我在这个 demo 里实际遇到过、原文日志里也隐含了对应信息的问题。

6.1 Server jar 不存在

错误信息通常是:

Server jar 不存在: C:\mydemo\security-ai-mcp-demo\target\security-ai-mcp-demo-1.0-SNAPSHOT.jar

原因不复杂:先执行mvn package,确保target目录下真的生成了 jar。在 IDEA 里直接运行主类时,工作目录默认是工程根目录,但../security-ai-mcp-demo/target/...这个相对路径是从当前工作目录往上跳一级,所以你需要确认 ai-client-demo 和 security-ai-mcp-demo 确实在同一个父目录下。

让 Codex 帮你排查时,直接把项目目录结构粘贴给它,它会建议用绝对路径避免歧义。最后用的命令是:

java -Dmcp.server.jar=C:/mydemo/security-ai-mcp-demo/target/security-ai-mcp-demo-1.0-SNAPSHOT.jar \ -jar security-ai-mcp-client-demo-1.0-SNAPSHOT.jar

6.2 school_list 返回 success:false

现象是isError: false,但 content 里success: false。如果你和原文一样用的是demo-key-001,先不要怀疑 MCP 协议,去 Server 端确认这个 appKey 是否存在、是否被禁用。因为 code 是 200,说明请求已经到达业务层,MCP Client 本身没有协议层问题。

这时把McpClientService里的DEFAULT_APP_KEY换成一个在 Server 端真实有效的 appKey,重新调用。如果 Server 端没有独立的 appKey 表,就去application.yml里看StdioMcpConfig写了什么校验逻辑。

6.3 STDERR 一直静默,没有任何 Server 日志

如果StdioClientTransport启动后 STDERR 一条日志都没有,通常不是 MCP Server 的问题,而是 java 命令没找到。Windows 下可能是java不在 PATH 里,或者项目用了 Java 21 但终端里默认的是 Java 17。让 Codex 生成一条诊断命令:

java -version

然后把输出贴给它,它会告诉你版本和路径是否匹配。原文日志里明确写了using Java 21.0.8,所以本地 JDK 最好也是 21 或更高。如果用 17,Tomcat 可能能启动,但某些字节码版本会报UnsupportedClassVersionError

7. 把初始化顺序固化成注释,减少同类排障

排障结束后,把 Codex 给出的排查结论写进main()前面的注释里。不是因为注释能解决问题,而是下次再看这段代码,你不会再把build()当成连接完成。

// 初始化顺序说明: // 1. mvn package 先构建 security-ai-mcp-demo,生成 target/xxx.jar // 2. StdioClientTransport 负责启动 java -jar,Server 日志输出到 STDERR // 3. client.initialize() 完成协议握手,之后 listTools() 才有内容 // 4. callTool 返回的 isError 只代表 JSON-RPC 层错误,业务成败看 content 里的 success

写完后跑一次http://localhost:8080/getRemoteSchools?pageNum=1&pageSize=5。如果能正常返回 JSON,你的 MCP Client 开发就走通了。这条链路里,TaoToken 解决的是「让 Codex 能看懂这些日志并及时给出修改建议」的问题,MCP Client 本身仍然运行在你的本地 JVM 中。

配好后,可以在 TaoToken 模型对话 里用同一把 Key 发一条测试消息,确认模型 ID 和 Base URL 没填错。如果后续要长期用 Codex 写代码,可以打开 Coding Plan 看套餐是否够用;Key 在 控制台 API Keys 创建。Codex 的环境变量配置也可以参考 TaoToken 接入文档 里对 Base URL 的说明,但核心就一句:官网地址用于注册和看用量,https://taotoken.net/api 才是填进工具的接口地址。

MCP Client 的坑大多不在 SDK API,而在「你以为是协议问题,其实是业务问题」和「你以为是业务问题,其实是 jar 没构建」这两种误判之间反复横跳。把 Codex 接上 TaoToken 之后,相当于多了一个能同时读代码和日志的排查搭档,但最终执行mvn package、运行java -jar、把报错贴回对话的人,仍然是你自己。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询