2026/10/4 14:38:33

LangChain4j 1.x 核心源码剖析-MCP篇:McpClient 与 McpTransport 的 TaoToken 接入实践

LangChain4j 1.x 核心源码剖析-MCP篇:McpClient 与 McpTransport 的 TaoToken 接入实践 1. 从一次工具调用超时说起McpClient 与 McpTransport 到底谁在管网络如果你在用 LangChain4j 1.x 接 MCP 服务大概率遇到过这种场景本地 stdio 起的 MCP Server 跑得好好的换成远程 HTTP 端点就卡在initialize不动日志里只有一行local proxy failed或者干脆静默超时。我试过把HttpMcpTransport的 timeout 从默认值一路调到 60 秒问题依旧——因为根因不在超时而在McpTransport的 SSE 通道建立顺序和McpClient的初始化握手节奏没对齐。先把这两个核心类的关系讲清楚。McpClient是 LangChain4j 对 MCP 协议的语义层封装它定义的是「我要列工具、我要执行工具、我要读资源、我要渲染提示」这些业务动作而McpTransport是传输层抽象负责把这些语义动作翻译成 JSON-RPC 消息再通过 HTTPSSE 或 stdio 送出去。换句话说McpClient不碰 socketMcpTransport不懂工具语义两者通过McpOperationHandler和McpTransport接口解耦。这个解耦设计带来一个很实际的后果你换 MCP 服务端地址改的是 Transport 的 baseUrl不是 Client 的代码。这正是把 MCP 服务端指向 TaoToken 统一通道的切入点——TaoToken 提供 OpenAI 兼容的 API 入口https://taotoken.net/api你只需要让HttpMcpTransport的请求打到这个入口McpClient侧的工具调用逻辑一行不用动。本文要交付的东西很具体一份可复制的McpTransport配置片段含 Base URL、Key、Model ID 三件套一段DefaultMcpClient初始化代码以及一次完整的listToolsexecuteTool验证动作和预期返回。适合已经在用 LangChain4j 1.x、想把 MCP 服务端从本地 stdio 迁到统一 API 通道的 Java 开发者。如果你还没跑通第一个 MCP Demo建议先补一下McpToolProvider的基础用法再回来。需要提前说明的是MCP 协议本身是 Anthropic 主导的开放协议LangChain4j 的 MCP 模块是对该协议的 Java 实现。TaoToken 在这里扮演的角色是统一的模型 API 通道让 MCP 服务端在需要调用底层模型时有一个稳定的入口而不是让你去直连各家厂商。理解这一点后面的配置才不会拧巴。2. TaoToken 前置准备Key、Base URL 与 Model ID 三件套在动McpTransport之前先把 TaoToken 侧的三样东西拿到手否则后面配置片段里的占位符你没法替换。第一样是API Key。访问https://taotoken.net/api-keys登录后创建一个新的 Key。建议按项目命名比如langchain4j-mcp-demo方便后面排查是哪个调用方出的问题。Key 只在创建时完整显示一次复制后先存到环境变量里别硬编码进代码——后面配置片段我会用${TAOTOKEN_API_KEY}这种形式。第二样是Base URL。TaoToken 的 API 入口是https://taotoken.net/api注意这里不带任何 UTM 参数因为它是给程序调用的端点不是给人点的链接。很多人在这一步踩坑把带?utm_source...的官网地址直接填进baseUrl结果请求路径变成/api?utm_source.../v1/chat/completions服务端直接 404。记住程序用的 Base URL 就是干净的https://taotoken.net/api。第三样是Model ID。这个取决于你的 MCP 服务端在工具执行时实际要调哪个模型。TaoToken 的模型列表可以在控制台里查常见的有claude-sonnet-4-5、gpt-4o这类。MCP 场景下Model ID 通常不是给McpClient直接用的而是给 MCP Server 内部调用模型时用的——但如果你用的是 LangChain4j 里那种「MCP 工具 本地 ChatModel」的组合那 Model ID 就要同时配在 ChatModel 上。把这三样整理成一张对照表后面配置时直接查配置项值用途常见错误Base URLhttps://taotoken.net/apiTransport 请求根路径误加 UTM 参数导致 404API Key控制台创建形如sk-...请求鉴权硬编码进 Git 仓库Model ID如claude-sonnet-4-5ChatModel / MCP Server 内部调用填了不存在的模型名导致 400如果你打算长期跑编码类 Agent建议顺手看一下 Coding Plan 的入口https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite它和按量计费的 Key 是两套体系别混用。本文的验证流程用普通 API Key 就够了。环境变量设置Linux/macOSexport TAOTOKEN_API_KEYsk-你的实际Key export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_MODEL_IDclaude-sonnet-4-5Windows PowerShell$env:TAOTOKEN_API_KEYsk-你的实际Key $env:TAOTOKEN_BASE_URLhttps://taotoken.net/api $env:TAOTOKEN_MODEL_IDclaude-sonnet-4-5设完之后用echo $TAOTOKEN_BASE_URL确认一下别设了个空值自己不知道。这一步看着简单但后面 401 报错十有八九是这里 Key 没生效。3. 可复制配置HttpMcpTransport 与 DefaultMcpClient 初始化这一节是全文的核心直接给可复制的代码。先讲HttpMcpTransport的构建再讲DefaultMcpClient的初始化最后把两者串起来。LangChain4j 1.x 里HttpMcpTransport用的是 Builder 模式。关键参数有三个baseUrl、sseUrl或ssePath、以及请求头里的鉴权。注意 MCP 的 HTTP 传输是SSE 建通道 POST 发请求的双通道模型所以baseUrl和 SSE 路径要分开配。import dev.langchain4j.mcp.client.DefaultMcpClient; import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport; import dev.langchain4j.mcp.client.transport.McpTransport; import java.time.Duration; public class McpTaoTokenConfig { public static McpTransport buildTransport() { String apiKey System.getenv(TAOTOKEN_API_KEY); String baseUrl System.getenv(TAOTOKEN_BASE_URL); return HttpMcpTransport.builder() .baseUrl(baseUrl) // https://taotoken.net/api .ssePath(/mcp/sse) // 按你的 MCP Server 实际路径调整 .messagePath(/mcp/message) // POST 请求路径 .apiKey(apiKey) // 会以 Authorization: Bearer 发出 .timeout(Duration.ofSeconds(30)) .logRequests(true) .logResponses(true) .build(); } }这里有几个点必须说清楚。第一baseUrl填的是https://taotoken.net/apissePath和messagePath是相对于 baseUrl 的路径最终拼出来是https://taotoken.net/api/mcp/sse。如果你的 MCP Server 部署在别的路径下改这两个 path 就行baseUrl 不动。第二apiKey方法在 LangChain4j 1.x 的HttpMcpTransport.Builder里会把它塞进Authorization: Bearer key请求头。如果你的 MCP Server 用的是别的鉴权头名比如X-Api-Key那就得用.customHeaders(Map.of(X-Api-Key, apiKey))替代.apiKey()。第三logRequests和logResponses在调试阶段一定打开它们会把完整的 JSON-RPC 请求和响应打到日志里。生产环境记得关掉否则 Key 可能进日志。接下来是DefaultMcpClient的初始化import dev.langchain4j.mcp.client.DefaultMcpClient; import dev.langchain4j.mcp.client.McpClient; public class McpClientFactory { public static McpClient createClient() { McpTransport transport McpTaoTokenConfig.buildTransport(); return new DefaultMcpClient.Builder() .transport(transport) .clientName(langchain4j-mcp-taotoken-demo) .clientVersion(1.0.0) .toolExecutionTimeout(Duration.ofSeconds(60)) .build(); } }DefaultMcpClient的 Builder 里transport是必填的clientName和clientVersion会在initialize握手时发给服务端用于服务端侧识别调用方。toolExecutionTimeout是工具执行的超时和 Transport 层的timeout是两回事——前者管「工具跑多久算超时」后者管「HTTP 请求多久算超时」。两个都要设且toolExecutionTimeout应该大于等于 Transport timeout否则工具还没跑完 HTTP 就先断了。如果你用的是 Spring Boot 项目可以把这两个 Bean 注册进去Configuration public class McpConfig { Bean public McpTransport mcpTransport() { return McpTaoTokenConfig.buildTransport(); } Bean public McpClient mcpClient(McpTransport transport) { return new DefaultMcpClient.Builder() .transport(transport) .clientName(spring-boot-mcp-client) .clientVersion(1.0.0) .toolExecutionTimeout(Duration.ofSeconds(60)) .build(); } }注意DefaultMcpClient在构造时会自动调用transport.start()和initialize()所以 Bean 创建完成就意味着握手已经完成。如果握手失败Spring 启动阶段就会抛异常这其实是好事——早失败早发现别等到运行时才报错。关于McpToolProvider它是把多个McpClient的工具聚合起来给 ChatModel 用的。配置片段如下McpToolProvider toolProvider McpToolProvider.builder() .mcpClients(List.of(mcpClient)) .failIfOneServerFails(false) // 单个 Server 挂了不影响其他 .build();failIfOneServerFails(false)这个参数在多 MCP Server 场景下很关键。默认是true意味着任何一个 Server 出问题整个工具列表就构建失败。设成false后挂掉的 Server 的工具会被跳过其余正常返回。4. 验证请求一次完整的 listTools 与 executeTool 调用配置写完不验证等于没写。这一节给一个完整的验证流程从listTools到executeTool每一步都给预期返回。先写验证主类import dev.langchain4j.agent.tool.ToolExecutionRequest; import dev.langchain4j.mcp.client.McpClient; import dev.langchain4j.mcp.client.McpTool; import dev.langchain4j.service.tool.ToolExecutionResult; import java.util.List; public class McpVerify { public static void main(String[] args) throws Exception { McpClient client McpClientFactory.createClient(); // 第一步健康检查 boolean healthy client.checkHealth(); System.out.println(MCP Server healthy: healthy); // 第二步列出工具 ListMcpTool tools client.listTools(); System.out.println(Available tools: tools.size()); tools.forEach(t - System.out.println( - t.name() : t.description())); // 第三步执行一个工具 ToolExecutionRequest request ToolExecutionRequest.builder() .name(tools.get(0).name()) .arguments({\input\: \hello from taotoken\}) .build(); ToolExecutionResult result client.executeTool(request); System.out.println(Tool result: result.resultText()); client.close(); } }预期输出分三段。第一段健康检查正常返回MCP Server healthy: true。如果返回false说明initialize握手没成功去看 Transport 日志里的 JSON-RPC 错误码。第二段列工具输出类似Available tools: 3 - get_weather: Get current weather for a city - search_docs: Search documentation by keyword - run_sql: Execute a read-only SQL query工具数量和名称取决于你的 MCP Server 实现了什么。如果这里返回 0 个工具但健康检查是true那大概率是服务端的tools/list方法没实现或返回了空数组。第三段执行工具输出类似Tool result: {temperature: 22, condition: sunny, city: Beijing}resultText()返回的是工具执行结果的文本形式。如果工具返回的是结构化 JSON这里就是 JSON 字符串如果是纯文本就是文本。如果你想在日志里看到完整的 JSON-RPC 往返把logRequests和logResponses打开后控制台会打印类似-- POST https://taotoken.net/api/mcp/message -- {jsonrpc:2.0,id:2,method:tools/call,params:{name:get_weather,arguments:{input:hello from taotoken}}} -- {jsonrpc:2.0,id:2,result:{content:[{type:text,text:{\temperature\:22,...}}]}}看到这个往返就说明McpClient→McpTransport→ TaoToken 通道 → MCP Server 的整条链路是通的。如果你还想验证模型对话侧可以访问https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite在网页上直接试一下同一个 Model ID确认 Key 和模型名没问题。这一步不是必须的但在排查「到底是 MCP 链路问题还是模型问题」时很有用。5. 常见报错排查401、local proxy failed、reading choices、OAuth这一节按真实报错来组织每个报错给现象、根因、修法。401 Unauthorized。现象是initialize阶段就失败日志里 HTTP 状态码 401。根因九成是 Key 没生效。排查顺序先echo $TAOTOKEN_API_KEY确认环境变量非空再确认代码里读的是同一个变量名最后确认 Key 没有过期或被删除。如果用的是.apiKey()方法检查 Transport 日志里Authorization头是不是Bearer sk-...格式。注意别把 Key 前后的引号或空格带进去。local proxy failed。这个报错通常出现在 stdio 传输切 HTTP 传输的时候。现象是StdioMcpTransport启动子进程失败或者HttpMcpTransport连不上 SSE 端点。根因有两类一是ssePath配错了实际服务端没有这个路径二是网络层到不了taotoken.net。排查方法先用curl -v https://taotoken.net/api/mcp/sse看能不能建立 SSE 连接如果 curl 都连不上那就是网络或路径问题跟 LangChain4j 无关。如果 curl 能连上但 Java 连不上检查是不是走了系统代理——Java 的HttpClient默认不读HTTP_PROXY环境变量需要显式配ProxySelector。reading choices 相关报错。这个报错一般不在 MCP 链路本身而在 MCP Server 内部调用模型时。现象是工具执行返回错误日志里有Error reading choices或choices field is null。根因是模型 API 返回的响应格式不符合预期常见于 Model ID 填错或 Base URL 配错。排查确认 MCP Server 内部调用模型时用的 Base URL 是https://taotoken.net/apiModel ID 是控制台里存在的模型。如果 MCP Server 用的是 OpenAI Java SDK检查baseUrl有没有多拼/v1——TaoToken 的入口已经包含版本路径再拼就重复了。OAuth 相关报错。现象是initialize返回 401 且响应体里有OAuth字样。根因是某些 MCP Server 实现了 OAuth 鉴权流程而HttpMcpTransport的.apiKey()只发 Bearer 头不参与 OAuth 授权码流程。修法有两种一是改用.customHeaders()手动带上 OAuth token二是如果服务端支持 API Key 模式切到 API Key 模式。LangChain4j 1.x 的HttpMcpTransport目前不内置 OAuth 流程需要自己在 Transport 外层包一层。工具执行超时但 HTTP 没超时。现象是executeTool抛TimeoutException但 Transport 日志显示请求已发出且没报错。根因是toolExecutionTimeout小于工具实际执行时间。修法把DefaultMcpClient.Builder的toolExecutionTimeout调大同时确认 Transport 的timeout也相应调大。两个超时的关系是Transport timeout 管单次 HTTP 请求toolExecutionTimeout 管整个工具执行可能包含多次 HTTP 往返。工具列表为空但健康检查通过。现象是checkHealth()返回true但listTools()返回空列表。根因通常是 MCP Server 的tools/list方法返回了空数组或者McpToolProvider的过滤条件把所有工具都滤掉了。排查先直接调client.listTools()看原始返回如果这里就是空问题在服务端如果这里非空但McpToolProvider返回空检查BiPredicate过滤条件是不是写反了。把上面这些报错整理成一张速查表报错根因修法401 UnauthorizedKey 未生效检查环境变量与 Authorization 头local proxy failedssePath 错或网络不通curl 验证端点检查代理reading choicesModel ID / Base URL 错确认https://taotoken.net/api与模型名OAuth 报错Transport 不支持 OAuth改 customHeaders 或切 API Key 模式工具执行超时toolExecutionTimeout 太小调大两个超时值工具列表为空服务端 tools/list 为空先调 client.listTools 定位6. 把 MCP 服务端稳定指向 TaoToken 的接入路径走到这里McpClient和McpTransport的链路已经跑通了。最后说一下长期使用的接入路径以及几个容易忽略的细节。第一Base URL 和 Key 的管理。生产环境不要把 Key 写进代码或配置文件用环境变量或密钥管理服务。TaoToken 的 API Key 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite支持创建多个 Key建议按环境dev/staging/prod分开出问题时能快速定位和吊销。第二Transport 的重连机制。HttpMcpTransport在 SSE 通道断开后不会自动重连需要你在外层监听onFailure回调并重建 Transport。DefaultMcpClient有自动重连机制但它依赖 Transport 层能重新start()。如果你发现长时间运行后工具调用开始失败先检查 SSE 通道是不是断了。第三多 MCP Server 的工具冲突。当你有多个 MCP Server 且工具名重复时McpToolProvider的行为取决于具体实现。LangChain4j 1.x 里默认是后加入的覆盖先加入的建议在工具名前加 Server 前缀避免冲突。第四日志脱敏。logRequests和logResponses在调试时很有用但会把 Key 和工具参数打进日志。生产环境务必关掉或者配置日志过滤器把Authorization头脱敏。如果你在接入过程中遇到本文没覆盖的报错可以对照https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里的接口文档核对请求格式。MCP 协议本身在演进LangChain4j 的 MCP 模块也在跟进遇到行为不一致时优先看源码里的DefaultMcpClient和HttpMcpTransport实现比看文档快。最后给一个实用技巧在McpTransport外层包一个简单的重试装饰器对initialize和tools/call这类幂等操作做最多 3 次重试能挡掉大部分网络抖动导致的偶发失败。重试间隔用指数退避别用固定间隔否则容易在服务端恢复瞬间打爆。