首页 / 资讯中心 / 文章详情

MCP协议开发实战:用Node.js SDK从零搭建AI Agent工具链并接入TaoToken

MCP协议开发实战:用Node.js SDK从零搭建AI Agent工具链并接入TaoToken ★ FEATURED ARTICLE
1. 为什么我要在 Node.js 里手搓一条 MCP 工具链MCP 协议Model Context Protocol是 Anthropic 提出的开放标准用来把 AI 模型和外部工具、数据源之间的通信方式统一起来。你可以把它理解成「AI 世界的 USB-C 接口」以前每接一个工具就要写一套私有适配层现在只要工具端实现 MCP Server客户端按 MCP 协议握手就能即插即用。它适合谁适合正在做 AI Agent、想让模型调用真实业务能力查数据库、调内部 API、跑脚本的 Node.js 开发者尤其是那些被「工具集散、协议不统一、扩展性差」折磨过的人。这篇不讲概念空转直接带你从零搭一条可运行的链路用modelcontextprotocol/sdk写一个 MCP Server注册工具、暴露资源、定义提示词模板再写一个 Agent 客户端完成协议握手与调用链编排最后把模型请求统一走 TaoToken 的 Key 接入。全程 Node.js命令和配置都能直接复制。我试过把这条链路跑通后再往上面加新工具基本就是「写一个文件、注册一次」的事扩展成本比传统 Agent 开发低很多。需要提前说明的是MCP 本身只负责「客户端与工具服务器怎么对话」它不绑定任何模型厂商。所以模型侧你可以自由选择本文用 TaoToken 作为统一入口一个 Key 就能切换不同模型省去多平台配置的麻烦。2. 前置准备Node 环境与 TaoToken 统一 Key2.1 环境要求与依赖安装Node.js 版本建议 18 以上SDK 用到较新的 ESM 与 fetch 能力我用的是 20 LTS。先建目录并初始化mkdir mcp-agent-chain cd mcp-agent-chain npm init -y npm install modelcontextprotocol/sdk zod dotenv三个依赖各司其职modelcontextprotocol/sdk是官方 Node SDKzod用来做工具参数的运行时校验dotenv负责把 Key 从环境变量读进来避免硬编码。2.2 拿到 TaoToken 的 API Key模型调用这一层我统一用 TaoToken 的 Key。它的好处是一个 Key 覆盖多种模型Agent 里切换模型不用改接入代码。操作路径很直接打开控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 登录后在 API Keys 页面创建一个新 Key复制保存。这个 Key 就是后面.env里的TAOTOKEN_API_KEY。注意Key 只显示一次创建后立刻存到密码管理器或本地.env不要提交到 Git。2.3 项目骨架最终目录结构如下先有个全局印象后面逐个文件填mcp-agent-chain/ ├── .env ├── package.json ├── servers/ │ └── weather-server.js ├── client/ │ └── agent-client.js └── config/ └── server-config.json.env内容TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_BASE_URLhttps://taotoken.net/api这里TAOTOKEN_BASE_URL指向 https://taotoken.net/api 注意 API 地址不带任何查询参数保持干净。3. 可复制配置写一个带工具、资源、提示词的 MCP Server3.1 用 SDK 初始化 Server 并注册工具MCP Server 的核心是「声明能力 响应请求」。下面这个weather-server.js注册了一个get_weather工具参数用 zod 校验返回结构化 JSON。为了让你能直接跑天气数据先用本地模拟真实项目里把fetchWeather换成第三方 API 即可。// servers/weather-server.js import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from modelcontextprotocol/sdk/types.js; import { z } from zod; import fs from node:fs/promises; import path from node:path; const server new Server( { name: weather-server, version: 1.0.0 }, { capabilities: { tools: {}, resources: {} } } ); // 工具参数 schema const WeatherArgs z.object({ city: z.string().describe(城市名称例如北京、上海), units: z.enum([metric, imperial]).default(metric), }); const MOCK { 北京: { temp: 22, condition: 晴朗, humidity: 45 }, 上海: { temp: 25, condition: 多云, humidity: 65 }, 广州: { temp: 28, condition: 阵雨, humidity: 80 }, }; async function fetchWeather({ city, units }) { const key Object.keys(MOCK).find((k) k city); if (!key) throw new Error(未找到城市 ${city} 的天气数据); const raw MOCK[key]; let temp raw.temp; let unit °C; if (units imperial) { temp Math.round((temp * 9) / 5 32); unit °F; } return { city: key, temperature: { value: temp, unit }, condition: raw.condition, humidity: ${raw.humidity}%, updated: new Date().toISOString(), }; } // 声明工具列表 server.setRequestHandler(ListToolsRequestSchema, async () ({ tools: [ { name: get_weather, description: 查询指定城市的当前天气情况, inputSchema: { type: object, properties: { city: { type: string, description: 城市名称 }, units: { type: string, enum: [metric, imperial], default: metric }, }, required: [city], }, }, ], })); // 处理工具调用 server.setRequestHandler(CallToolRequestSchema, async (req) { if (req.params.name ! get_weather) { throw new Error(未知工具: ${req.params.name}); } const args WeatherArgs.parse(req.params.arguments ?? {}); const result await fetchWeather(args); return { content: [{ type: text, text: JSON.stringify(result, null, 2) }] }; }); // 暴露一个只读资源服务器配置 server.setRequestHandler(ListResourcesRequestSchema, async () ({ resources: [ { uri: config://server, name: server-config, description: 服务器配置信息, mimeType: application/json, }, ], })); server.setRequestHandler(ReadResourceRequestSchema, async (req) { if (req.params.uri ! config://server) { throw new Error(资源不存在: ${req.params.uri}); } const cfgPath path.join(process.cwd(), config, server-config.json); const text await fs.readFile(cfgPath, utf-8); return { contents: [{ uri: req.params.uri, mimeType: application/json, text }] }; }); // 用 Stdio 传输启动 const transport new StdioServerTransport(); await server.connect(transport); console.error(weather-server 已通过 stdio 启动);几个关键点值得展开。capabilities里声明了tools和resources客户端握手时才知道这个 Server 能干什么。工具的参数校验交给 zodWeatherArgs.parse会在参数不合法时直接抛错避免脏数据流进业务逻辑。传输层用StdioServerTransport也就是标准输入输出这是本地开发最省事的方式客户端把 Server 当子进程拉起即可。3.2 配置文件与资源读取config/server-config.json放一些可被客户端读取的元信息{ server: { name: weather-server, version: 1.0.0 }, features: { enableCaching: true, maxConcurrentRequests: 10 }, logging: { level: info } }资源Resource和工具的区别在于工具是「可执行的动作」资源是「可读取的数据」。客户端可以先读配置了解服务能力再决定调哪个工具这种「先看说明书再动手」的模式在多服务器编排时特别有用。4. 验证请求写 Agent 客户端完成握手与调用链4.1 客户端连接 Server 并列出工具客户端负责拉起 Server 子进程、完成 MCP 握手、发现能力。下面agent-client.js先做连接与工具发现// client/agent-client.js import { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; import dotenv/config; const transport new StdioClientTransport({ command: node, args: [servers/weather-server.js], }); const client new Client( { name: agent-client, version: 1.0.0 }, { capabilities: {} } ); await client.connect(transport); console.log(MCP 握手完成); const { tools } await client.listTools(); console.log(可用工具:, tools.map((t) t.name)); const { resources } await client.listResources(); console.log(可用资源:, resources.map((r) r.uri));运行node client/agent-client.js如果看到「MCP 握手完成」和工具列表说明协议层已经通了。这一步是整个链路的地基握手失败后面全白搭。4.2 调用工具并读取资源在连接基础上追加调用逻辑// 读取服务器配置资源 const cfg await client.readResource({ uri: config://server }); console.log(服务器配置:, cfg.contents[0].text); // 调用天气工具 const weather await client.callTool({ name: get_weather, arguments: { city: 北京, units: metric }, }); console.log(天气结果:, weather.content[0].text);成功时你会看到类似输出天气结果: { city: 北京, temperature: { value: 22, unit: °C }, condition: 晴朗, humidity: 45%, updated: 2025-01-01T00:00:00.000Z }4.3 把工具结果交给模型接入 TaoToken工具返回的是结构化数据真正给用户看的自然语言报告还得模型来生成。这里用 TaoToken 的统一接口把工具结果拼进 promptasync function askModel(weatherJson) { 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-3-5-sonnet, messages: [ { role: user, content: 根据以下天气数据用一句话给出出行建议\n${weatherJson}, }, ], }), }); const data await res.json(); return data.choices?.[0]?.message?.content ?? 模型无返回; } const advice await askModel(weather.content[0].text); console.log(模型建议:, advice);到这里完整链路就跑通了客户端握手 → 发现工具 → 调用工具 → 结果喂给模型 → 输出建议。模型侧想换别的只改model字段Key 和地址都不用动这就是统一入口的价值。如果你更想先在网页里验证模型连通性可以直接用模型对话 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 快速试一条请求。5. 本篇常见错排查5.1 握手失败或进程直接退出最常见的原因是 Server 里用了console.log输出调试信息。Stdio 传输下标准输出是协议通道任何非协议内容都会污染消息流导致客户端解析失败。记住一条铁律Server 里所有日志走console.error把 stdout 留给协议。5.2 工具调用报参数校验错误如果报 zod 校验失败先检查客户端传的arguments字段名是否和inputSchema一致。MCP 不会帮你做字段名映射city写成City就会直接失败。另外units有默认值但如果你显式传了nullzod 的.default()不会兜底需要自己处理。5.3 模型请求 401 或 404401 通常是 Key 没读到检查.env是否被dotenv/config正确加载以及变量名拼写。404 多半是 base URL 写错正确地址是 https://taotoken.net/api 不要多加/v1之外的路径也不要在末尾带斜杠。请求路径本身是/v1/chat/completions两者拼起来才是完整端点。5.4 资源读取路径找不到readResource里用了process.cwd()它取决于你在哪个目录启动客户端。如果你在项目根目录跑node client/agent-client.jscwd就是根目录config/server-config.json能找到如果换了目录就会失败。稳妥做法是用import.meta.url推导绝对路径避免依赖启动位置。5.5 多服务器编排时的工具重名当你同时接入天气、日历、邮件多个 Server如果两个 Server 都注册了search工具客户端listTools会拿到重名项调用时无法区分。解决办法是在客户端维护「服务器名 工具名」的命名空间映射或者在 Server 侧给工具加前缀比如weather_get_weather。6. 继续往下走把链路变成长期可用的 Agent单次跑通只是起点。真正要长期用建议把这条链路沉淀成可复用的工程结构每个能力独立成一个 MCP Server客户端只负责发现与编排模型接入统一走 TaoToken。这样新增能力时你只需要写一个新的 Server 文件并注册客户端几乎不用改。如果你打算把 Agent 用在日常编码或长时间运行的任务上可以考虑 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 配合 MCP 工具链做代码检索、文件操作这类高频动作会更顺。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言 SDK 的完整参数说明遇到字段不确定时翻一下比猜快。Key 管理仍然在 API Keys https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 页面建议给不同项目建不同的 Key方便按项目排查用量。最后留一个我踩过的坑MCP Server 启动是异步的客户端connect之后不要立刻假设工具已就绪稳妥做法是listTools成功返回后再进入业务逻辑。这个顺序在本地看不出问题一旦 Server 启动慢比如要连远程数据库就会偶发失败加上这一步判断能省掉很多玄学 bug。
阅读完成 · 觉得有帮助?
咨询建站