1. 为什么你的 AI Agent 还是“睁眼瞎”:MCP 协议到底补上了哪一环
很多人第一次接触 MCP(Model Context Protocol)时,会把它当成又一个“工具调用框架”。但真正上手跑通一次链路后你会发现,它解决的不是“怎么调工具”,而是“工具怎么被任何客户端即插即用地发现和调用”。这两个问题看着像,实际差了一整个生态位。
在 MCP 出现之前,我试过用 Function Calling 硬编码工具。模型输出一段 JSON,我在 Python 里解析、分发、执行、再把结果塞回对话。单机跑没问题,但只要换一个模型、换一个客户端,整套 schema 就得重写。LangChain Tools 好一点,至少抽象了一层,可它本质还是框架私有协议,你的工具绑死在 LangChain 生态里,迁移成本高得离谱。
MCP 的思路完全不同。它定义了一套基于 JSON-RPC 2.0 的开放协议,Server 负责暴露 Tools、Resources、Prompts,Client 负责发现并调用。通信层可以是 stdio,也可以是 SSE。只要你的工具实现了这套协议,Claude Desktop、Cursor、Cline、Continue.dev 这些支持 MCP 的客户端都能直接连上来用。这就是“最后一公里”的含义:Agent 的大脑已经够聪明了,缺的是标准化的手脚。
但这里有个容易被忽略的工程问题。MCP Server 本身不负责模型推理,它只负责工具能力。真正驱动 Agent 去“决定调用哪个工具”的,还是背后的大模型 API。也就是说,你的链路其实是两段:第一段是 Client 把工具列表和用户意图发给模型,模型返回 tool_call;第二段是 Client 通过 JSON-RPC 把 tool_call 转发给 MCP Server 执行。这两段里,第一段对 API 通道的稳定性、Key 管理、模型兼容性要求很高。如果你同时接多个客户端、多个模型,Key 散落在各处,排查问题会非常痛苦。
这就是我把 TaoToken 拉进来的原因。它不是 MCP 协议的一部分,但它解决的是 MCP 链路里“模型侧统一接入”的问题。你可以把它理解成一个统一的 API 通道:一个 Key,一套 Base URL,背后可以切换不同模型。MCP Client 负责工具发现和调用,TaoToken 负责模型推理这一段,两边职责清晰,互不干扰。
适合读这篇的人有三类:一是已经在用 Claude Desktop 或 Cline 但还没跑通自定义 MCP Server 的;二是想用 Python 写自己的 MCP Server 但卡在配置和调试上的;三是手里有多个 AI 客户端、想统一模型接入层减少重复配置的。下面我会从环境准备开始,一步步把 Server 写出来、把 TaoToken 的 Key 配进去、再演示一次完整的工具调用验证。整个过程你可以直接复制粘贴跟做。
2. TaoToken 前置准备:统一 Key 与 API 通道怎么配才不踩坑
在写 MCP Server 之前,先把模型侧的通道准备好。这一步很多人会跳过,结果后面调试时分不清是 Server 的问题还是 API 的问题。我的建议是:先把模型 API 单独验证通,再接入 MCP 链路。
TaoToken 的官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api 。注意 API 地址后面不加 UTM 参数,直接用于代码里的 base_url。你需要先在控制台创建一个 API Key,入口在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。创建完 Key 后,建议立刻复制保存,因为部分控制台只展示一次。
拿到 Key 之后,先别急着写 MCP 代码。用最朴素的方式验证一下通道是否可用。Python 环境下,你可以用 OpenAI SDK 兼容的方式测试,因为 TaoToken 的 API 兼容 OpenAI 格式:
from openai import OpenAI client = OpenAI( api_key="你的_TaoToken_Key", base_url="https://taotoken.net/api" ) resp = client.chat.completions.create( model="claude-sonnet-4-20250514", messages=[{"role": "user", "content": "只回复两个字:通了"}] ) print(resp.choices[0].message.content)如果输出“通了”,说明 Key 和通道都没问题。这一步看起来简单,但它帮你排除了后面 80% 的“MCP 连不上”误判。因为 MCP 链路里,Client 调模型和 Client 调 Server 是两条独立的通道,模型通道不通,工具列表根本传不到模型面前。
接下来是模型 ID 的选择。TaoToken 支持多种模型,你在 MCP 场景下要优先选支持 tool calling 的模型。因为 MCP 的核心动作就是模型输出结构化的 tool_call,如果模型不支持 function calling,Client 就没法把工具列表交给它决策。实测下来,Claude 系列和 GPT 系列在 tool calling 上表现稳定,适合做 MCP 的推理后端。
关于 Key 的管理,我踩过的坑是:把 Key 硬编码在 MCP Server 的代码里。这有两个问题。第一,MCP Server 的职责是提供工具,不应该持有模型 Key,模型 Key 应该属于 Client 侧或独立的 API 网关。第二,一旦 Key 泄露,你没法单独轮换,因为 Server 代码可能已经分发到多台机器。正确的做法是:MCP Server 只管工具逻辑,模型 Key 配在 Client 的配置里,或者通过环境变量注入。
如果你用的是 Claude Code 或 Cline 这类客户端,它们通常有自己的模型配置入口。以 Cline 为例,你需要在设置里填 Base URL、API Key、Model ID 三件套。Base URL 填 https://taotoken.net/api ,API Key 填你刚创建的 Key,Model ID 填你验证过的模型名。这三者缺一不可,而且必须和你在 Python 里测试时用的完全一致,否则会出现“Python 能通但 Client 报 401”的诡异现象。
还有一个细节:TaoToken 的 API 通道支持流式输出。MCP 场景下,Client 和模型的交互通常是流式的,因为模型要边生成边决定是否调用工具。如果你的 Client 配置里有关闭流式的选项,建议保持开启,否则 tool_call 的解析可能会延迟或截断。
最后提醒一点:不要把 TaoToken 的 Key 和 MCP Server 的启动命令混在一起。MCP Server 通过 stdio 启动时,它的 stdin/stdout 是给 JSON-RPC 用的,任何多余的打印都会污染协议流。所以 Server 代码里不要 print 调试信息,日志走 stderr 或文件。这一点后面排障章节会再展开。
3. 可复制配置:MCP Server 的 JSON-RPC 握手与 Python 侧 settings 片段
现在进入正题,写一个能跑的 MCP Server。我选的是系统信息查询工具,因为它不依赖外部服务,适合验证链路。但为了贴合“连接世界”的主题,我会在基础版上加一个 HTTP 请求工具,让 Agent 能真正访问外部 API。
先建项目目录和虚拟环境:
mkdir mcp-sysinfo && cd mcp-sysinfo python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install mcp psutil httpx然后创建 server.py。这个文件的核心是三个部分:Server 实例、工具列表声明、工具调用分发。MCP 的握手过程由 stdio_server 自动处理,你不需要手动写 JSON-RPC 的 initialize 请求,框架会帮你完成协议协商。
import json import psutil import httpx from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent server = Server("sysinfo") @server.list_tools() async def list_tools(): return [ Tool( name="get_cpu_info", description="获取 CPU 使用率和核心数信息", inputSchema={"type": "object", "properties": {}, "required": []} ), Tool( name="get_memory_info", description="获取系统内存使用情况", inputSchema={"type": "object", "properties": {}, "required": []} ), Tool( name="http_get", description="发送 HTTP GET 请求并返回响应摘要", inputSchema={ "type": "object", "properties": { "url": {"type": "string", "description": "完整 URL"} }, "required": ["url"] } ) ] @server.call_tool() async def call_tool(name: str, arguments: dict): if name == "get_cpu_info": cpu_percent = psutil.cpu_percent(interval=1) result = { "cpu_percent": cpu_percent, "cpu_count_logical": psutil.cpu_count(), } elif name == "get_memory_info": mem = psutil.virtual_memory() result = { "total_gb": round(mem.total / (1024**3), 2), "available_gb": round(mem.available / (1024**3), 2), "percent": mem.percent, } elif name == "http_get": url = arguments.get("url", "") async with httpx.AsyncClient(timeout=10) as client: r = await client.get(url) result = { "status_code": r.status_code, "content_length": len(r.text), "preview": r.text[:200] } else: result = {"error": f"未知工具: {name}"} return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))] async def main(): async with stdio_server() as (read_stream, write_stream): await server.run(read_stream, write_stream, server.create_initialization_options()) if __name__ == "__main__": import asyncio asyncio.run(main())这段代码里,list_tools 返回的是工具元数据,call_tool 是实际执行入口。MCP 的 JSON-RPC 握手由 stdio_server 封装,Client 发来的 initialize、tools/list、tools/call 都会被框架路由到对应处理函数。你不需要手动解析 JSON-RPC 报文。
接下来是 Client 侧的配置。以 Claude Desktop 为例,配置文件路径是:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json Windows: %APPDATA%\Claude\claude_desktop_config.json
配置内容如下:
{ "mcpServers": { "sysinfo": { "command": "/absolute/path/to/.venv/bin/python", "args": ["/absolute/path/to/server.py"], "env": { "PYTHONUNBUFFERED": "1" } } } }注意 command 要指向虚拟环境里的 python,不要用系统 python,否则依赖找不到。args 里的路径必须是绝对路径。env 里加 PYTHONUNBUFFERED 是为了避免 stdout 缓冲导致 JSON-RPC 消息延迟。
如果你用的是 Cline 或 Continue.dev,配置格式类似,但字段名可能不同。Cline 的 MCP 配置通常在设置面板里,你需要填 Server 名称、启动命令、参数。有些客户端还支持 SSE 模式的远程 Server,那种情况下你需要把 stdio_server 换成 SSE server,并暴露一个 HTTP 端口。
关于模型侧的配置,如果你在 Cline 里同时配了 TaoToken 的 API,那么 Cline 会用 TaoToken 的模型来决策工具调用,然后通过 stdio 把 tool_call 转发给你的 Server。这时候你的 settings 里应该有三件套:
Base URL: https://taotoken.net/api API Key: 你的_TaoToken_Key Model ID: claude-sonnet-4-20250514
这三者要和你在 Python 里验证时用的完全一致。Model ID 尤其重要,因为不同模型对 tool calling 的支持程度不同。如果你填了一个不支持 function calling 的模型,Client 会报“model does not support tools”之类的错误。
配置完成后,重启 Claude Desktop 或重新加载 Cline。你会看到工具列表里多出 get_cpu_info、get_memory_info、http_get 三个工具。这时候链路已经建立,但还没验证。下一节我会演示一次完整的工具调用,确认 Agent 真的能连通外部能力。
4. 验证请求与成功结果:一次完整的工具调用长什么样
配置好之后,怎么确认 Agent 真的调用了你的 MCP Server,而不是在“假装调用”?最直接的方式是看 Server 侧的日志和 Client 侧的返回。
先启动 Server 单独测试。在终端里运行:
python server.py如果没有任何输出,说明 Server 在等待 stdin 的 JSON-RPC 请求。这时候你可以手动发一条 initialize 请求测试:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | python server.py你应该会看到一条 JSON-RPC 响应,包含 serverInfo 和 capabilities。这说明 Server 的握手逻辑正常。
然后回到 Client 侧。在 Claude Desktop 里输入:“帮我查一下当前 CPU 使用率和内存情况。” 如果一切正常,你会看到 Claude 先输出一段“我来调用工具查询”,然后工具图标旁边出现执行状态,最后返回类似这样的结果:
{ "cpu_percent": 12.3, "cpu_count_logical": 8, "total_gb": 16.0, "available_gb": 9.2, "percent": 42.5 }这个结果不是模型编的,而是你的 Python 代码通过 psutil 真实读取的。你可以打开任务管理器对照,数字应该基本一致。
再测试 http_get 工具。输入:“用 http_get 访问 https://taotoken.net/api 看看返回什么。” 注意这里访问的是 API 根路径,可能返回 404 或 405,但重点是验证工具被调用了。你应该看到返回里有 status_code 和 content_length。如果 status_code 是 404,说明请求发出去了,只是路径不对,这恰恰证明链路通了。
如果你想更严谨地验证,可以在 Server 的 call_tool 里加一行 stderr 日志:
import sys print(f"[MCP] 调用工具: {name}, 参数: {arguments}", file=sys.stderr)注意必须输出到 stderr,不能输出到 stdout,否则会污染 JSON-RPC 流。重启 Server 后,在 Client 里再调用一次,你会在终端看到对应的日志。这是最可靠的验证方式:Client 侧看到结果,Server 侧看到日志,两边对得上,说明整条链路没有断点。
还有一个验证技巧:故意传一个不存在的工具名。比如在 Client 里问“调用一个叫 foo 的工具”,如果 Client 返回“工具不存在”,说明工具列表已经正确同步到模型侧。如果模型直接编了一个结果,说明工具列表没传过去,问题出在模型通道或 Client 配置上。
实测下来,最常见的“假成功”是:Client 显示调用了工具,但 Server 侧没有任何日志。这通常是因为 Client 缓存了旧的工具列表,或者 Server 进程没重启。解决办法是彻底退出 Client(不是关窗口,是退出进程),再重新打开。
另外,如果你在 Cline 里测试,它有一个“MCP Servers”面板,会显示每个 Server 的连接状态和工具数量。如果显示“connected”但工具数量为 0,说明 list_tools 返回了空列表,检查你的 @server.list_tools() 装饰器是否生效。
验证通过后,你就可以在这个基础上扩展更多工具了。比如加一个 search_files 工具做本地文件搜索,或者加一个 query_database 工具做只读 SQL 查询。每加一个工具,都要重新走一遍“Client 调用 → Server 日志 → 结果返回”的验证流程,确保没有引入新的断点。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth 逐个拆
MCP 链路的报错有个特点:同一个现象可能来自不同层。比如“工具调用失败”可能是模型通道 401,也可能是 Server 进程没起来。下面我按真实遇到的报错逐个拆。
401 Unauthorized
这个报错几乎都出在模型通道,不是 MCP Server。如果你在 Client 里看到 401,先检查三件套:Base URL 是不是 https://taotoken.net/api ,API Key 是不是复制完整(有没有多余空格),Model ID 是不是拼写正确。特别注意,有些客户端会把 Base URL 和完整路径拼接,比如自动加上 /v1/chat/completions。TaoToken 的 API 地址是 https://taotoken.net/api ,如果客户端自动补 /v1,最终变成 https://taotoken.net/api/v1/chat/completions,这个路径是兼容的。但如果客户端补成了别的路径,就会 404 或 401。解决办法是在 Client 的 Base URL 里只填 https://taotoken.net/api ,不要手动加 /v1。
local proxy failed
这个报错通常出现在 Client 尝试通过本地代理访问模型 API 时。如果你没有配代理,但 Client 设置里残留了 proxy 配置,就会报这个。检查 Client 的网络设置,把 proxy 关掉或清空。另外,有些 Client 会读取系统环境变量 HTTP_PROXY 和 HTTPS_PROXY,如果这两个变量指向了一个不可用的地址,也会导致 local proxy failed。在终端里 unset 这两个变量再启动 Client 试试。
reading choices 报错
这个报错一般长这样:“error reading choices: unexpected end of JSON input” 或 “cannot read property choices of undefined”。它说明 Client 收到了模型返回,但解析失败。常见原因有三个:一是模型返回了非 JSON 格式的内容,比如纯文本;二是流式响应被截断;三是 Client 的模型配置里选了不支持 tool calling 的模型。解决办法是先在 Python 里用同样的模型 ID 发一条普通对话,确认返回格式正常。如果 Python 正常但 Client 报错,检查 Client 是否开启了流式,尝试关闭流式再试。
OAuth 相关报错
有些 MCP Client 在连接远程 Server 时会走 OAuth 流程。如果你用的是 stdio 本地 Server,一般不会遇到 OAuth。但如果你在 Client 里配置了 SSE 模式的远程 Server,而 Server 端没有实现 OAuth 端点,就会报“OAuth discovery failed”或“invalid token”。解决办法是确认你的 Server 是 stdio 还是 SSE。stdio 不需要 OAuth,SSE 需要。如果你只是本地测试,用 stdio 就够了,不要配 SSE。
工具列表为空
Client 显示 connected 但工具数量为 0。检查 Server 的 list_tools 是否被正确装饰。有些教程里用的是 @server.list_tools() 不带参数,但不同版本的 mcp 库 API 可能不同。确认你安装的 mcp 版本,用 pip show mcp 查看。如果版本较新,list_tools 可能需要返回 types.ListToolsResult 而不是裸列表。遇到这种情况,升级或降级 mcp 库到文档对应的版本。
Server 启动即退出
运行 python server.py 后立刻回到命令行,没有任何输出。这通常是因为 asyncio.run(main()) 里的 stdio_server 没有正确进入等待状态。检查你的 main 函数是否用了 async with stdio_server(),以及 server.run 是否传入了 create_initialization_options()。另外,如果你在 Server 代码里写了 print 到 stdout,会导致 JSON-RPC 流被污染,Server 可能异常退出。把所有调试输出改到 stderr。
Client 调用工具后卡住
Client 显示“正在调用工具”但一直不返回。这通常是 Server 里的工具函数阻塞了事件循环。比如用了同步的 requests 而不是异步的 httpx,或者 psutil.cpu_percent(interval=1) 这种阻塞调用。解决办法是把阻塞操作放到线程池里执行,或者用异步库替代。MCP 的 stdio 通信是异步的,任何阻塞都会导致 JSON-RPC 响应延迟。
排障的核心思路是分层:先确认模型通道通(Python 直连测试),再确认 Server 能启动(手动发 initialize),再确认 Client 能发现工具(看工具列表),最后确认工具能执行(看 Server 日志)。每一层单独验证,不要混在一起猜。
6. 语义一致 CTA:把统一 Key 和 MCP 链路固化下来
跑通一次工具调用之后,下一步是把这套配置固化下来,让它能稳定复现。我的做法是:模型侧统一走 TaoToken 的 API 通道,MCP Server 侧保持纯工具逻辑,两边通过 Client 的配置解耦。
如果你还在调试阶段,建议先把 API Key 和接入文档过一遍。API Key 在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 创建和管理,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 有完整的 Base URL 和参数说明。这两个页面建议收藏,后面换模型或加客户端时会反复用到。
想先验证模型对话是否正常,可以用 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 里的对话入口,直接测试 tool calling 相关的模型。如果你打算长期跑编码类 Agent,比如让 MCP Server 配合代码编辑器做自动化,Coding Plan 在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 有更详细的配置说明。
回到 MCP 本身。这套链路的稳定性取决于两个变量:模型通道的可用性和 Server 进程的健壮性。模型通道交给 TaoToken 统一管理,你只需要维护一个 Key 和一套 Base URL。Server 进程建议用 systemd 或 supervisor 托管,避免终端关闭后进程退出。如果你在 Claude Code 里用 MCP,配置入口在 https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode-anthropic&utm_campaign=rewrite ,里面有 Claude Code 专用的接入参数。
最后说一个实用技巧:把 MCP Server 的启动命令写成一个 shell 脚本,里面先激活虚拟环境,再启动 Python。这样 Client 配置里只需要指向这个脚本,不用关心虚拟环境路径。脚本内容大概是这样:
#!/bin/bash cd /absolute/path/to/mcp-sysinfo source .venv/bin/activate exec python server.py给脚本加执行权限,然后在 Client 配置里把 command 指向这个脚本。这样即使你换了 Python 版本或虚拟环境路径,只需要改脚本,不用动 Client 配置。实测下来,这个做法能省掉很多“路径不对”的排查时间。