资讯动态

三小时搞定AI工具开发:基于MCP的Node.js极简实践与TaoToken统一Key接入

发布时间:2026/10/9 18:32:14 来源:尧图企业网站定制
1. 从零搭一个 MCP 工具服务为什么值得花三小时MCP 全称 Model Context Protocol你可以把它理解成大模型世界的 USB-C 接口。以前每接一个外部能力比如查天气、读数据库、跑一段计算都要为不同模型写一套适配代码有了 MCP工具服务端只要按统一规范暴露能力Claude、GPT 甚至本地模型都能用同一套接口调用。对做 AI 工具开发的人来说这意味着你写一次工具就能被多个客户端复用。这篇面向的是想快速上手 MCP 的 Node.js 开发者你不需要先啃完协议文档也不用搭复杂基础设施只要会写 Express 路由、能跑 npm 命令就能在三小时内完成一个可被大模型调用的工具服务并接上统一 Key 通道做端到端联调。核心检索词就是 MCP、Node.js、AI 工具开发、极简实践。我试过的路径是这样的先跑通一个最小 MCP server让它暴露一个能返回股票价格的工具再用 curl 模拟模型侧的 JSON-RPC 调用最后把模型请求接到统一 API 通道上让真实对话触发工具调用。整个过程最耗时的不是写代码而是搞清 MCP 的消息格式和工具描述规范。下面把每一步拆开你跟着敲就行。先明确 MCP 和传统 Function Calling 的差别这决定了你后面怎么设计工具。Function Calling 是厂商私有协议工具声明通常硬编码在客户端MCP 是开放标准工具通过 discovery 端点动态发现消息基于 JSON-RPC 2.0还支持 SSE 流式进度。简单说Function Calling 像给某个品牌手机配专用充电线MCP 像通用 Type-C谁都能插。一个最小可用的 MCP 服务端需要两个端点一个是发现端点告诉客户端“我有哪些工具”另一个是调用端点接收 JSON-RPC 请求并执行对应工具。工具描述里要包含名称、功能说明、参数 schema模型靠这些信息判断该不该调用、传什么参数。参数 schema 用 JSON Schema 写和 OpenAI 的 tools 格式基本能互转。环境准备很轻Node.js 18 以上一个空目录npm 初始化后装 express 和 eventsource。如果你后面要接真实模型再装 openai 或对应 SDK。统一 Key 通道的作用在这里体现你不需要为每个模型单独申请和切换 KeyBase URL 指向同一个入口模型 ID 按需切换工具服务端保持不动。时间分配建议前 40 分钟写 server 和 discovery/invoke 两个端点40 分钟写客户端调用和 curl 验证40 分钟接统一 Key 做真实对话联调剩下时间排错和加流式。别一上来就追求完整协议先把“模型能发现工具并成功调用一次”跑通后面都是增量。2. TaoToken 前置统一 Key 与 Base URL 怎么配在写代码之前先把模型侧的通道准备好。TaoToken 在这里扮演的是统一 API 入口你拿到一个 Key把 Base URL 指向它的 API 地址就能用 OpenAI 兼容的方式请求不同模型。对 MCP 联调来说好处是你不用在客户端里维护多套鉴权逻辑工具服务端只管暴露能力模型请求统一走一个通道。第一步是拿 Key。打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台创建 API Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 只在创建时完整显示一次复制后存到环境变量里别写死在代码里。第二步是确认 Base URL。API 地址是 https://taotoken.net/api 注意这个地址不带 UTM 参数直接作为 OpenAI SDK 的 baseURL 使用。如果你用的是 OpenAI Node SDK配置大概是这样import OpenAI from openai; const client new OpenAI({ apiKey: process.env.TAOTOKEN_API_KEY, baseURL: https://taotoken.net/api });第三步是选模型 ID。不同模型在工具调用能力上表现不一样联调阶段建议选一个对 tools 支持稳定的模型。模型 ID 可以在模型对话页面里确认地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。你先把模型 ID 记下来后面写进请求参数。环境变量建议这样组织放在项目根目录的.env里TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_BASE_URLhttps://taotoken.net/api MCP_SERVER_URLhttp://localhost:3000 MODEL_ID你的模型ID如果你用的是 Claude Code 这类编码工具或者 Cline、Codex 这类客户端配置逻辑是一样的三件套Base URL、API Key、Model ID。以 Claude Code 为例它读取的是环境变量或 settings 文件你需要把ANTHROPIC_BASE_URL指向统一入口Key 填进去模型 ID 按客户端要求填。具体路径和字段名以客户端文档为准接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。这里有个容易踩的坑Base URL 末尾不要多加/v1或斜杠SDK 会自己拼接路径。如果你手动用 curl请求地址是https://taotoken.net/api/chat/completions别写成/api/v1/chat/completions。另一个坑是 Key 权限创建时如果选了受限范围可能只能调部分模型联调时先用全量权限的 Key跑通后再收紧。长期做编码或 Agent 的话可以考虑 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它更适合高频调用场景。但本文的联调阶段用普通 API Key 就够了先把链路跑通再考虑套餐。3. 可复制配置MCP server 与客户端 settings 片段现在进入代码部分。先初始化项目mkdir mcp-node-demo cd mcp-node-demo npm init -y npm install express eventsource openai dotenv然后在根目录建server.js。这个文件实现两个核心端点/mcp/discover返回工具列表/mcp/invoke执行工具调用。消息格式严格按 JSON-RPC 2.0 来jsonrpc字段必须是2.0请求带id响应原样带回。import express from express; import dotenv from dotenv; dotenv.config(); const app express(); app.use(express.json()); // 工具注册中心每个工具包含描述、参数 schema、执行逻辑 const tools { stock_price: { description: 获取指定股票的实时价格, parameters: { type: object, properties: { symbol: { type: string, description: 股票代码如 AAPL } }, required: [symbol] }, handler: async ({ symbol }) { // 这里用模拟数据真实场景替换为行情 API const price (Math.random() * 200 50).toFixed(2); return { symbol, price, currency: USD }; } }, add_numbers: { description: 计算两个数字之和, parameters: { type: object, properties: { a: { type: number }, b: { type: number } }, required: [a, b] }, handler: async ({ a, b }) ({ result: a b }) } }; // 发现端点客户端据此了解可用工具 app.post(/mcp/discover, (req, res) { res.json({ jsonrpc: 2.0, result: { protocol_version: 1.0, tools: Object.entries(tools).map(([name, def]) ({ name, description: def.description, parameters: def.parameters })) }, id: req.body.id ?? null }); }); // 调用端点method 格式为 tool.{name} app.post(/mcp/invoke, async (req, res) { const { jsonrpc, method, params, id } req.body; if (jsonrpc ! 2.0) { return res.status(400).json({ jsonrpc: 2.0, error: { code: -32600, message: Invalid Request }, id }); } const [, toolName] method.split(.); const tool tools[toolName]; if (!tool) { return res.json({ jsonrpc: 2.0, error: { code: -32601, message: Method not found }, id }); } try { const result await tool.handler(params); res.json({ jsonrpc: 2.0, result, id }); } catch (err) { res.json({ jsonrpc: 2.0, error: { code: -32603, message: err.message }, id }); } }); const PORT process.env.PORT || 3000; app.listen(PORT, () { console.log(MCP server running at http://localhost:${PORT}); });启动服务node server.js看到MCP server running at http://localhost:3000就说明服务起来了。接下来写客户端client.js它做三件事从 discovery 端点拉工具列表、转成模型能识别的 tools 格式、发起对话并在模型要求调用工具时执行 MCP 调用。import OpenAI from openai; import dotenv from dotenv; dotenv.config(); const client new OpenAI({ apiKey: process.env.TAOTOKEN_API_KEY, baseURL: process.env.TAOTOKEN_BASE_URL }); const MCP_SERVER process.env.MCP_SERVER_URL; // 把 MCP 工具描述转成 OpenAI tools 格式 function toOpenAITool(tool) { return { type: function, function: { name: tool.${tool.name}, description: tool.description, parameters: tool.parameters } }; } async function getMCPTools() { const res await fetch(${MCP_SERVER}/mcp/discover, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ jsonrpc: 2.0, method: discover, id: 1 }) }); const { result } await res.json(); return result.tools.map(toOpenAITool); } async function callMCPTool(method, params) { const res await fetch(${MCP_SERVER}/mcp/invoke, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ jsonrpc: 2.0, method, params, id: Math.floor(Math.random() * 10000) }) }); const data await res.json(); if (data.error) throw new Error(data.error.message); return data.result; } async function main() { const tools await getMCPTools(); const userMessage 帮我查一下 AAPL 的股价再算一下 12 加 30 等于多少; const first await client.chat.completions.create({ model: process.env.MODEL_ID, messages: [{ role: user, content: userMessage }], tools }); const msg first.choices[0].message; const toolCalls msg.tool_calls || []; if (toolCalls.length 0) { console.log(模型未调用工具, msg.content); return; } const toolResults []; for (const call of toolCalls) { const args JSON.parse(call.function.arguments); const result await callMCPTool(call.function.name, args); toolResults.push({ role: tool, tool_call_id: call.id, content: JSON.stringify(result) }); } const final await client.chat.completions.create({ model: process.env.MODEL_ID, messages: [ { role: user, content: userMessage }, msg, ...toolResults ] }); console.log(最终回答, final.choices[0].message.content); } main().catch(console.error);运行node client.js如果一切正常你会看到模型先要求调用tool.stock_price和tool.add_numbers客户端执行后把结果回传模型给出自然语言总结。这就是 MCP 端到端联调的最小闭环。如果你用的是 Cline 或 Claude Code 这类客户端配置片段要写全三件套。以 Cline 的 MCP 配置为例settings 里通常包含{ mcpServers: { local-tools: { url: http://localhost:3000/mcp, transport: http } } }而模型侧的统一 Key 配置在客户端的环境变量或设置里Base URL 填https://taotoken.net/apiKey 填你的 KeyModel ID 填你选的模型。三件套缺一不可少一个就会报鉴权或模型不存在。4. 验证请求用 curl 和日志确认工具调用成功代码跑通不代表链路稳定你需要能独立验证每一层。最直接的方式是用 curl 打 MCP server 的两个端点看返回是否符合 JSON-RPC 规范。先验证 discoverycurl -s -X POST http://localhost:3000/mcp/discover \ -H Content-Type: application/json \ -d {jsonrpc:2.0,method:discover,id:1} | jq预期返回里result.tools是一个数组每个元素有name、description、parameters。如果tools为空检查tools对象是否正确定义以及Object.entries是否被正确调用。再验证 invokecurl -s -X POST http://localhost:3000/mcp/invoke \ -H Content-Type: application/json \ -d {jsonrpc:2.0,method:tool.add_numbers,params:{a:12,b:30},id:2} | jq预期返回{ jsonrpc: 2.0, result: { result: 42 }, id: 2 }如果返回Method not found说明method里的工具名和tools对象的 key 不一致。注意method格式是tool.{name}split(.)后取第二段所以tool.add_numbers对应tools.add_numbers。验证模型侧通道直接用 curl 打统一 APIcurl -s https://taotoken.net/api/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: $MODEL_ID, messages: [{role:user,content:只回复两个字收到}] } | jq .choices[0].message.content如果这一步返回正常文本说明 Key 和 Base URL 没问题。如果报 401检查 Key 是否复制完整、是否有多余空格如果报模型不存在检查 Model ID 是否和平台一致。日志方面在 server 的 invoke 端点里加一行打印能帮你看清模型实际传了什么参数app.post(/mcp/invoke, async (req, res) { console.log([MCP invoke], JSON.stringify(req.body)); // ... 原有逻辑 });联调时观察终端输出你会看到模型发来的method和params。常见情况是模型把参数名写错比如把symbol写成stock这时工具 handler 解构出来是 undefined返回结果就不对。解决办法是在工具描述里把参数说明写清楚模型会按 schema 来。流式进度是 MCP 的一个加分项。对于耗时超过几秒的工具你可以加一个 SSE 端点让客户端实时看到进度。最小实现app.get(/mcp/stream/:taskId, (req, res) { res.setHeader(Content-Type, text/event-stream); res.setHeader(Cache-Control, no-cache); const { taskId } req.params; let progress 0; const timer setInterval(() { progress 20; res.write(data: ${JSON.stringify({ jsonrpc: 2.0, method: progress_update, params: { taskId, progress } })}\n\n); if (progress 100) { clearInterval(timer); res.write(data: ${JSON.stringify({ jsonrpc: 2.0, method: task_complete, params: { taskId, result: done } })}\n\n); res.end(); } }, 500); req.on(close, () clearInterval(timer)); });用 curl 验证 SSEcurl -N http://localhost:3000/mcp/stream/task-1你会看到每隔 500ms 推一条data:消息直到 100% 后连接关闭。这个能力在长任务场景里很有用模型侧可以边执行边给用户反馈。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth联调阶段最容易卡在几个固定报错上下面按真实错误信息对照排查。401 Unauthorized。这个通常出在模型侧请求。先确认Authorization头格式是Bearer sk-xxx中间有一个空格。再确认 Key 没有过期或被禁用。如果你用的是环境变量打印一下process.env.TAOTOKEN_API_KEY的前几位和后几位确认没有换行符或引号。还有一种情况是 Base URL 写错比如写成了带/v1的地址导致请求打到了不存在的路径有些网关会返回 401 而不是 404。local proxy failed。这个报错一般出现在客户端配置了本地代理但代理没启动或者代理地址填错。排查顺序先确认本地没有多余代理进程占用端口再检查客户端 settings 里的 proxy 字段是否为空或指向了不存在的地址最后确认 Base URL 是直连地址不需要额外代理。如果你在容器里跑检查容器网络是否能访问外网。reading choices of undefined。这是客户端代码里最常见的错误说明response.choices是 undefined。原因通常是请求失败但没检查错误直接取了choices。修复方式是在取choices前先判断const data await res.json(); if (!data.choices) { console.error(响应异常, JSON.stringify(data)); throw new Error(模型未返回 choices); }另一个原因是流式和非流式混用流式响应没有choices数组需要逐块解析delta。确认你的请求参数里stream是 false 还是 true两者解析方式不同。OAuth 相关报错。如果你在 Claude Code 或类似客户端里看到 OAuth 失败通常是因为客户端默认走 OAuth 流程而你用的是 API Key 模式。解决办法是在客户端配置里显式指定 API Key 认证把 Base URL 和 Key 填到对应字段关闭 OAuth 选项。具体字段名看客户端文档接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。工具调用返回空结果。模型要求调用工具但 handler 返回 undefined。检查 handler 是否 async 且 return 了值检查参数解构是否对得上 schema检查method.split(.)后工具名是否正确。加日志打印toolName和params最快定位。JSON-RPC id 不匹配。有些客户端会校验响应 id 和请求 id 一致。确保 invoke 端点把请求的id原样返回不要自己生成新 id。discovery 端点同理。端口占用。EADDRINUSE说明 3000 端口被占换端口或杀掉占用进程lsof -i :3000 kill -9 PID排障的核心思路是分层验证先用 curl 确认 MCP server 本身正常再用 curl 确认模型通道正常最后跑客户端看两层拼接。哪一层报错就查哪一层不要混在一起猜。6. 把工具接到真实场景下一步怎么走最小闭环跑通后你可以把模拟的stock_price换成真实数据源比如接一个行情 API在 handler 里发 HTTP 请求并返回结构化结果。工具描述里的description要写清楚用途和返回格式模型靠它判断何时调用。参数 schema 尽量用enum限制取值范围减少模型传错参数的概率。如果你要做多个工具的组合调用MCP 的 discovery 端点天然支持。模型会一次性看到所有工具按需选择。你可以在客户端里加一个循环处理多轮工具调用直到模型不再要求调用为止。注意设置最大轮数避免死循环。对于长期运行的 Agent 场景建议把工具服务独立部署客户端通过环境变量配置 MCP server 地址。统一 Key 通道的好处在这里更明显工具服务不需要关心模型鉴权模型请求统一走一个入口换模型只改 Model ID。需要更高调用频率的话Coding Plan 地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 适合编码和 Agent 类持续调用。最后提醒几个工程细节工具 handler 里做好超时和错误捕获别让一个工具挂掉整个服务参数校验用 JSON Schema 加一层手动检查模型偶尔会传多余字段日志里别打印完整 Key只打印前后几位用于排查。把这些做完你的 MCP 工具服务就能从 demo 走向可用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑