为什么你需要 Stdio MCP 而不是重写原生 Server
MCP(Model Context Protocol)是Agent调用外部能力的标准接口,但每来一个新工具就写一个原生 MCP Server——定义 schema、实现 handler、写错误处理、打包发布——至少半天起步,对个人开发者太重了。
Stdio MCP Server 的思路是:绝大多数命令行工具已经具备"输入参数→输出结果"的纯函数特性,只要把它通过 stdin/stdout 按 MCP 协议包一层,几分钟就能接入,不需要理解 MCP 的复杂协议细节。
对比两种方式:
| 维度 | 写原生 MCP Server | Stdio MCP 包装 |
|---|---|---|
| 实现工作量 | 半天-数天(理解协议+写handler+测试) | 5-30分钟(写配置/几十行脚本) |
| 适用范围 | 复杂语义、需要缓存/状态/批量的场景 | 无状态命令行工具(90%日常需求) |
| 调试难度 | 需要断点调试 MCP 消息交互 | 直接在终端跑命令验证,极其直观 |
| 维护成本 | MCP协议升级要跟着改 | 不需要,包装器本身逻辑稳定 |
| 可移植性 | 需要对应运行时环境 | 只要目标机器有对应命令就能跑 |
原则:能 Stdio 包的就不写原生,除非你需要状态管理、流式输出、或者复杂的参数校验回退。
核心原理:50行代码看懂 Stdio 包装器在做什么
Stdio MCP Server 的工作流程极其简单:
Agent 发起工具调用 (MCP JSON-RPC)
↓
Stdio 包装器收到 {"name":"xxx","input":{"args":"...","timeout":60}}
↓
子进程 spawn:命令 + args(做参数白名单过滤)
↓
捕获 stdout / stderr / exitCode
↓
包装成 MCP 响应返回给 Agent:
{ content: [{type:"text", text: stdout + stderr}], isError: exitCode!=0 }
Node.js 版最小可运行包装器(50行不到):
// mcp-stdio-wrapper.mjs
import { spawn } from 'child_process';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
const ALLOWED_COMMANDS = {
// 白名单:Agent只能调用这里列的命令,防止任意命令执行漏洞
curl: {
bin: 'curl',
description: '通用HTTP请求工具,参数直接透传给curl',
argsSanitizer: (args) => args.filter(a => !a.includes(';') && !a.includes('&&')),
},
jq: {
bin: 'jq',
description: 'JSON解析与转换,-r 输出纯文本',
argsSanitizer: (args) => args,
},
ffmpeg: {
bin: 'ffmpeg',
description: '音视频处理,支持转码/裁剪/拼接等',
argsSanitizer: (args) => args,
},
};
const server = new Server(
{ name: 'stdio-tools', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: Object.entries(ALLOWED_COMMANDS).map(([name, cfg]) => ({
name,
description: cfg.description,
inputSchema: {
type: 'object',
properties: {
args: { type: 'array', items: { type: 'string' }, description: '命令参数数组' },
timeout: { type: 'number', description: '超时秒数,默认60' },
},
required: ['args'],
},
})),
}));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const cfg = ALLOWED_COMMANDS[req.params.name];
if (!cfg) throw new Error(`命令不在白名单: ${req.params.name}`);
const args = cfg.argsSanitizer(req.params.arguments.args || []);
const timeout = (req.params.arguments.timeout || 60) * 1000;
const child = spawn(cfg.bin, args, { timeout });
let stdout = '', stderr = '';
child.stdout.on('data', d => stdout += d);
child.stderr.on('data', d => stderr += d);
return new Promise(resolve => {
child.on('close', exitCode => resolve({
content: [{ type: 'text', text: stdout + (stderr ? `\n---STDERR---\n${stderr}` : '') }],
isError: exitCode !== 0,
}));
});
});
const transport = new StdioServerTransport();
await server.connect(transport);
⚠️ 安全红线:必须做命令白名单 + 参数过滤,绝对不要允许任意 shell 命令透传。Agent一旦被提示注入攻破,
rm -rf /可不是闹着玩的。上面例子里的argsSanitizer和ALLOWED_COMMANDS就是安全闸门,不能省。
安装与配置
使用官方通用版(最快,无需自己写代码)
官方已经提供了一个通用的 server-stdio,支持直接把命令包成 MCP 工具:
# 把 curl 包成 MCP 工具
npx -y @modelcontextprotocol/server-stdio -- \
--tool curl:command=curl:args=*:desc="通用HTTP请求,参数透传"
Claude Code(CLI)注册
# 单个命令注册
claude mcp add stdio-curl -- \
npx -y @modelcontextprotocol/server-stdio -- \
--tool curl:command=curl:args=*:desc="HTTP请求" \
--tool jq:command=jq:args=*:desc="JSON处理"
Cursor / VS Code 配置
编辑 ~/.cursor/mcp.json(Cursor)或对应扩展的 MCP 配置:
{
"mcpServers": {
"stdio-tools": {
"command": "npx",
"args": [
"-y", "@modelcontextprotocol/server-stdio", "--",
"--tool", "curl:command=curl:args=*:desc=通用HTTP请求,参数数组形式",
"--tool", "jq:command=jq:args=*:desc=JSON解析",
"--tool", "pandoc:command=pandoc:args=*:desc=文档格式转换"
]
}
}
}
配置完成后重启客户端,工具列表中应该能看到 curl、jq、pandoc 三个可调用工具。
三大高复用实战样板
样板一:curl → 让 Agent 拥有 HTTP 调用能力
没有 Stdio MCP 之前,Agent想调外部API只能"告诉你 curl 命令,你复制运行"。现在它自己就能调:
典型调用链:
- Agent 用 curl 调 GitHub API 拿到 issue 列表
- 输出 JSON 给 jq 过滤出需要的字段
- 用 curl 调飞书机器人 Webhook 把摘要发到群里
典型用法(Agent视角):
用 curl 工具 args=["-sS", "https://api.github.com/repos/user/repo/issues?state=open&per_page=5"] 拉取最近的 issue,返回 JSON 后再用 jq 提取每条的 number 和 title。
⚠️ curl 必加参数:-sS(静默但显示错误)、--max-time 30(超时)、-f(HTTP 错误码非2xx时返回失败)。把它们加进 argsSanitizer 的前缀,避免Agent每次写漏。
样板二:jq → 让 Agent 处理结构化数据
Agent本身能处理JSON,但面对几百KB的大JSON直接塞上下文很浪费。用 jq 在子进程里做过滤/聚合/转换:
典型能力:
jq '.data[] | {id, name}':提取嵌套字段,输出精简版jq '[.[] | select(.status=="open")] | length':流式聚合计算统计jq -r '.[] | "\(.id)\t\(.title)"':转 TSV 贴进表格工具
样板三:ffmpeg / pandoc → 让 Agent 处理媒体和文档
以前你让 Agent 做"把这个视频截个封面"或"把 Markdown 转成 Word",它只能给你命令让你自己跑。现在:
ffmpeg 典型调用:
用 ffmpeg 工具 args=["-i", "input.mp4", "-ss", "00:00:01", "-vframes", "1", "cover.jpg"] 截取第一帧做封面。
pandoc 典型调用:
用 pandoc 工具 args=["report.md", "-o", "report.docx", "--reference-doc=template.docx"] 按模板转成Word。
注意事项
1. 白名单和参数过滤是底线,不要跳过
Stdio MCP 的最大风险是任意命令执行。如果你的 argsSanitizer 是 (args) => args(什么都不过滤),又没做命令白名单,被注入 ; rm -rf / 就GG了。至少要:
- 命令白名单(ALLOWED_COMMANDS)
- 参数禁
;、&&、|、>、$()等shell元字符(如果直接传给shell的话) - 对已知高危参数(比如 curl 的
-o /etc/passwd)加黑名单
2. 输出截断,防止子进程输出把Agent上下文撑爆
比如不小心 curl 了一个100MB的文件,stdout全返回给Agent,上下文直接炸。建议在包装器里加:
const MAX_OUTPUT = 200_000; // 200KB
if (stdout.length > MAX_OUTPUT) {
stdout = stdout.slice(0, MAX_OUTPUT) +
`\n... [已截断,超出${MAX_OUTPUT}字节,请用更精确参数缩小输出范围]`;
}
3. 超时必须设默认值
子进程挂死会导致整个 MCP 连接卡死。timeout 参数默认60秒,并在 spawn 时传入。对 ffmpeg 等长时命令可单独放宽,但一定要有上限。
4. 真的需要复杂语义时再切原生 MCP
当你发现 Stdio 包装器里加了太多特化逻辑(缓存、重试、状态、批量合并),就该考虑写成原生 MCP Server 了。Stdio 的定位是「快速让 80% 简单工具可用」,不是替代所有原生实现。
Stdio MCP 是 MCP 生态里最被低估的"瑞士军刀"——掌握它,你手上所有现成的命令行工具,一夜之间都变成了 Agent 能自主调用的能力。