前言
随着 AI Agent 的流行,Model Context Protocol (MCP) 成为连接 LLM 与本地工具的桥梁。但在将现有的 Node.js CLI 工具改造为 MCP Server 的过程中,开发者往往会遇到两个极端问题:一是工具分发困难(npx 跑不动),二是日志输出干扰协议导致通信崩溃。
本文记录了我在改造 @xxx/cli 过程中的避坑指南,涵盖分发、规范约束及 MCP 场景适配。
一、 让你的 CLI 随时随地可运行
在 Monorepo 场景下,我们希望用户不创建项目也能通过 npx @xxx/cli 直接调用。如果发现调用失败,请检查以下三点:
- Bin 字段的玄学:在
package.json中,确保bin的键名与包名后缀一致。例如包名是@xxx/cli,bin设置为"cli": "index.js",这样npx的识别率最高。 - Shebang 必不可少:入口文件第一行必须是
#!/usr/bin/env node。 - 无环境执行:若在非 npm 项目下运行,确保包已发布且非私有。若是私有包,请确保环境中有
NPM_TOKEN。
二、 严格禁绝 Console:基于 AST 的 ESLint 进阶
在 MCP 模式下,标准输出 (STDOUT) 是神圣不可侵犯的。MCP 依靠 STDOUT 进行 JSON-RPC 通信,任何一行 console.log 都会导致协议解析失败。
- 为什么
no-console: error还不够?
原生的 no-console 报错信息太死板。在团队协作中,我们希望报错的同时告诉成员:“请使用项目封装的 log 方法,它会自动处理 MCP 静默策略”。
- 实战:定制化 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 模式下的日志与进程收拢
- 全局变量标识
在 CLI 入口处,根据启动参数或环境变量设置全局标识:
typescript
// cli.ts
if (process.argv.includes('--mcp')) {
global.IS_MCP_MODE = true;
}
请谨慎使用此类代码。
- 自定义 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);
}
}
};
请谨慎使用此类代码。
- 子进程
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 的生命线。