先装现成 MCP,还是自己构建?
Install existing MCP
- Filesystem, Context7, GitHub, Playwright, Firecrawl
- 需求通用,维护责任在官方或社区
- 你想更快上线,而不是维护一段新代码
Build custom MCP
- 私有 API、内部 playbook、固定 JSON 输出、窄权限流程
- 需求稳定,但上下文经常漂移,提示词越来越长
- 你想把读取范围、错误码和输出结构锁定下来
Custom MCP design canvas
先把工具契约写清楚,再开始编码。这个表会帮你锁定输入、输出、允许资源和错误边界。
Custom MCP design canvas markdown
| Field | Decision |
| --- | --- |
| Tool name | lookup_playbook_rule |
| User goal | Return one approved rule for a repeatable workflow |
| Inputs | topic: string |
| Output | { title: string, rule: string, source: string } |
| Allowed resources | ./playbooks/*.json only |
| Forbidden actions | No writes, no shell commands, no network |
| Timeout | 3 seconds |
| Error codes | NOT_FOUND, INVALID_INPUT, ACCESS_DENIED |
| Verification prompt | Ask for the rule for "release checklist" | Minimal custom MCP server
下面这个最小 TypeScript stdio server 足够演示一个稳定的本地规则查询工具,同时保留 inputSchema、错误码和只读边界。
Minimal custom MCP server ts
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const DATA_FILE = join(__dirname, '../data/playbooks.json');
const REQUEST_TIMEOUT_MS = 3_000;
type PlaybookRule = {
topic: string;
title: string;
rule: string;
source: string;
};
const server = new McpServer({ name: 'custom-playbook-mcp', version: '1.0.0' });
async function readRules(): Promise<PlaybookRule[]> {
// Allowlist: read only ./data/playbooks.json. Never accept user-controlled file paths.
const raw = await readFile(DATA_FILE, 'utf8');
return z.array(
z.object({
topic: z.string(),
title: z.string(),
rule: z.string(),
source: z.string(),
})
).parse(JSON.parse(raw));
}
function errorPayload(code: 'INVALID_INPUT' | 'NOT_FOUND' | 'ACCESS_DENIED', message: string) {
return JSON.stringify({ error: code, message });
}
server.registerTool(
'lookup_playbook_rule',
{
description: 'Return one approved rule from the local playbook.',
inputSchema: z.object({ topic: z.string().min(1) }),
},
async ({ topic }) => {
const normalizedTopic = topic.trim().toLowerCase();
if (!normalizedTopic) {
return {
content: [{ type: 'text', text: errorPayload('INVALID_INPUT', 'topic must not be empty') }],
};
}
const task = (async () => {
const rules = await readRules();
const match = rules.find((item) => item.topic.toLowerCase() === normalizedTopic);
if (!match) {
return {
content: [{ type: 'text', text: errorPayload('NOT_FOUND', `No rule found for "${topic}"`) }],
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
title: match.title,
rule: match.rule,
source: match.source,
}),
},
],
};
})();
return await Promise.race([
task,
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('ACCESS_DENIED: request timed out or touched a forbidden resource')), REQUEST_TIMEOUT_MS)
),
]);
}
);
const transport = new StdioServerTransport();
await server.connect(transport); Create custom MCP project
这组命令会创建一个最小本地工程。后面 OpenClaw 就通过 stdio 启动它。
Create custom MCP project bash
mkdir custom-playbook-mcp
cd custom-playbook-mcp
npm init -y
npm install @modelcontextprotocol/server zod
npm install -D tsx typescript @types/node
npm pkg set type=module
npm pkg set scripts.dev="tsx src/server.ts"
mkdir src data Run custom MCP server
本地开发阶段,OpenClaw 启动的就是这条命令。先在终端跑通它,再接到 config。
Run custom MCP server bash
npm run dev Custom MCP playbook fixture
先用一个 deterministic fixture,把协议、字段和错误处理稳定下来。
Custom MCP playbook fixture json
[
{
"topic": "release checklist",
"title": "Release checklist",
"rule": "Run tests, build, verify config, and request review before deploy.",
"source": "playbooks.json"
},
{
"topic": "incident handoff",
"title": "Incident handoff",
"rule": "Summarize impact, status, owner, next checkpoint, and customer risk.",
"source": "playbooks.json"
}
] Custom MCP OpenClaw config
v1 只保留一种最直接的 config 方式。把 cwd 替换成你的绝对路径,不要混用多套启动方案。
Custom MCP OpenClaw config json
{
"mcpServers": {
"custom-playbook": {
"command": "npx",
"args": ["tsx", "./src/server.ts"],
"cwd": "/absolute/path/to/custom-playbook-mcp"
}
}
} Verification checklist
- 正常输入:topic=release checklist 时返回 title、rule、source。
- 空输入:确认返回 INVALID_INPUT,而不是空对象或崩溃。
- 未知 topic:确认返回 NOT_FOUND。
- 禁止路径:确认工具不会接受外部路径或任意文件名。
- 超时:确认超过 3 秒时返回显式失败,而不是长时间挂起。
- 输出结构:确认 OpenClaw 看到的是稳定 JSON 字符串,字段没有漂移。
Custom MCP verification prompt
Custom MCP verification prompt text
Use the custom-playbook MCP to look up the rule for "release checklist".
Return only the JSON fields title, rule, and source. Production hardening
固定 schema,版本号和错误码都显式声明。
默认只读;除非真的需要,否则不要写文件、不要跑 shell、不要发网络请求。
把本地数据路径做成 allowlist,而不是让用户传路径。
日志里不要打印原始密钥、token 或客户数据。
给每个工具请求设置 timeout 和可观测错误。
加最小化测试,至少覆盖成功、空输入、未知 topic。
Common errors
server not showing
症状:OpenClaw 里看不到 custom-playbook。可能原因:cwd 不是绝对路径,或 tsx /依赖没装好。修复:先在项目目录单独跑 npm run dev,再核对 config 里的 cwd。
tool call failed
症状:工具能看到,但调用时报错。可能原因:playbooks.json 结构和代码预期不一致,或抛出了未处理异常。修复:先用固定 fixture 跑通,再把错误映射成显式错误码。
invalid JSON response
症状:模型拿到的不是稳定 JSON。可能原因:返回了额外解释文本,或对象结构不稳定。修复:只返回 JSON.stringify 后的 title、rule、source。
works locally but not in OpenClaw
症状:终端能跑,OpenClaw 里失败。可能原因:OpenClaw 的工作目录、环境变量或启动命令不同。修复:对齐 command、args、cwd,并检查 OpenClaw 实际启动日志。