1. MCP 到底是什么:从“AI 工具孤岛”到统一调用链路
如果你最近在折腾 Claude Code、Cline、Cursor 这类工具,大概率会反复看到一个词:MCP。它的全称是 Model Context Protocol,翻译过来叫“模型上下文协议”。你可以把它理解成 AI 世界里的 USB-C 接口——以前每个模型要对接每个工具,都得单独写一套适配代码,M 个模型对接 N 个工具就是 M×N 份工作量;有了 MCP,模型和工具之间只用遵守同一套协议,工作量直接降到 M+N。
MCP 能做什么?简单说,它让大模型可以标准化地调用外部能力:读本地文件、查数据库、调第三方 API、操作 GitHub、跑命令行。适合谁?适合所有想让 AI 从“只会聊天”变成“能干活”的开发者,尤其是正在做 Agent、RAG、自动化工作流的同学。
我先把 MCP 的核心角色拆清楚,不然后面看代码会晕:
- MCP Host(主机):你直接交互的 AI 应用,比如 Claude Desktop、某个 IDE 插件。
- MCP Client(客户端):主机内部负责和服务器通信的中间层,一个服务器对应一个客户端连接。
- MCP Server(服务器):真正提供能力的程序,比如一个“天气查询服务器”或“文件读取服务器”。
它们之间的通信走的是 JSON-RPC 2.0,传输方式支持 stdio(标准输入输出)、HTTP SSE、WebSocket 等。整个调用链路可以概括成五步:初始化握手 → 能力发现(服务器告诉客户端我有哪些 tool)→ 上下文提供(客户端把用户请求转成调用参数)→ 调用执行(服务器干活并返回结果)→ 结果整合(模型把结果融进回答)。
这里有个关键点很多人第一次会踩坑:模型必须支持 function calling(工具调用),否则 MCP 根本跑不起来。因为客户端拿到服务器的 tool 列表后,要把它转成模型能识别的 tools 格式发给模型,模型决定“要不要调、调哪个、传什么参数”,这个决策过程依赖 function calling 能力。Claude、GPT 系列都支持,一些老模型或纯文本模型就不行。
理解了这条链路,你再看源码就不会迷路:客户端做的事无非是“连服务器 → 拉 tool 列表 → 转格式 → 发给模型 → 收到 tool_use 就回调服务器”。下面我们就从零把这套链路在本地跑通,并且用 TaoToken 统一 Key 解决鉴权问题。
2. TaoToken 前置准备:统一 Key 与 API 通道配置
在正式写 MCP 代码前,先解决一个现实问题:MCP 客户端要调用大模型,就得有 API Key。如果你同时用 Claude、GPT 等多个模型,每个平台单独申请 Key、单独充值、单独管理额度,非常碎。TaoToken 的思路是提供一个统一的 API 通道和统一 Key,让你用一套凭证访问多个模型,MCP 客户端里只配一次就行。
先明确几个地址,后面配置会反复用到:
- 官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- API 基础地址:https://taotoken.net/api
- 模型对话体验:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite
- Coding Plan(长期编码/Agent 场景):https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite
- 控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
- API Keys 管理: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
操作顺序建议这样:先进控制台,再进 API Keys 页面创建一个 Key,复制出来先存到本地环境变量里,别硬编码进代码。创建 Key 的时候注意权限范围,MCP 客户端只需要调用模型的能力,不需要开一堆无关权限。
拿到 Key 后,把它写进环境变量。macOS/Linux 用:
export TAOTOKEN_API_KEY="sk-你的key"Windows PowerShell 用:
$env:TAOTOKEN_API_KEY="sk-你的key"如果你用的是 Claude Code 这类工具,它支持通过 settings 文件配置。在项目根目录建.claude/settings.json,写入下面这段(路径和字段名保持原样):
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "sk-你的key", "ANTHROPIC_MODEL": "claude-3-5-sonnet-20241022" } }这三件套——Base URL、Key、Model ID——是任何 MCP 客户端接入模型时都绕不开的。Base URL 指向 TaoToken 的 API 通道,Key 用你刚创建的,Model ID 填你要用的模型。如果你用的是 Cline 或 CC Switch,配置逻辑一样,只是字段名可能叫baseUrl、apiKey、model,对照文档填即可。
这里提醒一句:Key 不要提交到 Git,建议加进.gitignore,或者用.env文件配合 dotenv 加载。我见过太多人把 Key 写死在代码里然后推到公开仓库,结果被刷爆额度。
3. 可复制配置:MCP 服务端与客户端完整代码
这一节是重头戏,我们把服务端和客户端都写出来,并且让客户端通过 TaoToken 的 API 通道调用模型。先建项目:
mkdir mcp-demo && cd mcp-demo npm init -y npm install @modelcontextprotocol/sdk @anthropic-ai/sdk zod3.1 服务端:一个天气查询 MCP Server
新建server.js,实现一个get_weather工具:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const server = new McpServer( { name: "weather-server", version: "1.0.0" }, { capabilities: { tools: {} } } ); server.tool( "get_weather", "获取指定城市的天气信息", { city: z.string().describe("城市名称") }, async ({ city }) => { const weatherData = { 北京: "晴,25°C", 上海: "多云,22°C", 广州: "雨,28°C", }; const weather = weatherData[city] || "未知城市,暂无数据"; return { content: [{ type: "text", text: `今天${city}的天气:${weather}` }], }; } ); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("天气服务器已启动!"); } main().catch((error) => { console.error("服务器错误:", error); process.exit(1); });注意这里用console.error而不是console.log输出启动信息。因为 stdio 传输模式下,标准输出是留给 JSON-RPC 消息的,你往 stdout 打日志会污染协议数据,导致客户端解析失败。这是新手最常踩的坑之一。
3.2 客户端:通过 TaoToken 调用模型并回调 MCP
新建client.js,核心是把 MCP 的 tool 列表转成模型能识别的格式,并在模型返回tool_use时回调服务器:
import { Anthropic } from "@anthropic-ai/sdk"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import readline from "readline/promises"; const API_KEY = process.env.TAOTOKEN_API_KEY; class MCPClient { constructor() { this.anthropic = new Anthropic({ apiKey: API_KEY, baseURL: "https://taotoken.net/api", }); this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" }); this.transport = null; this.tools = []; } async connectToServer(serverScriptPath) { const command = process.execPath; this.transport = new StdioClientTransport({ command, args: [serverScriptPath], }); await this.mcp.connect(this.transport); const toolsResult = await this.mcp.listTools(); this.tools = toolsResult.tools.map((tool) => ({ name: tool.name, description: tool.description, input_schema: tool.inputSchema, })); console.log("已连接服务器,可用工具:", this.tools.map((t) => t.name)); } async processQuery(query) { const messages = [{ role: "user", content: query }]; const response = await this.anthropic.messages.create({ model: "claude-3-5-sonnet-20241022", max_tokens: 1024, messages, tools: this.tools, }); const finalText = []; for (const content of response.content) { if (content.type === "text") { finalText.push(content.text); } else if (content.type === "tool_use") { const result = await this.mcp.callTool({ name: content.name, arguments: content.input, }); finalText.push(`[调用工具 ${content.name}]`); messages.push({ role: "user", content: result.content }); const followUp = await this.anthropic.messages.create({ model: "claude-3-5-sonnet-20241022", max_tokens: 1024, messages, }); finalText.push( followUp.content[0].type === "text" ? followUp.content[0].text : "" ); } } return finalText.join("\n"); } async chatLoop() { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); try { console.log("\nMCP 客户端已启动,输入 quit 退出"); while (true) { const message = await rl.question("\nQuery: "); if (message.toLowerCase() === "quit") break; const response = await this.processQuery(message); console.log("\n" + response); } } finally { rl.close(); } } async cleanup() { await this.mcp.close(); } } async function main() { if (process.argv.length < 3) { console.log("用法: node client.js <server脚本路径>"); return; } const client = new MCPClient(); try { await client.connectToServer(process.argv[2]); await client.chatLoop(); } finally { await client.cleanup(); process.exit(0); } } main();关键配置就三处:apiKey用 TaoToken 的 Key,baseURL指向https://taotoken.net/api,model填你要用的模型 ID。这样客户端调用模型时走的是 TaoToken 通道,而 MCP 服务器本身不需要任何 Key,它只负责执行工具逻辑。
4. 验证请求:跑通一次完整调用并检查返回结果
配置写完了,现在跑起来验证。先启动客户端并传入服务端脚本路径:
node client.js ./server.js如果一切正常,你会看到:
已连接服务器,可用工具: [ 'get_weather' ] MCP 客户端已启动,输入 quit 退出 Query:输入“北京今天天气怎么样”,观察返回。预期结果是模型先返回一个tool_use,客户端回调get_weather,服务器返回“今天北京的天气:晴,25°C”,然后模型把结果整合成自然语言回答。
检查清单如下,逐条对照:
- 客户端启动后是否打印了可用工具列表?没有的话说明
listTools失败,检查服务器是否正常启动。 - 输入查询后是否出现
[调用工具 get_weather]?没有的话说明模型没触发 function calling,检查模型 ID 是否支持工具调用。 - 工具返回内容是否包含正确城市数据?如果返回“未知城市”,说明参数传递有问题,检查
content.input是否被正确解析。 - 最终回答是否自然整合了天气信息?如果只输出了工具原始结果,说明第二轮模型调用没执行。
实测下来,最容易出问题的是第二轮调用。有些模型在收到工具结果后不会自动继续生成,需要你显式再发一次messages.create,代码里已经处理了这一点。
如果你想单独验证 TaoToken 通道是否通,可以先用模型对话页面发一条消息测试:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。通道没问题再回来跑 MCP,能省很多排查时间。
5. 本篇常见错排查:401、local proxy failed、reading choices
这一节把真实会遇到的报错列出来,对照解决。
报错一:401 Unauthorized
Error: 401 {"error":{"message":"Invalid API key"}}原因通常是 Key 没读到或写错了。检查process.env.TAOTOKEN_API_KEY是否真的有值,可以在代码里临时打印API_KEY ? "已设置" : "未设置"。另外注意 Key 有没有多余空格,复制时容易带上换行。如果用的是 settings.json 配置,确认字段名是ANTHROPIC_AUTH_TOKEN而不是ANTHROPIC_API_KEY,两者在不同工具里含义不同。
报错二:local proxy failed / connection refused
Error: connect ECONNREFUSED 127.0.0.1:xxxx这种多半是 baseURL 配错了,或者本地有残留的代理配置指向了一个不存在的端口。检查baseURL是否严格写成https://taotoken.net/api,不要多加路径或斜杠。同时检查环境变量里有没有HTTP_PROXY、HTTPS_PROXY之类的残留,有的话先清掉。
报错三:reading 'choices' / Cannot read properties of undefined
TypeError: Cannot read properties of undefined (reading 'choices')这个报错通常出现在你把 OpenAI 格式的响应解析逻辑套在了 Anthropic 格式上,或者反过来。Anthropic SDK 返回的是response.content数组,OpenAI 返回的是response.choices。确认你用的 SDK 和模型格式匹配。如果你在 TaoToken 通道里切换了模型,也要确认客户端 SDK 是否对应。
报错四:OAuth / authentication_error
Error: authentication_error: invalid x-api-key如果你用的是 Claude Code 或类似工具,它可能默认走 OAuth 登录而不是 API Key。需要在 settings 里显式配置ANTHROPIC_AUTH_TOKEN和ANTHROPIC_BASE_URL,让它走 Key 鉴权而不是 OAuth。配置三件套(Base URL + Key + Model ID)缺一不可。
报错五:服务器启动了但客户端连不上
检查服务端是不是用了console.log往 stdout 打日志。stdio 模式下 stdout 被协议占用,任何非 JSON-RPC 输出都会导致解析失败。把所有日志改成console.error。
6. 语义一致 CTA:把 MCP 链路接到你的日常开发里
跑通这个 demo 之后,你其实已经掌握了 MCP 的完整调用链路:服务端定义 tool,客户端拉取并转格式,模型决策调用,客户端回调,结果整合。剩下的就是把这个模式套到真实场景——文件操作、数据库查询、API 调用,逻辑完全一样。
如果你要长期用 MCP 做编码或 Agent 开发,建议把 Key 管理统一到 TaoToken,避免多平台切换。需要创建或管理 Key 就去 API Keys 页面: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 。如果是长期编码场景,Coding Plan 会更划算:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。
最后留一个实用技巧:调试 MCP 时,先把服务端的 tool 逻辑单独跑一遍,确认输入输出正确,再接客户端。这样出问题时你能快速定位是协议层还是业务层。我试过跳过这一步直接联调,结果在协议格式上耗了大半天,后来分开验证就快多了。