尧图精选

MCP协议实现实例:从MCP Server到MCP Client,用TaoToken统一Key打通调用链

🕒 发布时间:2026/10/1 7:07:02 📁 来源:尧图网络
1. 为什么我要自己写一个 MCP Server 和 MCP ClientMCP 协议实现实例这件事光看文档很容易产生一种“我懂了”的错觉。真正动手才会发现MCP Server 和 MCP Client 之间的握手、能力协商、工具调用、进度通知每一步都有细节。我最初只是想验证一个天气查询工具能不能被模型调用结果卡在 initialize 的 capabilities 字段上整整一个下午。MCPModel Context Protocol本质上是一套基于 JSON-RPC 2.0 的通信规范。MCP Server 负责暴露工具tools、资源resources、提示词promptsMCP Client 负责发起握手、调用工具、接收通知。两者通过标准输入输出STDIO交换 JSON 消息每条消息一行以换行符分隔。这个设计的好处是进程隔离干净Server 崩溃不会拖垮 Client坏处是调试时看不到“网络请求”只能靠日志。适合谁看这篇如果你已经听说过 MCP但还没亲手跑通过一条完整的调用链或者你已经写了 Server但 Client 那边总是超时、报错、拿不到结果再或者你想把模型侧的鉴权统一到一个 Key 上不想在每个工具里散落不同的 API 凭证——那这篇就是为你准备的。我会用一个天气查询的 MCP Server 作为例子暴露两个工具get_weather_forecast和get_weather_alerts。然后写一个 MCP Client 完成 initialize 握手、tools/list 列举、tools/call 调用最后把模型侧的请求统一走 TaoToken 的 API 通道。整条链路跑通后你能在日志里清楚看到请求确实经由统一通道发出。先说一下最终效果Client 启动 Server 子进程发送 initialize 请求Server 返回协议版本和 capabilitiesClient 发送 tools/listServer 返回两个工具的定义Client 调用 get_weather_forecastServer 分步发送 progress 通知最后返回格式化后的天气预报文本。整个过程不需要任何额外的网络配置STDIO 就是通道。2. 前置准备TaoToken 统一 Key 与项目骨架在写代码之前先把模型侧的鉴权通道确定下来。我选择用 TaoToken 作为统一入口原因是它兼容 OpenAI 风格的接口Base URL 和 Key 的配置方式很直接不需要在每个工具里单独维护凭证。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。你需要先拿到一个 API Key。登录后进入控制台在 API Keys 页面创建一个新的 Key。这个 Key 后面会用在 Client 侧调用模型时作为鉴权凭证。注意MCP Server 本身不直接调用模型它只负责执行工具逻辑模型调用发生在 Client 侧或者更上层的 Agent 框架里。所以统一 Key 的配置点在 Client 的模型请求部分而不是 Server 的工具实现里。项目结构我建议这样组织weather-mcp/ ├── pom.xml ├── src/main/java/com/weather/mcp/ │ ├── WeatherMcpServer.java │ └── McpClientTest.java └── src/main/resources/ └── mcp-config.jsonpom.xml 里需要引入 Jackson 做 JSON 序列化以及 maven-assembly-plugin 打一个包含依赖的 fat jar。关键依赖如下dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.16.1/version /dependency dependency groupIdcom.fasterxml.jackson.datatype/groupId artifactIdjackson-datatype-jsr310/artifactId version2.16.1/version /dependency打包插件配置 mainClass 为com.weather.mcp.WeatherMcpServerdescriptorRef 用jar-with-dependencies。这样打出来的 jar 可以直接用java -jar启动Client 侧用 ProcessBuilder 拉起子进程时不需要额外指定 classpath。关于 Model ID 的选择如果你在 Client 侧要调用模型来做工具选择可以用 TaoToken 支持的任意模型。配置时三个要素缺一不可Base URL 填https://taotoken.net/apiAPI Key 填你创建的那个Model ID 填具体模型名称。这三个值在后面的 Client 配置片段里会完整出现。还有一个容易忽略的点MCP Server 的日志输出。因为 STDIO 通道被 JSON-RPC 消息占用了Server 的调试日志必须走 stderr不能走 stdout。我在代码里把 log 方法设计成同时发送notifications/logging/message通知和写 stderr这样既符合 MCP 规范又方便本地排查。3. 可复制配置Server 与 Client 的完整片段这一节给出可以直接复制运行的配置和代码骨架。先看 Server 侧的核心处理逻辑。Server 的主循环从 stdin 逐行读取 JSON解析 method 字段后分发到对应的 handler。initialize 请求的响应里必须包含 protocolVersion、capabilities 和 serverInfo 三部分private static void handleInitialize(MapString, Object message) { Object id message.get(id); MapString, Object result new HashMap(); result.put(protocolVersion, 2024-11-05); MapString, Object serverInfo new HashMap(); serverInfo.put(name, weather-mcp-server); serverInfo.put(version, 2.0.0); result.put(serverInfo, serverInfo); MapString, Object capabilities new HashMap(); capabilities.put(tools, new HashMap()); MapString, Object resources new HashMap(); resources.put(subscribe, true); capabilities.put(resources, resources); capabilities.put(prompts, new HashMap()); capabilities.put(logging, new HashMap()); result.put(capabilities, capabilities); result.put(instructions, Weather MCP Server - Provides weather forecasts and alerts.); sendResponse(id, result); }tools/list 返回工具定义数组每个工具包含 name、description 和 inputSchema。inputSchema 用 JSON Schema 描述参数类型和必填项。get_weather_forecast 需要 latitude 和 longitude 两个 number 类型参数private static void handleToolsList(MapString, Object message) { Object id message.get(id); ListMapString, Object tools new ArrayList(); MapString, Object forecastTool new HashMap(); forecastTool.put(name, get_weather_forecast); forecastTool.put(description, Get weather forecast for a location); MapString, Object schema new HashMap(); schema.put(type, object); MapString, Object props new HashMap(); MapString, Object latProp new HashMap(); latProp.put(type, number); latProp.put(description, Latitude); props.put(latitude, latProp); MapString, Object lonProp new HashMap(); lonProp.put(type, number); lonProp.put(description, Longitude); props.put(longitude, lonProp); schema.put(properties, props); schema.put(required, Arrays.asList(latitude, longitude)); forecastTool.put(inputSchema, schema); tools.add(forecastTool); MapString, Object result new HashMap(); result.put(tools, tools); sendResponse(id, result); }Client 侧的配置片段需要包含 Base URL、Key 和 Model ID 三件套。如果你用 Cline 或 Claude Code 这类工具接入配置文件通常长这样{ mcpServers: { weather: { command: java, args: [-jar, target/weather-mcp-server-2.0.0-jar-with-dependencies.jar], env: { TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_API_KEY: sk-your-key-here, TAOTOKEN_MODEL_ID: your-model-id } } } }注意 env 里的三个变量Base URL 指向 TaoToken 的 API 端点API Key 是你创建的那个Model ID 填具体模型名称。这三个值在 Client 发起模型请求时会被读取确保所有请求都经由统一通道发出。Client 的启动逻辑用 ProcessBuilder 拉起 Server 子进程然后开两个线程分别读 stdout 和 stderr。stdout 线程负责解析 JSON-RPC 响应和通知stderr 线程负责打印 Server 的调试日志ProcessBuilder pb new ProcessBuilder(java, -jar, jarPath); pb.redirectErrorStream(false); serverProcess pb.start(); writer new BufferedWriter(new OutputStreamWriter(serverProcess.getOutputStream())); reader new BufferedReader(new InputStreamReader(serverProcess.getInputStream())); errorReader new BufferedReader(new InputStreamReader(serverProcess.getErrorStream()));发送请求时每条 JSON 消息后面必须跟一个换行符并 flush否则 Server 的 readLine 会一直阻塞。这是 STDIO 通信最容易踩的坑之一。4. 端到端验证从 initialize 到 tools/call 的完整请求配置写好后跑一次完整的调用链来验证。Client 的测试流程按顺序执行initialize → notifications/initialized → tools/list → tools/call。每一步都有明确的请求和响应可以在控制台看到完整的 JSON 交换过程。先启动 Server 并发送 initialize 请求MapString, Object params new HashMap(); params.put(protocolVersion, 2024-11-05); params.put(capabilities, new HashMap()); MapString, Object clientInfo new HashMap(); clientInfo.put(name, test-client); clientInfo.put(version, 1.0.0); params.put(clientInfo, clientInfo); JsonNode response sendRequest(initialize, params);Server 返回的 result 里包含 protocolVersion、capabilities 和 serverInfo。确认 protocolVersion 是2024-11-05serverInfo.name 是weather-mcp-server。然后发送 initialized 通知这一步没有 idServer 不会返回响应sendNotification(notifications/initialized, null);接下来调用 tools/list拿到工具列表后选择 get_weather_forecast 进行调用。调用参数用堪萨斯州的坐标latitude 39.7456longitude -97.0892。MapString, Object callParams new HashMap(); callParams.put(name, get_weather_forecast); MapString, Object arguments new HashMap(); arguments.put(latitude, 39.7456); arguments.put(longitude, -97.0892); callParams.put(arguments, arguments); JsonNode callResponse sendRequest(tools/call, callParams);Server 处理这个调用时会分步发送 progress 通知0% 开始请求30% 获取网格点数据60% 获取预报数据90% 格式化100% 完成。这些通知的 method 是notifications/progressparams 里带 progressToken、progress、total 和 message。Client 侧的 notifications 列表会收集到这些通知打印出来就能看到进度变化。最终 tools/call 的响应里result.content 是一个数组第一个元素的 type 是 texttext 字段是格式化后的天气预报文本包含温度、风速、风向和详细预报。如果一切正常控制台会输出类似这样的内容Weather Forecast for 39.7456, -97.0892 **Today** Temperature: 72°F Wind: 10 mph SW Forecast: Sunny, with a high near 72. **Tonight** Temperature: 55°F Wind: 5 mph S Forecast: Clear, with a low around 55.验证请求确实经由统一通道发出的方法在 Client 侧调用模型时检查请求的 Base URL 是否为https://taotoken.net/api。你可以在 Client 的模型请求代码里加一行日志打印出实际使用的 endpoint 和 model ID。如果日志显示的是 TaoToken 的地址和你配置的 Model ID说明统一通道生效了。还有一个验证技巧在 TaoToken 控制台的 API Keys 页面查看调用记录。每次模型请求都会留下日志包括时间戳、模型名称和请求状态。如果 tools/call 之后紧接着有一条模型调用记录说明整条链路是通的。5. 常见报错排查401、local proxy failed 与 reading choices跑 MCP 调用链时遇到的报错大多集中在几个固定位置。我整理了几个真实踩过的坑对照着排查能省不少时间。401 Unauthorized这个报错通常出现在 Client 侧调用模型时。原因一般是 API Key 没有正确传入或者 Key 已经失效。检查三个地方env 里的TAOTOKEN_API_KEY是否填了完整的 Key代码里读取环境变量的逻辑是否正确请求头里的 Authorization 字段格式是否为Bearer sk-xxx。如果用的是 Cline 或 Claude Code检查配置文件里的 env 块是否被正确解析。local proxy failed这个报错说明 Client 尝试连接本地代理但失败了。MCP 的 STDIO 通信不需要任何网络代理如果你在环境变量里设置了HTTP_PROXY或HTTPS_PROXYJava 进程可能会尝试走代理导致连接失败。解决办法是在启动 Server 子进程时清空代理相关环境变量或者在 ProcessBuilder 里显式设置pb.environment().remove(HTTP_PROXY)。reading choices 报错这个通常出现在解析模型响应时。如果模型返回的 JSON 结构不符合预期Client 在读取choices[0].message.content时会抛异常。检查 Model ID 是否填写正确有些模型返回的字段名可能不同。另外确认 Base URL 末尾没有多余的斜杠https://taotoken.net/api和https://taotoken.net/api/在某些客户端里行为不一致。OAuth 相关报错如果你在 Client 配置里启用了 OAuth 认证但 TaoToken 的 Key 是 API Key 模式两者会冲突。解决办法是关闭 OAuth 选项只用 API Key 鉴权。在 Claude Code 的 settings 里把认证方式改为 API Key填入TAOTOKEN_API_KEY的值。Server 启动后无响应检查 Server 的 stdout 是否被其他日志污染。MCP 要求 stdout 只输出 JSON-RPC 消息任何额外的 println 都会导致 Client 解析失败。把调试日志全部改到 stderr或者用notifications/logging/message发送。tools/call 超时默认超时时间设的是 30 秒。如果天气 API 响应慢可以适当调大。另外检查 Server 的 scheduler 线程池是否被占满Executors.newScheduledThreadPool(10)在并发调用多的时候可能不够用。排查时建议打开 Server 的 stderr 日志里面会打印每个请求的处理状态和错误堆栈。Client 侧则打印完整的请求和响应 JSON对照着看能快速定位问题出在哪一环。6. 把统一 Key 接入你的编码工作流整条链路跑通之后最有价值的收获不是天气查询本身而是这套模式可以复用到任何 MCP 工具上。你只需要替换 Server 里的工具实现Client 侧的握手、调用、通知处理逻辑完全不用改。统一 Key 的配置也只需要维护一份所有工具共享同一个 API 通道。如果你日常用 Claude Code 做开发可以把 MCP Server 注册到 Claude Code 的配置里。在~/.claude/settings.json或者项目级的.mcp.json里加上 weather server 的定义command 指向 javaargs 指向打好的 jar 包env 里填 TaoToken 的三件套。这样在 Claude Code 里就能直接调用天气工具模型侧的请求自动走统一通道。对于长期编码和 Agent 场景建议把 Key 管理集中到环境变量或者密钥管理服务里不要硬编码在代码或配置文件里。TaoToken 的控制台支持创建多个 Key你可以按项目或按环境分开方便追踪调用来源。接入文档在 https://taotoken.net/doc 可以查到更详细的参数说明。API Keys 管理页面在 https://taotoken.net/api-keys 创建和吊销 Key 都在这里操作。如果只是想先验证模型对话是否正常可以用 https://taotoken.net/models 这个入口快速测试。最后说一个实用技巧在 Client 的 sendRequest 方法里加一个请求 ID 的自增计数器每次请求都把 id 打印出来。这样在日志里能清楚看到哪个请求对应哪个响应排查超时问题时特别有用。MCP 的 JSON-RPC 规范要求 id 必须唯一用 AtomicInteger 或者简单的 int 自增都能满足。跑通这条链路之后你可以尝试扩展 Server 的能力加一个 resources/read 返回本地文件内容或者加一个 prompts/get 返回预设的提示词模板。Client 侧只需要增加对应的测试方法核心的通信框架不用动。这种可扩展性正是 MCP 协议设计的初衷——让工具集成变得标准化让开发者专注于业务逻辑而不是通信细节。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →