纯Java实现STDIO通信的MCP Server与客户端验证:TaoToken统一Key接入实践
1. 为什么要在纯 Java 里手搓一个 STDIO MCP ServerMCPModel Context Protocol这两年成了大模型接工具的事实标准而 STDIO 通信是它最朴素也最稳的一种传输方式客户端把 Server 当成一个子进程拉起来双方通过标准输入stdin和标准输出stdout交换 JSON-RPC 消息。没有端口、没有 HTTP、没有网络配置进程活着通道就活着进程挂了通道就断调试起来一目了然。纯 Java 实现 STDIO MCP Server 这件事适合几类人一是你不想为了一个工具函数就引入 Spring Boot 全家桶想要一个几十行 main 方法就能跑起来的最小进程二是你要把 MCP Server 塞进已有的命令行工具或桌面程序里进程模型越简单越好三是你在做客户端联调需要一个可控的、能随时打断点看 stdin/stdout 的服务端。它本质上就是一个「读一行 JSON、算一下、写一行 JSON」的循环难点不在算法而在协议握手、能力声明和传输层别被日志污染。我这次的目标很明确用纯 Java不依赖 Spring Boot写一个只暴露一个加法 Tool 的 MCP Server打包成带依赖的 fat jar再用官方 Java SDK 的客户端把它拉起来完成一次 initialize → listTools → callTool 的完整往返。同时把鉴权这一层交给 TaoToken 的统一 Key 通道这样客户端侧不用为每个模型供应商维护一套密钥Base URL、Key、Model ID 三件套配一次就能复用。下面从依赖、代码、打包、联调到排错一步步走完。2. TaoToken 统一 Key 前置准备Base URL、Key 与 Model ID在动手写代码之前先把「鉴权通道」这件事理清楚。MCP Server 本身是本地进程它不直接跟模型说话真正需要 Key 的是调用模型的客户端或上层 Agent。TaoToken 在这里扮演的是统一入口你拿到一个 Key配一个 Base URL就能在多个模型之间切换不用为每家单独申请和轮换密钥。你需要准备三样东西我把它叫做「三件套」项目值说明Base URLhttps://taotoken.net/apiOpenAI 兼容风格的基础地址注意不要带多余路径API Key在控制台生成形如sk-...只显示一次务必存好Model ID例如claude-sonnet-4-5等按你实际要调用的模型填大小写敏感获取路径很直接打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册登录后进入控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 创建一个新 Key。创建时建议按用途命名比如mcp-local-dev方便以后按项目吊销。注意Key 只在创建时完整展示一次页面刷新后就只剩前缀。如果你打算在多个本地项目里复用建议写进环境变量而不是硬编码进代码避免提交到 Git。环境变量可以这样设Linux/macOSexport TAOTOKEN_API_KEYsk-你的Key export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_MODELclaude-sonnet-4-5Windows PowerShell$env:TAOTOKEN_API_KEYsk-你的Key $env:TAOTOKEN_BASE_URLhttps://taotoken.net/api $env:TAOTOKEN_MODELclaude-sonnet-4-5这里要强调一个容易混淆的点本篇的 MCP Server 是纯本地 STDIO 进程它自己不读这个 KeyKey 是给「调用模型的客户端」用的。也就是说链路是「你的 Agent/客户端 → 通过 TaoToken 调模型 → 模型决定调用哪个 Tool → 客户端通过 STDIO 把 Tool 调用转发给本地 MCP Server」。把这条链路想清楚后面联调时就不会纠结「Server 为什么不需要 Key」。如果你用的是 Claude Code 这类工具它的配置里同样填这三件套Base URL 指向https://taotoken.net/apiKey 填刚生成的Model ID 按需选。想先验证 Key 是否可用可以直接去模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 发一句话能正常返回就说明通道没问题再往下写代码。3. 可复制的纯 Java MCP Server 配置与代码这一节是全文的技术核心。项目结构很简单一个pom.xml加两个类MainServer和McpClientDemo客户端验证。先看pom.xml的关键部分。依赖用官方 SDK 的 BOM 统一管理版本当前用 0.9.0dependencyManagement dependencies dependency groupIdio.modelcontextprotocol.sdk/groupId artifactIdmcp-bom/artifactId version0.9.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdio.modelcontextprotocol.sdk/groupId artifactIdmcp/artifactId /dependency /dependencies打包要打成带依赖的 fat jar否则运行时得手动拼 ClassPath。加maven-assembly-plugin并指定主类plugin artifactIdmaven-assembly-plugin/artifactId version3.3.0/version configuration descriptorRefs descriptorRefjar-with-dependencies/descriptorRef /descriptorRefs archive manifest mainClasscom.osxm.ai.mcp.purejava.Main/mainClass /manifest /archive /configuration executions execution phasepackage/phase goals goalsingle/goal /goals /execution /executions /pluginServer 端代码的核心是三步建传输提供程序、建 Tool 规范、建同步 Server。传输用StdioServerTransportProvider它负责把 stdin/stdout 包装成 JSON-RPC 通道StdioServerTransportProvider transportProvider new StdioServerTransportProvider(new ObjectMapper()); var schema { type: object, properties: { operation: { type: string }, a: { type: number }, b: { type: number } } } ; SyncToolSpecification syncToolSpecification new McpServerFeatures.SyncToolSpecification( new Tool(calculator, Basic Calculator, schema), (exchange, arguments) - { String operation (String) arguments.get(operation); int a (Integer) arguments.get(a); int b (Integer) arguments.get(b); double result add.equals(operation) ? a b : a - b; return new McpSchema.CallToolResult(String.valueOf(result), false); }); var capabilities ServerCapabilities.builder() .tools(true) .build(); var mcpServer McpServer.sync(transportProvider) .capabilities(capabilities) .tools(syncToolSpecification) .build();这里tools(true)表示声明工具能力并开启工具列表变更通知。CallToolResult第二个参数false表示不是错误结果。工具名calculator是客户端调用时的唯一标识别写错。一个必须记住的坑不要在pom.xml里加 logback 之类的日志实现依赖。因为 STDIO 通道本身就占用标准输入输出日志框架默认也往 stdout 写两者一混JSON-RPC 消息就被日志文本污染客户端解析直接失败。开发阶段用System.err打调试信息是安全的因为 stderr 不参与协议通道。打包命令mvn clean package产物里会有一个pure-java-mcp-jar-with-dependencies.jar这就是客户端要拉起的那个 jar。4. 客户端验证一次完整的 initialize 到 callTool 往返Server 有了接下来写客户端把它拉起来验证。客户端用StdioClientTransport通过ServerParameters指定启动命令和参数本质就是「用 java -jar 把 Server 当子进程跑起来」var stdioParams ServerParameters.builder(java) .args(-jar, target/pure-java-mcp-jar-with-dependencies.jar) .build(); StdioClientTransport transport new StdioClientTransport(stdioParams); var client McpClient.sync(transport).build(); client.initialize(); ListToolsResult toolsList client.listTools(); System.out.println(Available Tools toolsList); CallToolResult result client.callTool( new CallToolRequest(calculator, Map.of(operation, add, a, 2, b, 3))); System.out.println(result result); client.closeGracefully();运行后你会看到两段关键输出listTools返回了calculator工具及其 schemacallTool返回了5.0。这说明整条 STDIO 链路是通的——客户端写 stdinServer 读并处理结果写 stdout客户端读回。如果你想把模型也接进来让模型自己决定调用这个 Tool那么客户端侧就要配上 TaoToken 的三件套。以 OpenAI 兼容调用为例配置片段如下{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-5 }把这段配置放进你的 Agent 或客户端设置里模型在需要计算时会发出 tool_call客户端再通过上面的 STDIO 通道转发给本地 Server。这样「模型鉴权走 TaoToken、工具执行走本地进程」两条线就分开了各管各的互不干扰。验证成功的判断标准很简单listTools有输出、callTool返回正确数值、进程能优雅关闭。三个都满足链路就算跑通了。想进一步验证模型侧可以去模型对话 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 手动发一条带工具调用的请求观察返回里是否出现对calculator的调用意图。5. 常见报错排查401、local proxy failed 与 reading choices联调阶段最容易卡在几个典型报错上我按「现象 → 原因 → 处理」列一遍。401 Unauthorized出现在模型调用侧不是 MCP Server 侧。原因通常是 Key 没设对、Key 被吊销、或者 Base URL 写成了带多余路径的形式。检查顺序先确认环境变量TAOTOKEN_API_KEY真的被进程读到了打印前几位即可别打印全量再确认 Base URL 是https://taotoken.net/api而不是别的。如果用的是 Claude Code 或 Cline 这类工具去它的设置里核对三件套是否齐全缺 Model ID 也会报鉴权类错误。local proxy failed / connection refused这类报错多半是客户端想连一个本地代理端口但没连上。如果你没配任何本地代理检查是不是工具里残留了旧的代理配置项把它清空。MCP 的 STDIO 通道本身不走网络出现 proxy 相关字样通常是上层模型调用的网络配置问题跟 Server 无关。reading choices 相关解析错误这是模型返回体解析失败常见于 Base URL 指向了不兼容的端点或者返回的不是标准 OpenAI 格式。确认你用的是 TaoToken 的兼容端点并且请求体里model字段拼写正确。Model ID 大小写敏感写错会返回非预期结构。STDIO 通道被日志污染现象是客户端报 JSON 解析失败或者 initialize 直接超时。原因就是前面说的日志依赖。处理办法移除 logback/log4j 的绑定依赖或者把日志输出重定向到 stderr。检查pom.xml里有没有意外引入的日志实现。jar 找不到主类运行java -jar报no main manifest attribute。说明打包时没配maven-assembly-plugin的mainClass或者你运行的是那个不带依赖的瘦 jar。确认用的是-jar-with-dependencies.jar结尾的文件。客户端拉起 Server 后立刻退出检查 Server 的 main 方法是否在 build 之后没有阻塞。McpServer.sync(...).build()之后进程需要保持存活等待 stdin如果代码里 build 完就 return进程会退出客户端自然读不到响应。排查时有个通用手法单独用命令行手动喂一条 JSON-RPC 给 Server看它 stdout 吐什么。比如echo {jsonrpc:2.0,id:1,method:initialize,params:{}} | java -jar target/pure-java-mcp-jar-with-dependencies.jar如果 stdout 返回了合法的 initialize 响应说明 Server 本身没问题问题在客户端配置如果 stdout 混着日志文本那就是日志污染。这一招能快速把问题范围缩小一半。6. 把链路固化下来从本地验证到长期编码接入跑通一次不难难的是把它变成日常能用的东西。我的做法是把「本地 MCP Server TaoToken 统一 Key」当成一套固定组合Server 负责具体工具能力Key 通道负责模型鉴权两者通过客户端的 STDIO 转发连接。这样每次新增一个工具只需要在 Server 里加一个SyncToolSpecification客户端和 Key 配置都不用动。如果你打算长期用这套组合做编码或 Agent 任务建议把 Key 和 Base URL 统一走 TaoToken 的 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 这样额度、模型切换、密钥管理都在一个地方省得每个项目单独维护。接入细节和参数说明可以对照接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 逐项核对尤其是 Base URL 和 Model ID 这两项写错一个字符就是 401 或解析失败。最后留一个实用习惯把 Server 的启动命令、jar 路径、客户端配置写进一个README或脚本里下次换机器时直接复制。STDIO 这套东西最大的优势就是可移植——只要 JVM 在、jar 在、Key 在链路就能原地复活。