ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MCP协议概述与Client源码解析:用TaoToken统一Key跑通第一个MCP Client

MCP协议概述与Client源码解析:用TaoToken统一Key跑通第一个MCP Client 1. 从一次“工具调不通”说起MCP 到底解决什么问题如果你最近在折腾 AI Agent大概率遇到过这种场景模型明明支持 Function Calling但你想让它查一下本地数据库、读一个文件、调一个内部 HTTP 接口就得在每个项目里重复写一遍工具定义、参数校验、结果解析。更麻烦的是这些工具散落在不同语言、不同进程里Python 写的工具 Java 项目用不了Go 写的服务前端又接不上。MCPModel Context Protocol就是冲着这个痛点来的。你可以把它理解成 AI 世界的 USB-C 接口以前每个外设都有自己的插头现在统一成一个标准口谁都能插。MCP 基于 JSON-RPC 2.0把外部能力抽象成三类东西——Tool可执行功能、Resource只读数据源、Prompt预定义模板Client 通过tools/list、tools/call、resources/read这些标准方法跟 Server 通信。这篇面向想从零理解 MCP 通信流程的开发者重点不是背概念而是把 Client 源码结构拆开看再用 TaoToken 的统一 Key 在本地跑通第一个 MCP Client。跑通之后你对“请求怎么发出去、工具怎么被发现、结果怎么回来”会有一个具象的认知而不是停留在架构图上。适合谁看写过一点 Spring AI 或 LangChain、想搞清楚 MCP Client 内部怎么工作、又不想一上来就被多语言环境卡住的人。下面所有配置我都实测过命令可以直接复制。2. 前置准备用 TaoToken 统一 Key 管住多模型调用MCP Client 本身不绑定某个模型但你要验证一次完整的请求-响应总得有个能调用的 LLM。这里我用 TaoToken 做统一入口原因是它把多家模型的 Key 收敛成一个config.toml 里只维护一份凭证切换模型不用改代码。官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。你需要先拿到一个 API Key。登录后进控制台在 API Keys 页面创建一个复制出来形如sk-xxxx的字符串。这个 Key 后面会写进 config.tomlMCP Client 调用模型时带上它。注意Key 只存在本地配置文件里不要提交到 Git。建议用环境变量注入或者把 config.toml 加进 .gitignore。TaoToken 的接入文档在 https://taotoken.net/doc 里面有各语言 SDK 的 base_url 写法。MCP Client 这边我们直接用 OpenAI 兼容格式base_url 填https://taotoken.net/api模型名按文档里支持的填比如gpt-4o-mini这类通用对话模型就够验证流程了。如果你后面要长期跑编码类 Agent可以看 Coding Plan https://taotoken.net/coding-plan 只是验证模型连通性用模型对话页 https://taotoken.net/chat 手动发一条也行。但本篇重点在 Client 源码和本地跑通所以走 API Key 这条路。3. 可复制配置config.toml 骨架与 MCP Client 最小配置先建目录结构我习惯这样放mcp-demo/ ├── config.toml ├── mcp_client.py └── servers/ └── echo_server.pyconfig.toml 是整个项目的凭证和端点中心骨架如下[llm] provider openai-compatible base_url https://taotoken.net/api api_key sk-你的TaoToken密钥 model gpt-4o-mini timeout 30 [mcp] client_name taotoken-mcp-client client_version 0.1.0 transport stdio [mcp.servers.echo] command python args [servers/echo_server.py]这里[mcp.servers.echo]定义了一个本地 STDIO 传输的 MCP ServerClient 启动时会以子进程方式拉起echo_server.py通过标准输入输出做 JSON-RPC 通信。这是最小可运行的传输方式不需要开端口适合本地验证。echo_server.py 写一个最简单的工具返回传入的文本import sys import json def handle(request): method request.get(method) req_id request.get(id) if method initialize: return {jsonrpc: 2.0, id: req_id, result: { protocolVersion: 2024-11-05, capabilities: {tools: {}}, serverInfo: {name: echo-server, version: 0.1.0} }} if method tools/list: return {jsonrpc: 2.0, id: req_id, result: {tools: [{ name: echo, description: 原样返回输入文本, inputSchema: {type: object, properties: { text: {type: string} }, required: [text]} }]}} if method tools/call: args request.get(params, {}).get(arguments, {}) text args.get(text, ) return {jsonrpc: 2.0, id: req_id, result: { content: [{type: text, text: fecho: {text}}], isError: False }} return {jsonrpc: 2.0, id: req_id, error: { code: -32601, message: Method not found}} for line in sys.stdin: line line.strip() if not line: continue try: req json.loads(line) resp handle(req) sys.stdout.write(json.dumps(resp) \n) sys.stdout.flush() except Exception as e: sys.stderr.write(str(e) \n)这个 Server 只实现了三个方法initialize握手、tools/list暴露工具、tools/call执行工具。真实项目里 Server 会复杂得多但通信骨架就是这样。Client 端我用 Python 写方便你直接跑。核心逻辑是读 config.toml拉起 Server 子进程发 initialize再发 tools/list最后发 tools/call。import json import subprocess import tomllib with open(config.toml, rb) as f: cfg tomllib.load(f) server_cfg cfg[mcp][servers][echo] proc subprocess.Popen( [server_cfg[command]] server_cfg[args], stdinsubprocess.PIPE, stdoutsubprocess.PIPE, stderrsubprocess.PIPE, textTrue ) def rpc(method, paramsNone, req_id1): msg {jsonrpc: 2.0, id: req_id, method: method} if params is not None: msg[params] params proc.stdin.write(json.dumps(msg) \n) proc.stdin.flush() line proc.stdout.readline() return json.loads(line) init rpc(initialize, { protocolVersion: 2024-11-05, capabilities: {}, clientInfo: {name: cfg[mcp][client_name], version: cfg[mcp][client_version]} }) print(initialize -, init[result][serverInfo]) tools rpc(tools/list, {}, req_id2) print(tools/list -, [t[name] for t in tools[result][tools]]) call rpc(tools/call, { name: echo, arguments: {text: hello mcp} }, req_id3) print(tools/call -, call[result][content][0][text])这段代码没有依赖任何 MCP SDK纯手写 JSON-RPC目的是让你看清每一次请求长什么样。等你理解了流程再换成官方 SDK 会顺很多。4. 验证请求一次完整的请求-响应链路先装依赖Python 3.11 自带 tomllib不用额外装。然后跑cd mcp-demo python mcp_client.py预期输出initialize - {name: echo-server, version: 0.1.0} tools/list - [echo] tools/call - echo: hello mcp三行输出对应三次 JSON-RPC 往返。第一次initialize是握手Client 告诉 Server 自己支持的协议版本和能力Server 回自己的信息。第二次tools/list是工具发现Client 拿到 Server 暴露的所有工具元数据。第三次tools/call是实际执行Client 把工具名和参数发过去Server 返回结果。如果你想把模型也接进来让 LLM 决定调哪个工具可以在拿到 tools/list 之后把工具定义转成 OpenAI 的 function 格式发给 TaoToken 的/v1/chat/completions。模型返回 tool_calls 后你再解析出工具名和参数走上面的tools/call。这一步就是把 MCP 和 LLM 串起来的关键也是 Client 源码里SyncMcpToolCallback这类适配器做的事——把 MCP 的 Tool 定义翻译成模型能理解的格式再把模型的调用意图翻译回 MCP 请求。验证模型连通性可以单独发一条curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的TaoToken密钥 \ -H Content-Type: application/json \ -d {model:gpt-4o-mini,messages:[{role:user,content:ping}]}返回里有choices[0].message.content就说明 Key 和端点都通。这一步和 MCP 无关但能帮你排除“到底是模型调不通还是 MCP 写错了”的干扰。5. 本篇常见错排查报错一FileNotFoundError: servers/echo_server.py原因是从错误的目录启动。config.toml里的args是相对路径必须保证工作目录是mcp-demo/。解决cd mcp-demo再跑或者把 args 改成绝对路径。报错二Client 卡在readline()不动多半是 Server 没有 flush stdout。Python 的sys.stdout.write默认带缓冲必须跟一句sys.stdout.flush()。上面 echo_server.py 里已经加了如果你自己改代码漏了这行就会死等。报错三json.decoder.JSONDecodeErrorServer 往 stdout 打了非 JSON 内容比如 print 调试信息。MCP 的 STDIO 传输要求 stdout 只能走协议消息调试信息一律走 stderr。检查你的 Server 有没有多余的 print。报错四TaoToken 返回 401Key 写错或没带Bearer前缀。检查 config.toml 里的 api_key以及 curl 里的 Authorization 头格式。另外确认 base_url 是https://taotoken.net/api不要多加/v1后缀具体路径以接入文档为准。报错五tools/call返回Method not foundServer 没实现tools/call分支或者方法名拼错。MCP 的方法名是固定的tools/list和tools/call都是复数别写成tool/list。报错六工具名冲突如果你接了多个 Server两个 Server 都有echo工具Client 侧会冲突。真实 MCP Client 会用前缀区分比如echo_server_echo。手写版可以先在 config 里给每个 Server 加个前缀字段拼工具名时带上。6. 下一步把 Client 接进真实项目跑通这个最小闭环之后你可以做三件事。第一把 echo_server.py 换成真实工具比如查数据库、读文件、调内部 APIServer 用任何语言写都行只要遵守 JSON-RPC 和 MCP 方法约定。第二把 Client 里的手写 RPC 换成官方 SDKSpring AI 那边对应的是McpSyncClient和SyncMcpToolCallbackProviderPython 侧有mcp包能省掉大量样板代码。第三把工具列表喂给模型让 LLM 自主决定调用哪个工具这才是 Agent 的完整形态。如果你要长期跑编码类 Agent建议把 Key 和模型配置统一到 TaoToken 的 Coding Plan https://taotoken.net/coding-plan 省得每个项目维护一份凭证。接入细节和 SDK 示例在接入文档 https://taotoken.net/doc 里都有API Key 在控制台 https://taotoken.net/api-keys 随时可以新建和轮换。模型对话页 https://taotoken.net/chat 适合快速验证某个模型能不能用不用写代码。MCP 的价值不在协议本身多复杂而在于它把“工具怎么被发现、怎么被调用”这件事标准化了。你手写完这一遍再看任何 MCP Client 的源码结构都会很清晰握手、发现、调用三步而已。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进