说真的,在没有集中整理之前,我完全没想到自己手边会凑出 91 个高频工具。它们的形态很乱:有 Python 小脚本、有命令行工具、有封装好的 REST 接口,甚至还有几段常年挂在笔记软件里的「复制即用」代码。每次想把这些能力交给 AI 客户端,我都得单独写一段工具描述、定义一套参数规则,再把结果格式折腾成模型能看懂的样子。91 个工具这么做下来,光是胶水代码就几千行,根本维护不动。
所以当我开始认真研究 MCP 协议,又因为要做成远程可调用的服务开始折腾 npm 发布和魔搭社区托管时,我知道这次必须把整套流程走通、走透。这篇文章就是这轮实践的完整记录:从工具怎么归类设计、MCP Server 怎么封装,到 npm 包发布时那些让人血压升高的报错,再到魔搭 Notebook 里跑服务遇到的保活问题,全部摊开讲。不管你是想把现有工具交给 AI 使用,还是单纯想了解 MCP 项目从本地到托管发布的完整链路,应该都能找到可以直接抄作业的部分。
1. 为什么要把 91 个工具统一做成 MCP
1.1 工具多到一定程度,接口混乱就是最大的成本
如果你只有三五个工具,那无所谓,每个工具单独接一遍也不费事。但工具到 91 个,真正的痛点已经不是「某个工具有没有 bug」,而是「这堆工具怎么被 AI 发现、怎么被统一调用」。
先说场景。我这边涉及的工作流很杂:有人要处理 JSON/XML/CSV 这类结构化数据,有人要查日期、做时间戳换算,有人要读文件、校验 URL 状态、跑正则测试,还有人要让我帮忙整理 Git 提交信息。以前这些能力分散在各处,AI 客户端想用它的时候,要么我重新描述,要么让模型自己猜参数格式,结果就是经常出现工具没被调用或者参数传错。
MCP 解决的是这个核心问题:它做了一层标准的「工具发现 + 工具调用」协议。客户端向 Server 发一条tools/list,就能拿到全部工具的名字、描述、参数结构;真正执行时发tools/call,Server 把结果以统一结构返回。这样 AI 就不再依赖开发者手写什么「使用说明」,它自己就能读懂每个工具怎么用。91 个工具接一遍 MCP,等于只接了一遍,后面所有支持 MCP 的客户端都能直接复用。
1.2 MCP 到底是什么:模型端口的「万能插座」
我一直觉得官方那个类比很到位:如果说大语言模型是一个只会聊天的大脑,那 MCP 就是给大脑配备的一套标准化接口。没有这个标准的时候,你想给模型接一个计算器,得单独造线;想接一个数据库,又得单独造一种线。但大家统一采用 MCP 之后,任何支持 MCP 的模型客户端,都能像 USB-C 一样直接插上任何 MCP Server。
在协议层面,你只需要关心三个角色:MCP Server 是提供工具的服务端,MCP Client 是宿主程序(比如 Claude Code、Codex、Cherry Studio 这类工具),两者之间走 JSON-RPC 2.0 消息。传输方式常见的就两种:本地用 stdio,也就是客户端直接拉起一个子进程;远程用 Streamable HTTP,通过网络访问。
这也解释了为什么最近「XX MCP」会突然变多:Figma、蓝湖、Unity、Cocos Creator 甚至一些安全测试工具都在做官方接入,因为它们都希望自己的功能能被 AI 直接调用。MCP 不限制你是什么生态的产品,只要实现一套协议,整个 AI 工具链都能成为你的入口。
1.3 MCP、Agent Skill、Computer Use 到底是什么关系
顺着这个话题,我把几个容易混的概念也理清一下。
插件和 Function Calling 属于「单机时代的方案」:每个模型厂商有自己的一套工具定义方式,你在 ChatGPT 里写的工具描述,拿到 Claude 里往往不通用。MCP 是跨厂商的中间协议,所有客户端用同一套对话方式。
Agent Skill 更靠近「流程编排」。一个 Skill 可以包含多步骤的提示词、内部工具处理逻辑,甚至能把多个 MCP 工具的调用串起来变成一个技能。所以更准确的理解是:MCP 是一个个能被调用的标准操作,Skill 是叠加在这些操作之上的使用策略。
Computer Use 则是另一个极端。它让模型像人一样看屏幕、动鼠标键盘,适合没有 API 的系统,但速度慢、误操作率高、也不容易审计。我这次做 91 个工具,优先考虑的是可靠和可追踪,所以全部走 MCP 的 API 式调用,让模型精确拿到结构化数据,而不是让它靠截图去猜。
2. 整体设计:如何给 91 个工具做一张统一的「工具清单」
2.1 先给 91 个工具分类:没有边界的工具集跑不了多远
动手封装前,我先把所有工具摊在桌面上分了一次类。这不是为了好看,而是为了后续几件事:哪些工具要默认加载、哪些工具要加安全确认、哪些模块适合单独拆包,都是以分类为基础的。
我最终的分类结构大概是这样的:
| 分类 | 数量 | 代表性工具 |
|---|---|---|
| 文件与编码相关 | 16 | 文件读取、路径解析、base64 编码、哈希计算 |
| HTTP 与网络请求 | 12 | 请求 JSON 接口、URL 状态检测、响应头查看 |
| 结构化数据转换 | 20 | JSON/XML/CSV/YAML 互转、格式校验、字段提取 |
| 时间与日历 | 8 | 时间戳换算、时区换算、日期计算 |
| 开发辅助与 Git 信息 | 15 | Git 状态、提交信息规范、语义化版本比较 |
| 系统环境与进程查询 | 10 | CPU/内存占用、端口检测、进程列表 |
| 文本生成与格式化 | 10 | 正则测试、文本截断、diff 生成、代码格式化 |
91 这个数字看起来吓人,但每个工具落到实处,平均就是几十行业务代码。真正的难度在于维护边界:你不能让一个「文本截断工具」顺手去删文件,也不能让一个「读取文件工具」变成任意文件读取漏洞。分类就是第一道边界,后面安全策略也跟着分类走。
2.2 用统一基类把 91 个工具的注册代码压到最少
规划完分类后,我没有给每个工具单独写一套 MCP 注册逻辑,而是把每个工具定义成一个结构化的ToolDefinition对象,让注册器统一处理。一个工具的核心信息包括:名字、描述、参数 Schema、执行函数。这样 91 个工具就是数组中 91 个对象,注册器遍历数组后自动在 MCP Server 上注册。
我给个简化示意,配合现在官方的@modelcontextprotocol/sdk写:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { randomUUID } from "node:crypto"; export interface ToolDefinition { name: string; title: string; description: string; inputSchema: { type: "object"; properties: Record<string, unknown>; required?: string[]; }; handler: (args: any) => Promise<{ ok: boolean; data?: unknown; error?: string }>; } const tools: ToolDefinition[] = [ { name: "uuid_generate", title: "生成 UUID", description: "生成一个随机 UUID v4 字符串。当用户需要唯一标识时调用。", inputSchema: { type: "object", properties: {}, required: [], }, handler: async () => { return { ok: true, data: randomUUID() }; }, }, // 这里实际一共有 91 个工具定义,按分类放在不同目录导入 ]; const server = new McpServer({ name: "mcp-91-tools", version: "1.4.0" }); for (const tool of tools) { server.registerTool(tool.name, { title: tool.title, description: tool.description, inputSchema: tool.inputSchema, }, async (args) => tool.handler(args)); }采用这种结构,新增一个工具时,我只需要关注业务函数本身,不用关心 MCP 注册细节。后面做发布前检测也方便:遍历数组就能自动检查有没有重复名字、有没有空描述、Schema 里是不是每个必填参数都做了说明。
2.3 工具描述和参数 Schema 的写作规范
在 MCP 场景下,工具描述不是写给用户看的说明书,而是写给模型看的「路由指标」。我从这轮踩坑中学到的经验是:描述里不能只写功能,更要写清楚「什么时候该调用它」。
举个例子,与其写「把 JSON 转成 YAML」,不如写:「当用户提供 JSON 内容并要求转换为 YAML 格式时调用。输入必须是合法 JSON 字符串,输出为缩进 2 空格的 YAML。」模型在判断调用哪个工具时,靠的就是这段话和用户请求的语义匹配度。描述写得精确一点,误调用的概率会明显下降。
参数 Schema 我坚持一个原则:能少就给少。一个工具超过五个参数,模型就很容易漏传或传错。那些可选的参数全给默认值,实在没法给默认值的,就把可选范围写死在描述里。比如端口号,你可以写清楚有效范围是 1~65535;比如编码格式,直接列出 UTF-8/GBK/UTF-16 供模型选择,别让它自由发挥。
2.4 安全开关与工具分级
这是 91 个工具里最容易被低估的部分。工具接上 MCP 后,模型在用户授权下是可能主动调用工具的,所以「只读工具」和「危险操作工具」必须分开。
我这里的做法是把工具分为两类:默认加载的绝大多数是只读或低风险工具,比如读文件、查时间、做格式转换;另一类涉及写文件、执行命令或者删除资源的工具,默认不会注册进 MCP Server,需要显式设置环境变量MCP_ENABLE_DANGEROUS=1才会加载。启动命令就是:
MCP_ENABLE_DANGEROUS=1 npx -y mcp-91-tools这个开关很重要。你平时在本地用,关闭危险工具省心;真要把服务暴露到公网或者放到共享环境,至少能保证模型默认情况下没有删除类权限。别嫌麻烦,这比事后补救便宜得多。
3. npm 发布全过程与环节排错
3.1 先确定分发粒度:一个主包还是按域拆包
91 个工具的包怎么发布,我犹豫过一阵。方案一是一个主包全部塞进去,简单粗暴;方案二是按分类拆成 7 到 10 个小包,用户按需安装。
拆包的好处是用户只需要装自己用得到的模块,版本管理和包体体积都更可控。但对 91 个工具来说,拆包的代价很高:包与包之间要共享底层代码,用户配置客户端时要写 7 个启动命令,版本同步也容易乱。我最终的结论是先用一个主包发布,内部按目录组织,等工具数量超过 200 或出现明显的领域隔离需求再拆。给客户端配置时,入口统一是:
npx -y mcp-91-tools这样用户在 Claude Desktop、Cherry Studio、Codex 这些客户端里只需要写一次启动命令就行,体验是最好的。
3.2 package.json 与构建产物配置
npm 包能不能被别人顺利安装使用,package.json 里的字段决定了一大半。我这次发布前重点检查了几个字段:name、version、type、bin、exports、files、engines、publishConfig。
基础配置我给个参考:
{ "name": "mcp-91-tools", "version": "1.0.0", "type": "module", "bin": { "mcp-91-tools": "./dist/cli.js" }, "exports": { ".": { "import": "./dist/index.js", "require": "./dist/index.cjs" } }, "files": [ "dist", "README.md", "LICENSE" ], "engines": { "node": ">=18" }, "publishConfig": { "access": "public" } }这里最容易踩坑的是两点。
第一,files字段一定要配。不配的话,npm 会把项目里几乎所有文件都打进包里,连node_modules里那些依赖也可能被卷进去,包体直接变大几十倍,而且可能泄露一些不该公开的文件。
第二,type: "module"配合exports时,如果同时想兼容 ESM 和 CommonJS,需要构建时打出两套产物。我用 tsup 配置了双格式输出,确保import和require都有对应文件。以前我只发 ESM 版本,结果遇到老项目用require加载时直接报ERR_REQUIRE_ESM,折腾了很久才补上 CJS 产物。
3.3 发布前一定先做本地模拟安装
我这次的教训是:npm publish之前,别只在项目目录里跑一遍就以为万事大吉。最可靠的方式是先执行npm pack,生成本地 tgz 包,然后在另外一个全新目录里安装这个 tgz,模拟用户从 npm 拉包的完整效果。
具体的验证流程我建议这么做:
- 先
npm run build,检查构建产物有没有生成完整。 - 跑
npm pack --dry-run,看打包清单里有没有多余文件或漏掉的目录。 - 正式
npm pack,得到类似mcp-91-tools-1.0.0.tgz的文件。 - 新建一个空目录,执行
npm init -y后安装本地 tgz:npm install /路径/mcp-91-tools-1.0.0.tgz。 - 在这个新目录里执行
npx mcp-91-tools --help,看能不能正常启动。 - 用测试客户端连接本地启动的 Server,执行一次 initialize、一次 tools/list,确认能看到完整工具列表。
这套流程走完,基本能排除 90% 的「在我电脑上是好的」问题。很多 npm 包发布后别人装不上,正是因为作者只在本项目里测过,本地依赖和运行时路径早就被 node_modules 掩盖了。
3.4 npm 环境高频报错与解决记录
如果你也常折腾 npm,下面这几个报错应该都不陌生。
第一个是 Windows PowerShell 下的执行策略问题。错误提示会出现「无法加载文件...npm.ps1,因为在此系统上禁止运行脚本」这类内容。原因是 PowerShell 默认执行策略是 Restricted,不允许直接跑 .ps1 脚本。解决办法是在 PowerShell 里执行:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser修改后会允许运行本地脚本,远程下载的未签名脚本仍然受限,比直接设成 Unrestricted 要安全一些。如果只想临时用,也可以切到 CMD 里执行 npm,CMD 不会走 PowerShell 执行策略。
第二个是「npm 不是内部或外部命令」。这基本上就是 Node.js 安装时没有把安装目录加入系统的 PATH,或者你改过环境变量后没有重新打开终端。Windows 下检查路径是否包含C:\Program Files\nodejs\,配置好后重启终端再试。
第三个是证书过期类报错,比如:
npm ERR! code CERT_HAS_EXPIRED npm ERR! request to https://registry.npm.taobao.org/... failed, reason: certificate has expired这个报错典型场景是 registry 还指向老的淘宝镜像源,那个源的证书已经失效。新版淘宝源地址已经变成了https://registry.npmmirror.com,执行下面的命令改回来就好:
npm config set registry https://registry.npmmirror.com如果不想用国内源,也可以直接切回官方源:
npm config set registry https://registry.npmjs.org/第四个是EUNSUPPORTEDPROTOCOL。这个报错通常是因为 registry 配置成了git://、git+ssh://这类非 HTTP(S) 协议,npm 不认。检查.npmrc里的 registry 值,确保是完整的https://地址。
第五个是npm WARN deprecated和npm WARN using --force recommended protections disabled。前者只是提示某个依赖包已经废弃,升级或替换即可;后者表示你用了--force强行安装,跳过了 npm 的一些保护检查,不建议作为常规操作。
3.5 发布后立刻用真实客户端实测
包发到 npm 之后,真正的测试才刚开始。我这边用了