从零把91个工具做成MCP:npm发布与魔搭托管的踩坑记录

红狐数据

复盘一个零依赖 Node.js MCP Server 从开发到上线的完整过程:这套服务把红狐数据覆盖 9 大平台的 91 个工具打包成 38KB 的服务,并详细说明了技术选型、npm 发布时的 2FA 要求、魔搭托管的三条检测规则,以及 JSON-RPC id=0 被当作通知丢弃、协议版本回显等踩坑与修复,为 MCP 开发者提供可直接借鉴的避坑经验。

文章目录

一个零依赖的 Node.js MCP Server 从开发到上线的完整复盘:JSON-RPC 协议细节、魔搭托管部署检测规则,以及一个让我排查了整整半天的 id=0 玄学 Bug。

魔搭页面

背景:为什么要把新媒体数据做成 MCP

做新媒体相关工具的人应该都有同感:抖音热点、小红书爆款、公众号 10w+、微博热搜、B 站榜单……数据分散在七八个平台,每个平台一套 API,对接成本极高。

更麻烦的是,这些数据大多是实时变化的——大模型训练数据里根本没有。想给 AI 应用接上"实时数据能力",就得走 MCP(Model Context Protocol):让 AI 客户端通过标准协议调用外部数据服务。

我们的目标很直接:把红狐数据(redfox.hk)覆盖全平台的新媒体数据能力,打包成一个 MCP Server,让任何支持 MCP 的客户端(Qoder / Claude / Cursor 等)开箱即用。

最终交付:91 个工具,9 大平台,包体 38KB,零依赖。

一、技术选型:为什么用纯 Node.js

第一版方案考虑过 Python(FastMCP),最终选了纯 Node.js,理由很实际:

  1. 零依赖:Node 18+ 内置 fetch、AbortController,不需要任何第三方库,npm install 都省了;
  2. 包体小:38KB 的包,npx 秒下,魔搭这类托管平台的部署检测不容易超时;
  3. 路径友好:__dirname 相对路径引用资源,打包后行为一致。

架构很简单,三层:

text
MCP 客户端 ──JSON-RPC/stdio──> server.js ──> build-tools.js 工具注册表
                                                │
                                                ▼
                                    redfox-client.js(X-API-KEY 双发)
                                                │
                                                ▼
                                    https://redfox.hk/story/api/...

目录结构:

text
redfox-mcp/
├── server.js              # MCP stdio 服务器入口(JSON-RPC 2.0)
├── tools-manifest.json    # 工具清单(API 端点/参数自动提取)
├── src/
│   ├── redfox-client.js   # API 统一客户端(双请求头、错误处理、任务轮询)
│   ├── build-tools.js     # 工具构建器(合并 manifest + overrides)
│   └── overrides.js       # 人工校正层(精确参数映射、异步任务定义)

几个实现细节值得说:

① 双请求头兼容:仓库里不同脚本用的请求头不一样(X-API-KEY / REDFOX_API_KEY),统一双发,少踩很多坑:

text
const headers = {
  'Content-Type': 'application/json',
  'X-API-KEY': apiKey,
  'REDFOX_API_KEY': apiKey,
};

② 异步任务自动轮询:视频生成、评论拉取这类耗时任务,提交后拿 taskId,自动轮询直到结果就绪,对客户端完全透明:

text
const taskId = dig(submitResp, taskIdField) || dig(submitResp, 'data.taskId');
for (let i = 0; i < maxAttempts; i++) {
  await sleep(intervalMs);
  const r = await request({ url: resultUrl.replace('{taskId}', encodeURIComponent(taskId)), ... });
  if (status === 'success' || status === 'done') return { taskId, result: r };
}

③ 日志走 stderr:MCP stdio 协议下 stdout 只能输出 JSON-RPC 消息,任何 console.log 都会污染协议通道。所有启动日志一律 console.error。

二、发布 npm:被 2FA 卡住的发布流程

包打包没问题(files 白名单 + npm pack --dry-run 验证),真正卡住的是发布本身。

现在的 npm 强制策略:浏览器登录的 session 不能用于 publish,必须 2FA 或带 Bypass 2FA 的 Granular Token。要么:

  • 开启两步验证,npm publish 时输入 OTP;
  • 或者生成 Granular Token(勾选 Read and write + All packages + Bypass two-factor authentication)。

小提示:publish 前一定要先 npm pack --dry-run 检查 files 白名单,确认 src/、tools-manifest.json 等运行时文件都进去了。我们发布前用临时目录 npm install 后 spawn 实测过 tools/list,91 个工具全部返回。

三、托管魔搭:部署检测的三条硬规则

魔搭 MCP 广场支持托管部署,检测流程是:下载包 → 启动进程 → 调 tools/list,成功即通过。三条硬规则踩过才知道:

规则 1:command 只认 npx / uvx

写 sh 或其他命令直接拒。正确姿势:

text
{
  "mcpServers": {
    "redfox-mcp-server": {
      "command": "npx",
      "args": ["-y", "redfox-mcp-server@latest"],
      "env": { "REDFOX_API_KEY": "your-api-key-here" }
    }
  }
}

规则 2:JSON 不允许注释和尾逗号,env 值不能为空字符串

环境变量留空也会被拒,先填占位值,用户连接时再填自己的 Key。

规则 3:tools/list 不能依赖外部凭证

部署检测阶段只调 tools/list,不填 API Key。所以工具列表必须静态注册,不能在检测时去请求外部服务。

四、踩坑合集:那个让我排查半天的 Bug

坑 1:JSON-RPC id=0 被当成"通知"丢弃(最致命)

这是本文含金量最高的一个坑。

MCP 客户端的 initialize 请求 id 通常是 0。而服务器代码判断"是否为通知(无需响应)"时写的是:

text
if (!id) {
  return null; // 忽略
}

!0 === true,于是 id=0 的 initialize 请求被当成通知静默丢弃了。 客户端永远等不到握手响应 → 超时 → 部署检测失败。而本地测试"通过"是因为测试脚本只校验了 id=1 的 tools/list 响应,根本没检查 id=0 的 initialize 有没有回包——这个 Bug 被完美掩盖了。

修复:

text
// 通知类(无响应):仅当 id 为 undefined/null 时才是通知
if (id === undefined || id === null) {
  return null;
}

教训:JSON-RPC 的 id 可以是 0,判断通知必须用严格相等,不能用 falsy 判断。

坑 2:initialize 协议版本要回显

一开始写死 protocolVersion: '2024-11-05',但新版 MCP SDK 可能请求 2025-06-18 / 2025-03-26。稳妥做法是回显客户端请求的版本(在自己支持列表内):

text
const SUPPORTED_PROTOCOLS = ['2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07'];
const negotiated = SUPPORTED_PROTOCOLS.includes(requested) ? requested : PROTOCOL_VERSION;
return respond(id, { protocolVersion: negotiated, ... });

坑 3:本地测试通过 ≠ 平台检测通过

本地冒烟测试脚本一定要同时断言 initialize 和 tools/list 两条响应,只测其中一个很容易被"响应正常"的假象骗过去——坑 1 就是这么溜掉的。

五、上线效果

服务托管魔搭后,用户只需在服务页填自己的 API Key,拿到专属 Remote URL,填入任意 MCP 客户端即可使用全部 91 个工具:

text
https://mcp-<uuid>.api-inference.modelscope.cn/sse

能力覆盖(节选):

能力域工具
抖音(12)douyin-search, douyin-hot-trend, douyin-daily-hot, douyin-top-account, douyin-account-diagnosis
小红书(16)xiaohongshu-search, xiaohongshu-title-score, xiaohongshu-account-analyzer, xiaohongshu-dailytop
公众号(14)wechat-10w-hot, wechat-original-hot, wechat-search, wechat-title
B站/快手/微博bilibili-keywords-search, kuaishou-search, weibo-hot-search, 评论/下载类工具
多平台cn-last30days 近30天跨平台爆款聚合, trending-hub, stock-feed
AI 搜索deepseek-websearch, doubao-websearch, kimi-websearch

总结

  • 纯 Node.js 零依赖实现 MCP Server,38KB 包体,91 个工具覆盖 9 大平台;
  • 发布 npm 需 2FA 或带 Bypass 的 Granular Token;
  • 魔搭托管检测:command 只认 npx/uvx、JSON 无注释、tools/list 静态注册;
  • 最大坑:JSON-RPC id=0 的 falsy 判断陷阱,判断通知必须用 id === undefined || id === null。

资源:

如果这篇文章对你有帮助,欢迎点赞收藏;有 MCP 部署问题可以在评论区交流,一起避坑。