Advanced guide

为 OpenClaw 构建你自己的 MCP Server

这条路径适合已经确认现成 MCP 无法稳定承载你工作流的人。大多数常见需求仍然优先安装现成 MCP,自建 MCP 只在你需要固定规则、私有 API、稳定输出或更窄权限时更合适。

先装现成 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 实际启动日志。

需要我帮你审一下自建 MCP 的配置、工具契约或报错日志

回到配置指南