资讯动态

自己写一个智能体-使用MCP服务:从零搭建可调试的本地工具链

发布时间:2026/10/1 19:57:31 来源:尧图企业网站定制
1. 为什么你的智能体需要 MCP 服务很多人第一次写智能体都会卡在同一个地方模型能聊天但干不了活。你问它“帮我读一下本地这个 CSV 文件统计一下每列缺失值”它会很礼貌地告诉你它做不到因为它没有手。智能体 大语言模型大脑 规划前额叶 工具手脚。这个公式里最容易被低估的就是“手脚”这一环。大脑再聪明没有工具调用能力它就只能停留在对话框里。而 MCPModel Context Protocol服务就是给智能体装手脚的那套标准接口。MCP 是什么你可以把它理解成 AI 时代的 USB-C。以前你要让模型读文件、查数据库、调内部 API得针对每个框架写一套胶水代码。LangChain 有 LangChain 的写法换个框架全得重写。MCP 把这个过程标准化了MCP Server 负责提供工具MCP Client 就是你的智能体LLM 负责决策调用哪个工具。三方只要遵守同一套协议就能即插即用。这篇文章面向的是想动手实现 Agent 工具调用的开发者。我会带你从零搭一条可调试的本地工具链写一个最小的 MCP 服务端注册一个真实可用的工具再写一个 MCP 客户端去连接它最后用一次真实调用验证整条链路是否打通。全程本地运行不需要任何外部服务你跟着敲就能跑起来。适合谁看如果你已经会用 OpenAI 兼容接口做对话但还没让模型真正“动手”过这篇就是你的第一步。如果你已经在用 Cline、Claude Code 这类工具想搞清楚它们背后的 MCP 机制这篇也能帮你把黑盒拆开。我试过把 MCP 服务端和客户端分开调试踩过的坑主要集中在传输层和工具 schema 上。下面我会把这些坑都标出来让你少走弯路。2. 前置准备TaoToken 接入与本地环境在写 MCP 服务之前得先解决模型调用的问题。MCP 负责工具能力但决策调用哪个工具的还是 LLM。所以你需要一个能稳定调用的模型接口。这里我用 TaoToken 作为模型接入层。它的 API 兼容 OpenAI 格式Base URL 是https://taotoken.net/api你拿到的 Key 直接填进去就能用。对于智能体开发来说这种兼容性很重要因为 MCP 客户端的代码里通常已经用了 OpenAI SDK换 Base URL 比换 SDK 省事得多。先拿 Key。打开https://taotoken.net/api-keys创建一个 API Key复制保存。注意这个 Key 只在创建时显示一次丢了就得重新建。然后确认本地环境。你需要Node.js 18 以上因为 MCP 官方 SDK 用了较新的 ESM 和 fetch。用node -v检查低于 18 就先升级。一个能跑 npm 的终端。Windows 用户建议用 PowerShell 或 WSLcmd 有时候对 stdio 传输支持不好。如果你打算用 Docker 跑 MCP 服务还需要 Docker Desktop。不过这篇教程里我会先写一个纯 Node.js 的 MCP 服务端不依赖 Docker降低起步门槛。环境变量方面建议建一个.env文件把模型 Key 和 Base URL 放进去TAOTOKEN_API_KEYsk-你的key TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_MODELgpt-4o-mini模型 ID 你可以根据自己账号里可用的模型来填。TaoToken 的模型对话页面https://taotoken.net/models能看到当前支持的模型列表选一个支持 Function Calling 的就行。MCP 的工具调用本质上就是 Function Calling所以模型必须支持 tools 参数。为什么不用本地模型因为本地小模型对 Function Calling 的支持参差不齐调试 MCP 链路时你会分不清是模型不会调工具还是 MCP 传输有问题。先用一个稳定的云端模型把链路跑通再换模型做对比这是更省时间的做法。如果你后续要做长期编码或 Agent 项目可以考虑 TaoToken 的 Coding Plan它在高频调用场景下更划算。入口在https://taotoken.net/coding-plan。不过这篇教程用按量计费的 API Key 就够了一次调试消耗的 token 很少。3. 可复制配置写一个最小的 MCP 服务端现在进入核心部分。我们要写一个 MCP 服务端注册一个真实可用的工具。为了让你能立刻验证我选一个不依赖外部服务的工具读取本地文件并返回内容摘要。先建项目目录mkdir mcp-local-demo cd mcp-local-demo npm init -y npm install modelcontextprotocol/sdk zodmodelcontextprotocol/sdk是官方 SDKzod用来定义工具的输入 schema。MCP 的工具定义需要 JSON Schemazod 能帮你自动生成。创建server.jsimport { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { z } from zod; import fs from fs/promises; import path from path; const server new McpServer({ name: local-file-tools, version: 1.0.0, }); server.tool( read_file_summary, 读取指定路径的文本文件返回行数、字符数和前 200 个字符, { filePath: z.string().describe(要读取的文件绝对路径), }, async ({ filePath }) { try { const content await fs.readFile(filePath, utf-8); const lines content.split(\n).length; const chars content.length; const preview content.slice(0, 200); return { content: [ { type: text, text: 文件: ${path.basename(filePath)}\n行数: ${lines}\n字符数: ${chars}\n预览:\n${preview}, }, ], }; } catch (err) { return { content: [ { type: text, text: 读取失败: ${err.message}, }, ], isError: true, }; } } ); const transport new StdioServerTransport(); await server.connect(transport); console.error(MCP server 已启动等待客户端连接...);这段代码做了三件事创建 MCP 服务实例、注册一个叫read_file_summary的工具、通过 stdio 传输启动服务。注意console.error而不是console.log。因为 stdio 传输用 stdout 传协议消息你往 stdout 打日志会污染协议流导致客户端解析失败。这是新手最容易踩的坑之一。工具注册的 schema 用 zod 定义SDK 会自动转成 MCP 需要的 JSON Schema。describe里的文字会作为工具描述传给 LLM写得越清楚模型越知道什么时候该调这个工具。现在建一个测试文件test.txtecho 第一行内容 第二行内容 第三行内容 test.txt服务端写好了。但 MCP 服务不能直接node server.js跑因为它等的是 stdio 连接你直接跑会看到它挂在那里。下一步我们写客户端来连它。4. 验证请求写 MCP 客户端并跑通一次真实调用客户端要做的事启动 MCP 服务端进程、建立 stdio 连接、获取工具列表、把工具转成 OpenAI 格式、让模型决策、执行工具调用、把结果回传给模型。创建client.jsimport { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; import OpenAI from openai; import dotenv/config; const transport new StdioClientTransport({ command: node, args: [server.js], }); const client new Client( { name: local-agent, version: 1.0.0 }, { capabilities: {} } ); await client.connect(transport); console.log(已连接到 MCP 服务端); const mcpResponse await client.listTools(); console.log(可用工具:, mcpResponse.tools.map((t) t.name)); const openaiTools mcpResponse.tools.map((tool) ({ type: function, function: { name: tool.name, description: tool.description, parameters: tool.inputSchema, }, })); const llmClient new OpenAI({ apiKey: process.env.TAOTOKEN_API_KEY, baseURL: process.env.TAOTOKEN_BASE_URL, }); const userPrompt { role: user, content: 帮我读取 test.txt 这个文件告诉我它有多少行。, }; const response await llmClient.chat.completions.create({ model: process.env.TAOTOKEN_MODEL, messages: [userPrompt], tools: openaiTools, tool_choice: auto, }); const assistantMessage response.choices[0].message; if (assistantMessage.tool_calls) { for (const toolCall of assistantMessage.tool_calls) { const name toolCall.function.name; const args JSON.parse(toolCall.function.arguments); console.log(模型请求调用工具:, name, args); const result await client.callTool({ name: name, arguments: args, }); const toolContent result.content .map((item) item.text) .join(\n); const finalMessages [ userPrompt, assistantMessage, { role: tool, tool_call_id: toolCall.id, content: toolContent, }, ]; const finalResponse await llmClient.chat.completions.create({ model: process.env.TAOTOKEN_MODEL, messages: finalMessages, }); console.log(最终回复:, finalResponse.choices[0].message.content); } } else { console.log(模型没有调用工具直接回复:, assistantMessage.content); } await client.close();跑之前确认.env文件在项目根目录并且装了dotenvnpm install dotenv openai然后执行node client.js正常的话你会看到类似输出已连接到 MCP 服务端 可用工具: [ read_file_summary ] 模型请求调用工具: read_file_summary { filePath: /你的路径/test.txt } 最终回复: test.txt 共有 3 行内容...到这里整条链路就打通了客户端启动服务端进程 → 获取工具列表 → 模型决策 → 客户端通过 MCP 调用工具 → 结果回传模型 → 模型生成最终回复。注意filePath参数。模型可能会传相对路径而你的工具实现里用的是fs.readFile相对路径会相对于服务端进程的工作目录。如果客户端和服务端不在同一目录就会读不到文件。稳妥的做法是在工具实现里把相对路径转成绝对路径或者让模型传绝对路径。这也是实际调试中很常见的一个坑。5. 常见报错排查401、local proxy failed、reading choices链路跑通不代表以后不出问题。下面这几个报错是我在调试 MCP 工具链时真实遇到过的按出现频率排序。401 Unauthorized这个通常不是 MCP 的问题而是模型接口的 Key 或 Base URL 配错了。检查.env里的TAOTOKEN_API_KEY有没有多余空格TAOTOKEN_BASE_URL是不是https://taotoken.net/api。注意 Base URL 末尾不要加/v1OpenAI SDK 会自己拼路径加了会变成/api/v1/chat/completions有些兼容层不认。如果你用的是其他兼容接口确认它支持tools参数。不支持 Function Calling 的模型会直接忽略 tools然后返回一段普通文本你会看到assistantMessage.tool_calls是 undefined。local proxy failed / connection refused这个报错一般出现在客户端启动服务端进程时。StdioClientTransport的command和args必须能正确执行。如果你写的是command: node但系统 PATH 里找不到 node就会启动失败。Windows 上有时需要写node.exe的完整路径。另一个原因是服务端脚本里有语法错误进程启动后立刻退出。这时候客户端会报连接断开。排查方法是先在终端单独跑node server.js看有没有报错。虽然它会挂起等待 stdio但至少能确认脚本本身没问题。reading choices这个报错说明response.choices是 undefined通常是模型接口返回了错误结构。常见原因有三个Key 无效、模型 ID 不存在、请求体格式不对。先打印完整的response看看返回了什么。如果返回的是{ error: { message: ... } }那就是接口层的问题跟 MCP 无关。还有一种情况是你把tool_choice设成了required但模型不支持强制工具调用接口会直接报错。调试阶段用auto最稳。工具被调用但参数解析失败JSON.parse(toolCall.function.arguments)抛异常说明模型返回的 arguments 不是合法 JSON。这在模型能力较弱时会出现。解决办法是在工具 schema 里把参数描述写得更明确或者换一个 Function Calling 能力更强的模型。也可以在解析前加一层 try-catch把原始字符串打出来看。工具执行成功但模型不总结有时候工具返回了结果但模型第二轮回复是空的。检查finalMessages里role: tool的消息有没有带tool_call_id这个字段必须和assistantMessage.tool_calls里的 id 一致。少了它接口会认为消息序列不合法。如果你用的是 Claude Code 或 Cline 这类工具它们内部已经处理了这些细节。但自己写客户端时这些字段一个都不能少。想省事的话可以直接用 TaoToken 的模型对话页面先验证模型是否支持工具调用再回到代码里调试。6. 把工具链接到真实场景下一步怎么走现在你已经有了一个能跑的最小 MCP 工具链。服务端注册了一个读文件的工具客户端能连接、获取工具、让模型决策并执行。这套骨架可以直接扩展成更复杂的智能体。扩展方向有几个。第一往服务端加更多工具。比如加一个write_file工具让模型能写文件加一个list_dir工具让它能浏览目录。每加一个工具就是在给智能体多装一只手。工具之间可以组合模型会自己规划先调哪个再调哪个。第二把 stdio 传输换成 SSE 或 HTTP。stdio 适合本地进程间通信但如果你想让多个客户端共享同一个 MCP 服务或者服务端跑在另一台机器上就需要网络传输。MCP 协议本身支持多种传输方式SDK 里也有对应的实现。第三接入现成的 MCP 服务。社区里已经有很多封装好的 MCP Server比如文件系统、数据库、浏览器操作等。你的客户端代码几乎不用改只要把StdioClientTransport的启动命令换成对应的服务就行。这就是 MCP 作为标准协议的价值工具的实现和智能体的逻辑解耦了。如果你要做长期编码或 Agent 项目建议把模型调用层独立出来用环境变量管理 Key 和 Base URL。TaoToken 的 API 兼容 OpenAI 格式所以你的代码里不需要任何特殊适配换 Base URL 就能切换模型。接入文档在https://taotoken.net/doc里面有各语言的调用示例。最后提醒一点MCP 工具的执行权限要控制好。读文件、写文件、执行命令这类工具一旦被模型误调用后果可能很严重。生产环境里建议加一层权限校验或者把危险工具放在沙箱里执行。本地调试阶段可以先放开但心里要有这根弦。整条链路的核心就一句话模型负责决策MCP 负责能力你的客户端负责连接。把这三者拆开调试哪里出问题就查哪里比一上来就写一个大而全的智能体要高效得多。

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

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

免费获取报价 →
↑