从 CLI 到 MCP:Node.js 工具包改造与 ESLint 规范实战

61 阅读3分钟

前言

随着 AI Agent 的流行,Model Context Protocol (MCP) 成为连接 LLM 与本地工具的桥梁。但在将现有的 Node.js CLI 工具改造为 MCP Server 的过程中,开发者往往会遇到两个极端问题:一是工具分发困难(npx 跑不动),二是日志输出干扰协议导致通信崩溃。

本文记录了我在改造 @xxx/cli 过程中的避坑指南,涵盖分发、规范约束及 MCP 场景适配。


一、 让你的 CLI 随时随地可运行

在 Monorepo 场景下,我们希望用户不创建项目也能通过 npx @xxx/cli 直接调用。如果发现调用失败,请检查以下三点:

  1. Bin 字段的玄学:在 package.json 中,确保 bin 的键名与包名后缀一致。例如包名是 @xxx/clibin 设置为 "cli": "index.js",这样 npx 的识别率最高。
  2. Shebang 必不可少:入口文件第一行必须是 #!/usr/bin/env node
  3. 无环境执行:若在非 npm 项目下运行,确保包已发布且非私有。若是私有包,请确保环境中有 NPM_TOKEN

二、 严格禁绝 Console:基于 AST 的 ESLint 进阶

在 MCP 模式下,标准输出 (STDOUT) 是神圣不可侵犯的。MCP 依靠 STDOUT 进行 JSON-RPC 通信,任何一行 console.log 都会导致协议解析失败。

  1. 为什么 no-console: error 还不够?

原生的 no-console 报错信息太死板。在团队协作中,我们希望报错的同时告诉成员:“请使用项目封装的 log 方法,它会自动处理 MCP 静默策略”。

  1. 实战:定制化 AST 校验规则

在 Monorepo 的 Node.js 项目中,由于插件冲突(如 Alloy 等),普通的规则常被覆盖。建议使用 overrides 并配合 no-restricted-syntax

javascript

// .eslintrc.js
module.exports = {
  overrides: [
    {
      files: ['packages/*/src/**/*.{ts,tsx,js,jsx}'],
      rules: {
        // 1. 关闭原生规则
        "no-console": "off",
        // 2. 利用 AST 选择器精准拦截
        "no-restricted-syntax": [
          "error",
          {
            // 匹配所有对 console 对象的成员访问(log, warn, error等)
            "selector": "MemberExpression[object.name='console']",
            "message": "❌ [MCP规范] 禁止直接调用 console。请使用 logger.log(),它在 MCP 模式下会自动静默,防止破坏通信协议。"
          }
        ]
      }
    }
  ]
}

请谨慎使用此类代码。

注:使用 MemberExpression 方案比 CallExpression 更稳定,能防止任何形式的 console 调用。


三、 MCP 模式下的日志与进程收拢

  1. 全局变量标识

在 CLI 入口处,根据启动参数或环境变量设置全局标识:

typescript

// cli.ts
if (process.argv.includes('--mcp')) {
  global.IS_MCP_MODE = true;
}

请谨慎使用此类代码。

  1. 自定义 Logger 的分流策略

typescript

export const logger = {
  info: (msg: string) => {
    // MCP 模式下必须禁绝 STDOUT 输出
    if (!global.IS_MCP_MODE) {
      console.log(msg);
    }
  },
  error: (msg: string) => {
    if (global.IS_MCP_MODE) {
      // MCP 场景下,报错应直接抛出,由顶层 Server 捕获并转为 JSON-RPC Error
      throw new Error(msg);
    } else {
      console.error(msg);
      process.exit(1);
    }
  }
};

请谨慎使用此类代码。

  1. 子进程 exec 的静默陷阱

在 CLI 中经常会调用 exec 或 spawn绝对不要在 MCP 模式下使用 stdio: 'inherit'

  • 错误示范execSync('npm install', { stdio: 'inherit' }) —— 这会把子进程的日志直接塞进 MCP 管道。

  • 正确示范

    typescript

    const stdioSetting = global.IS_MCP_MODE ? 'pipe' : 'inherit';
    const result = execSync('npm install', { stdio: ['ignore', stdioSetting, 'inherit'] });
    

    请谨慎使用此类代码。

    注意:stderr 设置为 inherit 通常是安全的,因为 MCP 客户端一般会忽略或单独记录 STDERR。


四、 总结

将 Node.js 工具适配 MCP 不仅仅是写一个 Server 接口,更是一场关于“输出管理”的修行:

  • 分发层:搞定 bin 和 npx
  • 规范层:用 ESLint AST 强制引导团队使用收拢的 Logger。
  • 执行层:对 console 和 child_process 进行严格的流重定向。

守住了 STDOUT,就守住了 MCP 的生命线。