1. 从一个真实痛点说起:为什么外部系统接 MCP 总是跑不通
很多人第一次接触 MCP(Model Context Protocol)时,脑子里想的都是"这不就是个能调工具的协议吗",然后兴冲冲地打开编辑器,写一个call_api()函数,把公司工单系统的 REST 接口原样包一层,结果模型要么不调用,要么乱传参数,要么调用完拿到一堆看不懂的报错。问题不在 HTTP 请求怎么写,而在于你没有把外部系统的能力切成模型能理解、能安全调用的接口。
MCP Tool 的本质是接口设计。它要回答四个问题:这个工具给谁用、输入长什么样、输出长什么样、出错时怎么表达。把这四件事想清楚,再动手写 Server,成功率会高很多。这篇内容聚焦一条完整链路:从 Tool schema 定义、Server 端参数校验,到 OpenClaw 侧调用验证,最后用 TaoToken 统一 Key 通道管理鉴权。目标很明确——你照着配置就能跑通一个能被 MCP 客户端识别的外部工具。
适合谁看?已经理解 MCP 基本概念、想把公司内部系统(工单、CRM、部署平台、知识库)接进 Agent 的开发者。如果你还在纠结"MCP 是什么",建议先补一下基础,再回来跟着做。下面所有步骤都可以直接复制,我尽量把每个参数、每个报错都写清楚。
2. 前置准备:TaoToken 统一 Key 通道与 MCP Server 环境
在写 schema 之前,先把"鉴权通道"这件事解决掉。外部系统通常需要 token、OAuth 或服务账号,如果每个 MCP Server 都自己管一套密钥,很快就会乱。我的做法是:所有出站请求统一走 TaoToken 的 API 通道,MCP Server 只持有一个 TaoToken Key,外部系统的真实凭证由通道侧管理。这样模型侧永远看不到真实密钥,日志里也不会打印敏感信息。
TaoToken 在这里扮演的是统一 Key/API 通道的角色。你可以在官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 了解整体能力,实际接入时用 API 地址 https://taotoken.net/api(注意这个地址不加 UTM 参数)。它的价值在于:一个 Key 打通多个模型和工具调用,MCP Server 不需要为每个上游系统维护独立凭证。
环境准备清单:
- Node.js 18+ 或 Python 3.10+(本文用 Node.js 示例,Python 思路一致)
- 一个可用的 TaoToken API Key(在控制台创建,见下方 deep link)
- OpenClaw 客户端(用于最终调用验证)
- 一个你想接入的外部系统(本文以"内部订单系统"为例)
创建 Key 的入口:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=mcp_tool_server&utm_campaign=rewrite ,Key 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=mcp_tool_server&utm_campaign=rewrite 。接入文档参考 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=mcp_tool_server&utm_campaign=rewrite 。
注意:不要把 TaoToken Key 硬编码进 MCP Server 源码,用环境变量注入。stdio server 的日志一律写 stderr,写 stdout 会破坏 JSON-RPC 消息流,这是新手最常踩的坑。
初始化项目:
mkdir mcp-order-server && cd mcp-order-server npm init -y npm install @modelcontextprotocol/sdk zod装完这两个包,基础环境就好了。@modelcontextprotocol/sdk提供 Server 和 Transport 实现,zod用来做参数校验和 schema 生成。
3. 可复制配置:Tool schema 设计与 Server 落地片段
这一步是核心。先选一个窄场景,不要把整个订单系统一次性接进来。我们只做三件事:按订单号查询、查退款状态、添加内部备注。前两个是读操作,第三个有副作用但风险低。真正的退款动作不做成无确认工具。
3.1 Tool schema 模板
坏 schema 长这样:do_order_action(action, data)——模型根本不知道action能填什么,data里该放什么。好 schema 要具体到字段类型、枚举值、必填项。
{ "name": "order_lookup", "description": "根据订单号查询订单基础信息,只读操作,不产生任何副作用。", "inputSchema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,格式为 ORD- 开头加 8 位数字,例如 ORD-20240101" }, "include_items": { "type": "boolean", "description": "是否返回订单明细行,默认 false", "default": false } }, "required": ["order_id"], "additionalProperties": false } }三个关键点:description写清楚工具做什么、是否只读;order_id给出格式示例,模型才知道怎么填;additionalProperties: false防止模型塞入未定义字段。
3.2 Server 端参数校验与实现
用 zod 定义 schema,SDK 会自动生成 JSON Schema 并做运行时校验:
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const TAOTOKEN_BASE = "https://taotoken.net/api"; const TAOTOKEN_KEY = process.env.TAOTOKEN_API_KEY; const OrderLookupInput = z.object({ order_id: z.string().regex(/^ORD-\d{8}$/, "订单号格式应为 ORD- 加 8 位数字"), include_items: z.boolean().optional().default(false), }); async function callUpstream(path, body) { const res = await fetch(`${TAOTOKEN_BASE}${path}`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${TAOTOKEN_KEY}`, }, body: JSON.stringify(body), }); if (!res.ok) { return { ok: false, code: res.status === 401 ? "UPSTREAM_UNAUTHORIZED" : "UPSTREAM_ERROR", message: `上游返回 ${res.status}`, retryable: res.status >= 500, }; } return { ok: true, data: await res.json() }; } const server = new Server( { name: "order-mcp-server", version: "1.0.0" }, { capabilities: { tools: {} } } ); server.setRequestHandler("tools/list", async () => ({ tools: [ { name: "order_lookup", description: "根据订单号查询订单基础信息,只读操作。", inputSchema: { type: "object", properties: { order_id: { type: "string", description: "订单号,如 ORD-20240101" }, include_items: { type: "boolean", default: false }, }, required: ["order_id"], additionalProperties: false, }, }, ], })); server.setRequestHandler("tools/call", async (req) => { if (req.params.name !== "order_lookup") { return { content: [{ type: "text", text: JSON.stringify({ ok: false, code: "TOOL_NOT_FOUND", message: `未知工具 ${req.params.name}`, retryable: false, })}], isError: true, }; } const parsed = OrderLookupInput.safeParse(req.params.arguments); if (!parsed.success) { return { content: [{ type: "text", text: JSON.stringify({ ok: false, code: "INVALID_ARGUMENT", message: parsed.error.issues[0].message, retryable: false, })}], isError: true, }; } const result = await callUpstream("/v1/order/lookup", parsed.data); return { content: [{ type: "text", text: JSON.stringify(result) }], isError: !result.ok, }; }); const transport = new StdioServerTransport(); await server.connect(transport); console.error("order-mcp-server started"); // 必须写 stderr3.3 OpenClaw 侧 MCP Server 配置
在 OpenClaw 的 MCP 配置里注册这个 Server。配置文件通常放在~/.openclaw/mcp.json(路径以你本地实际为准):
{ "mcpServers": { "order": { "command": "node", "args": ["/absolute/path/to/mcp-order-server/index.js"], "env": { "TAOTOKEN_API_KEY": "sk-your-taotoken-key" } } } }三件套必须齐全:Base URL(https://taotoken.net/api)、Key(环境变量注入)、Model ID(在 OpenClaw 模型配置里指定,例如claude-sonnet-4或你账号下可用的模型标识)。缺任何一个,调用都会失败。
4. 验证请求:一次端到端调用与成功结果
配置写完后,先别急着让模型调用,手动验证三步:Server 能启动、tools/list能看到工具、tools/call能返回结果。
启动 Server:
TAOTOKEN_API_KEY=sk-your-key node index.js如果终端输出order-mcp-server started且没有报错,说明 stdio 通道正常。接着用 MCP Inspector 或直接发 JSON-RPC 消息验证:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | \ TAOTOKEN_API_KEY=sk-your-key node index.js预期返回:
{ "jsonrpc": "2.0", "id": 1, "result": { "tools": [ { "name": "order_lookup", "description": "根据订单号查询订单基础信息,只读操作。", "inputSchema": { "type": "object", "properties": { "...": {} } } } ] } }再验证一次真实调用:
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"order_lookup","arguments":{"order_id":"ORD-20240101"}}}' | \ TAOTOKEN_API_KEY=sk-your-key node index.js成功时返回{"ok":true,"data":{...}},失败时返回结构化错误。到这里,Server 侧就通了。
最后在 OpenClaw 里验证。重启 OpenClaw 让它加载新的 MCP 配置,然后在对话里输入:
帮我查一下订单 ORD-20240101 的状态
如果 OpenClaw 正确识别并调用了order_lookup,你会看到工具调用记录和返回结果。这一步跑通,说明整条链路——schema 定义、Server 校验、TaoToken 通道鉴权、OpenClaw 消费——全部打通。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
接入过程中最常见的四类报错,我按真实日志对照给你排查思路。
401 Unauthorized / UPSTREAM_UNAUTHORIZED:TaoToken Key 没注入或写错。检查env里的TAOTOKEN_API_KEY是否和 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=mcp_tool_server&utm_campaign=rewrite 里创建的一致。注意 Key 不要带多余空格,环境变量名大小写要匹配。
local proxy failed / connection refused:Server 进程没起来,或者 OpenClaw 配置里的command/args路径不对。用绝对路径,别用相对路径。先手动node index.js确认能启动,再让 OpenClaw 拉起。
Error reading choices / unexpected token:通常是 stdout 被污染了。检查代码里有没有console.log,stdio server 的所有日志必须走console.error。JSON-RPC 消息流里混入普通文本,客户端解析就会失败。
OAuth / token expired:外部系统的 OAuth token 过期。如果你走 TaoToken 通道,检查通道侧凭证是否有效;如果是直连外部系统,需要实现 token 刷新逻辑。建议把刷新逻辑放在通道侧,MCP Server 只关心业务参数。
提示:每次改完配置,先手动跑一遍
tools/list,确认 Server 本身没问题,再去 OpenClaw 里测。这样能把"Server 问题"和"客户端配置问题"分开定位。
6. 继续深入:从单工具到多工具与 Coding Plan
跑通一个工具后,下一步是扩展。按同样的方法,把order_refund_status(读)和order_add_internal_note(低风险写)加进来。注意写操作要有副作用标记,高风险动作(如真实退款)不要做成无确认工具,而是拆成"生成退款候选清单"这种只读工具,让人工确认后再执行。
如果你要长期做 Agent 开发、频繁调用模型和工具,可以考虑 TaoToken 的 Coding Plan,适合持续编码和 Agent 场景:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=mcp_tool_server&utm_campaign=rewrite 。想先验证模型对话效果,用 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=mcp_tool_server&utm_campaign=rewrite 。Claude Code 相关接入参考 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=mcp_tool_server&utm_campaign=rewrite 。
最后留一个练习:选一个你手边的外部系统,列出三个适合暴露的能力,判断它们分别是 Tool、Resource 还是 Prompt,为其中一个写出输入 schema 和结构化错误格式。写完对照本文的order_lookup模板改一遍,你会发现大部分坑在动手前就能避开。