
1. 从 REST 到 MCPNode.js 服务为什么要改造如果你手里已经有一套跑得好好的 Node.js REST API比如用户管理、订单查询、商品目录这些接口现在想让 Claude、Cursor 或者自研的 AI 智能体直接调用它们最直接的想法可能是「让模型自己发 HTTP 请求不就行了」。我试过这条路结论是能跑但很脆。模型得靠提示词记住每个端点的路径、参数格式、鉴权头稍微换个字段名就报错而且没法动态发现你新增了哪些能力。MCPModel Context Protocol解决的正是这个问题。它是一套面向 AI 客户端的标准协议用 JSON-RPC 把「工具Tools」「资源Resources」「提示词Prompts」暴露出去AI 客户端启动时通过list_tools就能知道你的服务能干什么、需要什么参数。对 Node.js 开发者来说这意味着原有的业务逻辑不用重写只需要在它外面包一层 MCP 服务器把 REST 调用编排成语义化的工具。这篇文章聚焦的是工程落地路径用官方 TypeScript SDK 定义 tools 和 resources把现有路由映射成 MCP 能力同时用 TaoToken 统一管理模型调用的 Key 和 API 通道。TaoToken 在这里的角色是「模型侧的统一入口」——你的 MCP 服务器如果要调用大模型做推理、总结或意图解析不用在代码里散落各家厂商的 Key而是走一个统一的 Base URL 和 API Key。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。适合谁看已经有一套 Node.js REST 服务、想让它被 AI 客户端直接调用的后端工程师正在做 Agent 工具链、需要把内部系统暴露给模型的团队以及想理解 MCP 服务器到底怎么写的 TypeScript 开发者。下面从环境搭建开始一步步给出可复制的配置和代码。2. 前置准备TaoToken 统一 Key 与 Node.js MCP 环境在写 MCP 服务器之前先把两件事理清楚模型调用的鉴权怎么统一以及 Node.js 侧的依赖怎么装。2.1 为什么需要 TaoToken 统一 KeyMCP 服务器经常不只是「转发 REST 请求」它可能还要调用大模型做参数补全、结果摘要、或者把自然语言意图翻译成结构化参数。如果每个模型厂商都配一套 Key代码里会到处是process.env.OPENAI_KEY、process.env.ANTHROPIC_KEY这类分支换模型就得改代码。TaoToken 提供的是统一的 API 通道一个 Base URLhttps://taotoken.net/api加一个 API Key就能访问多种模型。对 MCP 服务器来说这意味着模型调用部分只需要维护一份配置。你可以在控制台创建 Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 创建完在 API Keys 页面复制即可页面在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。注意API Key 只放在服务端环境变量里绝对不要写进会被提交到 Git 的配置文件也不要暴露给前端。2.2 初始化 Node.js 项目假设你已有 Node.js 18 以上的环境。新建一个目录作为 MCP 层和原有 REST 服务分开部署这样互不影响。mkdir node-mcp-server cd node-mcp-server npm init -y npm install modelcontextprotocol/sdk zod node-fetch npm install -D typescript types/node ts-node这里几个依赖的分工modelcontextprotocol/sdk是官方 TypeScript SDK提供McpServer和传输层zod用来定义工具参数的 schemaSDK 内部也依赖它做校验node-fetch用于在工具处理器里调用你原有的 REST API。接着初始化 TypeScript 配置npx tsc --init把tsconfig.json里的target改成ES2022module改成NodeNextmoduleResolution改成NodeNextoutDir设为dist。这样 SDK 的 ESM 导入路径才能正确解析。2.3 配置环境变量在项目根目录建一个.env文件记得加进.gitignore把模型通道和原 REST 服务的地址都放进去# TaoToken 统一模型通道 TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的Key # 原有 Node.js REST API 地址 REST_API_URLhttp://localhost:3000 REST_API_TOKEN你的内部服务令牌然后在代码里用process.env读取。如果你不想引入dotenv可以在启动命令前手动 export或者用 Node 20 自带的--env-file参数。2.4 实例化 MCP 服务器骨架创建src/mcp-server.ts先把服务器实例和传输层搭起来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: MyNodeAPIServer, version: 1.0.0, capabilities: { tools: {}, resources: {}, prompts: {}, }, }); const transport new StdioServerTransport(); async function startServer() { // 工具和资源注册代码稍后加在这里 await server.connect(transport); console.error(MCP server running on stdio); } startServer().catch(console.error);这里用StdioServerTransport是为了本地测试方便AI 客户端通过标准输入输出和你的进程通信。如果要做远程部署换成StreamableHttpServerTransport把服务挂到一个 HTTP 端口上。注意日志用console.error输出因为console.log会污染 stdio 通道导致协议解析失败——这是新手最容易踩的坑之一。3. 可复制配置把 REST 路由映射为 MCP 工具与资源这一步是核心。不要把所有 REST 端点原样暴露而是按「AI 意图」重新组织成高层级工具。3.1 工具设计原则语义化而非原子化REST 习惯把操作拆得很细GET /users/{id}、PUT /users/{id}/email、POST /billing/{id}/plan。但 LLM 更擅长处理「管理用户订阅」这种带意图的操作。所以推荐把多个 REST 调用编排进一个 MCP 工具里。下面是一个完整的工具注册示例把「更新用户邮箱 修改订阅计划」合并成manage_subscriptionconst UpdateUserSchema z.object({ userId: z.string().describe(待更新用户的唯一 ID), newEmail: z.string().email().optional().describe(用户的新邮箱地址可选), newSubscriptionPlan: z .enum([basic, premium, pro]) .optional() .describe(新的订阅计划可选 basic/premium/pro), }); server.registerTool( manage_subscription, { title: 管理用户订阅及档案, description: 更新用户邮箱地址和/或修改其订阅计划需传入用户 ID。适用于客服或运营场景。, argsSchema: UpdateUserSchema, outputSchema: z.object({ status: z.string(), updatedFields: z.array(z.string()), }), }, async (args) { const { userId, newEmail, newSubscriptionPlan } args; const updatedFields: string[] []; const REST_API_BASE process.env.REST_API_URL; if (newEmail) { const res await fetch(${REST_API_BASE}/users/${userId}/email, { method: PUT, body: JSON.stringify({ email: newEmail }), headers: { Content-Type: application/json, Authorization: Bearer ${process.env.REST_API_TOKEN}, }, }); if (!res.ok) { return { status: Error, updatedFields: [email 更新失败: ${res.status}], }; } updatedFields.push(email); } if (newSubscriptionPlan) { const res await fetch(${REST_API_BASE}/billing/${userId}/plan, { method: POST, body: JSON.stringify({ plan: newSubscriptionPlan }), headers: { Content-Type: application/json, Authorization: Bearer ${process.env.REST_API_TOKEN}, }, }); if (!res.ok) { return { status: Error, updatedFields: [subscriptionPlan 更新失败: ${res.status}], }; } updatedFields.push(subscriptionPlan); } return { status: Success, updatedFields: updatedFields.length 0 ? updatedFields : [未执行任何修改], }; } );关键点description要写清楚「什么时候用这个工具」LLM 靠它判断是否调用argsSchema用 Zod 定义SDK 会自动生成 JSON Schema 给客户端返回值必须是结构化对象方便模型解析。3.2 用资源暴露只读数据对于GET /products/{id}这类只读接口用 Resource 更合适。资源不会被执行而是作为上下文提供给模型server.registerResource( product_catalog_item, { title: 产品目录项, description: 产品目录中的单个商品信息包含价格、库存和描述, uriTemplate: api://my-node-api-mcp/products/{productId}, dataSchema: z.object({ id: z.string(), name: z.string(), price: z.number(), description: z.string(), }), }, async (uri) { const productId uri.split(/).pop(); const response await fetch( ${process.env.REST_API_URL}/products/${productId}, { headers: { Authorization: Bearer ${process.env.REST_API_TOKEN}, }, } ); const data await response.json(); return { contents: [ { uri, mimeType: application/json, text: JSON.stringify(data), }, ], }; } );3.3 在工具里调用 TaoToken 模型通道如果你的工具需要模型参与比如把用户自然语言描述转成结构化参数可以走 TaoToken 的统一通道。下面是一个调用示例用fetch直接请求兼容接口async function callModel(prompt: string): Promisestring { const res await fetch(${process.env.TAOTOKEN_BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.TAOTOKEN_API_KEY}, }, body: JSON.stringify({ model: claude-sonnet-4-5, messages: [{ role: user, content: prompt }], max_tokens: 512, }), }); if (!res.ok) { throw new Error(模型调用失败: ${res.status}); } const data await res.json(); return data.choices[0].message.content; }这样模型调用和 REST 调用都通过环境变量管理换模型只改model字段不用动鉴权逻辑。模型 ID 可以在模型对话页面确认地址是 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。3.4 客户端接入配置以 Claude Code 为例MCP 服务器写好后需要在 AI 客户端里注册。以 Claude Code 为例配置文件通常放在~/.claude/settings.json或项目级.mcp.json格式如下{ mcpServers: { my-node-api: { command: node, args: [/absolute/path/to/dist/mcp-server.js], env: { TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_API_KEY: sk-你的Key, REST_API_URL: http://localhost:3000, REST_API_TOKEN: 你的内部服务令牌 } } } }三件套要写全Base URL 指向https://taotoken.net/apiKey 用控制台创建的Model ID 在工具内部指定。如果你用的是 Cline 或 Codex配置字段名可能不同但 Base URL、Key、Model ID 这三项是必须的。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。4. 验证请求curl 与客户端实测成功结果配置写完得验证它真的能被调用。分两步先用命令行确认 MCP 服务器能启动并响应再在 AI 客户端里实测。4.1 编译并启动服务器npx tsc node dist/mcp-server.js如果终端没有报错说明服务器已在 stdio 上等待连接。注意它不会输出「启动成功」到 stdout因为那会破坏协议日志走的是 stderr。4.2 用 curl 验证模型通道在接入 MCP 客户端之前先单独确认 TaoToken 通道是通的curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d { model: claude-sonnet-4-5, messages: [{role: user, content: 回复 OK 两个字母}], max_tokens: 16 }正常返回类似{ id: chatcmpl-xxx, choices: [ { index: 0, message: { role: assistant, content: OK }, finish_reason: stop } ] }如果这里就报 401说明 Key 有问题先解决再往下走。4.3 在 AI 客户端里实测工具调用把上面 §3.4 的配置写进客户端后重启客户端。以 Claude Code 为例输入/mcp可以看到已注册的服务器列表应该能看到my-node-api处于 connected 状态。然后直接用自然语言触发工具帮我把用户 u_10086 的邮箱改成 newexample.com订阅计划升到 pro客户端会先调用list_tools发现manage_subscription然后按 schema 生成参数并调用。成功时你会看到工具返回{ status: Success, updatedFields: [email, subscriptionPlan] }同时你原有的 REST 服务日志里应该出现两条请求PUT /users/u_10086/email和POST /billing/u_10086/plan。这就说明整条链路通了AI 客户端 → MCP 服务器 → 原 REST API。4.4 验证资源读取对于资源可以在客户端里问读取 api://my-node-api-mcp/products/p_001 的内容客户端会调用resources/read返回商品 JSON。如果返回的是空或报错检查uriTemplate的占位符和解析逻辑是否匹配。5. 本篇常见错排查401、local proxy failed 与 choices 读取失败实际部署时报错基本集中在几个地方。下面按真实错误信息对照排查。5.1 401 Unauthorized最常见。分两种来源一种是调用 TaoToken 时返回 401说明TAOTOKEN_API_KEY无效或没传。检查环境变量是否真的注入到了 MCP 进程里——很多客户端不会自动继承 shell 的.env需要在配置文件的env字段里显式写。另一种是调用你原有 REST API 时返回 401说明REST_API_TOKEN不对或者原服务用的是别的鉴权方式比如 Cookie、签名需要相应调整请求头。5.2 local proxy failed / connection refused这个报错通常出现在客户端启动 MCP 服务器时。原因可能是command路径不对比如写了node但客户端环境里没有 Node或者args里的脚本路径不是绝对路径。解决办法用which node确认 Node 绝对路径把command改成绝对路径args里的dist/mcp-server.js也改成绝对路径。另外确认npx tsc已经成功编译dist目录存在。5.3 reading choices of undefined这个错误发生在解析模型响应时。典型原因是请求体里的model字段写错了或者 Base URL 拼错导致请求打到了非预期地址返回的不是标准结构。检查两点TAOTOKEN_BASE_URL是否精确为https://taotoken.net/api不要多加/v1路径拼接时容易重复model字段是否是通道支持的模型 ID。可以在模型对话页面确认可用模型。5.4 OAuth / 鉴权头冲突如果你的原 REST API 用的是 OAuth而 MCP 服务器又自己加了一层Authorization可能出现头覆盖。排查方法在工具处理器里打印实际发出的请求头用console.error确认Authorization只有一个。如果原服务需要动态刷新 token建议在 MCP 层做一个 token 缓存而不是每次请求都重新获取。5.5 工具注册了但客户端看不到先确认capabilities里声明了tools: {}。然后检查客户端是否真的连上了——/mcp命令能看到服务器状态。如果服务器 connected 但工具列表为空多半是registerTool在server.connect之后才执行。确保所有注册代码都在connect之前完成。5.6 stdio 被日志污染前面提过console.log会破坏 stdio 协议。表现是客户端连上后立刻断开或者报 JSON 解析错误。全局搜索代码里的console.log全部换成console.error。第三方库如果往 stdout 打印也会导致同样问题必要时重定向process.stdout.write。6. 让原 REST 服务被 AI 直接调用下一步怎么走到这里你的 Node.js REST API 已经通过 MCP 层暴露给了 AI 客户端。回顾一下链路AI 客户端通过 stdio 连接 MCP 服务器服务器用 TypeScript SDK 注册了语义化工具和资源工具内部编排原有的 REST 调用模型调用则统一走 TaoToken 的 Base URL 和 Key。几个可以继续优化的方向。第一把工具按业务域拆分到多个文件用registerTool分别注册避免单文件过长。第二给工具加超时和重试REST 调用失败时返回结构化错误而不是抛异常这样模型能理解失败原因并决定是否重试。第三如果要做远程部署把StdioServerTransport换成 HTTP 传输配合反向代理和鉴权让多个客户端共享一个 MCP 服务。如果你还在选模型通道可以先在模型对话页面测几个模型的实际表现再决定工具里默认用哪个。长期做编码和 Agent 场景的话Coding Plan 页面有更细的通道说明。接入细节和字段定义以官方文档为准遇到配置问题优先对照文档里的示例。最后提醒一句MCP 服务器本质上是把内部能力暴露给 AI权限边界一定要收紧。工具能做什么、不能做什么在description和参数校验里写清楚敏感操作加二次确认别让模型直接触达生产库的写接口。