Skip to content
Published at:

第 33 章:自定义扩展开发

第 15-18 章我们学习了 Claude Code 的四大扩展机制——MCP 协议、插件系统、Skills 技能系统和 Hooks 钩子系统。但那四章的核心视角是"使用者":怎么配置、怎么安装、怎么调用。本章切换视角,成为"构建者":从零开始,亲手打造自己的 Claude Code 扩展

这是第七篇"精通篇"的核心章节之一。使用他人的扩展能让你效率翻倍,而构建自己的扩展能让你自由定制 Claude Code 的行为边界——把团队的工作流、公司的内部工具、你个人的开发偏好,全部编码为可复用的扩展。

本章目标:掌握四种扩展类型(MCP Server、Plugin、Skill、Hook)的完整开发流程,能够独立构建、测试、发布自定义扩展,理解每种扩展的适用场景和开发难度。

33.1 扩展开发生态概览

在动手之前,先建立一个全局视图:四种扩展各自的能力边界是什么?什么场景应该选择哪种扩展?每种扩展的开发门槛有多高?

四种扩展类型总览

quadrantChart title 四种扩展类型的定位 x-axis "低开发复杂度" --> "高开发复杂度" y-axis "对外部系统" --> "对 Claude 内部" quadrant-1 "能力扩展 + 复杂" quadrant-2 "行为定制 + 复杂" quadrant-3 "能力扩展 + 简单" quadrant-4 "行为定制 + 简单" "Skill": [0.2, 0.7] "Hook": [0.35, 0.55] "Plugin": [0.65, 0.65] "MCP Server": [0.8, 0.3]

四种扩展的核心区别可以总结为一张表:

扩展类型本质开发语言运行方式典型产物开发难度
MCP Server独立进程,暴露工具和资源Node.js / Python / 任意语言独立子进程,stdio/HTTP 通信npm 包 / Python 包 / 可执行文件⭐⭐⭐⭐
Plugin打包的扩展单元,可含多种能力主要 Markdown + 可选 TypeScript随 Claude Code 加载plugin.json + commands/hooks/skills 目录⭐⭐⭐
SkillMarkdown 文件,可复用的指令模板Markdown(纯文本)按需加载到上下文单个 .md 文件
Hook事件驱动的自动化脚本Shell / Python / Node.js事件触发时自动执行单个脚本文件⭐⭐

决策框架:什么时候构建哪种扩展?

选择正确的扩展类型是开发的第一步。以下决策框架帮助你根据需求快速定位:

你的需求是什么?

├─ 需要接入外部系统(数据库、API、文件系统等)
│   └─ 构建 MCP Server
│      理由:MCP 是标准的外部连接协议,可以被任何 MCP Client 复用

├─ 需要打包多种能力(命令 + 技能 + 钩子)为一个可分发单元
│   └─ 构建 Plugin
│      理由:Plugin 是容器,可以包含 Commands、Skills、Hooks、甚至 MCP Server

├─ 需要封装一套可复用的工作流方法论(多步骤、有顺序)
│   └─ 编写 Skill
│      理由:Skill 是指令模板,告诉 Claude "遇到这类问题怎么做",开发成本最低

├─ 需要在特定事件发生时自动执行操作(格式化、备份、检查)
│   └─ 编写 Hook
│      理由:Hook 是事件驱动,不需要 Claude 主动调用,适合自动化副作用

└─ 只是想让 Claude 执行一个简单操作
    └─ 写一个 Slash Command(见第 19 章)或直接写 Prompt
       理由:不需要扩展机制,一个命令或一段提示词就够了

技能要求对照

扩展类型需要掌握的技能
SkillMarkdown 写作能力。如果你能写好一段 Prompt,你就能写 Skill
HookShell 脚本基础。能写 if/else、理解 exit code、会用环境变量
Plugin理解 plugin.json 结构 + Markdown(命令)+ 可选 TypeScript(复杂钩子)
MCP ServerNode.js/Python 编程能力。理解异步编程、JSON Schema、进程间通信

建议路径:如果你的团队刚开始构建自定义扩展,从 Skill 开始(成本最低 → 收益最快),然后根据实际需要逐步过渡到 Hook → Plugin → MCP Server。不要一上来就挑战最复杂的 MCP Server 开发。

四种扩展的协作关系

在实际项目中,四种扩展往往组合使用:

flowchart TB subgraph PLUGIN["Plugin(容器)"] CMD["Slash Commands<br/>用户交互入口"] SK["Skills<br/>工作流方法论"] HK["Hooks<br/>自动化触发器"] end subgraph MCP["MCP Server(外部)"] TL["Tools<br/>可执行操作"] RS["Resources<br/>数据暴露"] end CMD -->|"调用"| SK CMD -->|"触发"| HK SK -->|"使用"| TL HK -->|"调用"| TL style PLUGIN fill:#e3f2fd style MCP fill:#fff3e0

一个典型的组合场景:Plugin 提供一个 /deploy 命令 → 命令触发一个 deploy-workflow Skill → Skill 在关键步骤触发 PreToolUse/PostToolUse Hooks → Hooks 通过 MCP Server 调用公司内部的部署 API。四种扩展各司其职,无缝配合。

33.2 编写自定义 MCP Server:Project Stats

第 15.6 节我们实现了一个最简单的 "Hello World" MCP Server。但真正的生产级 MCP Server 需要处理更实际的场景。本节我们构建一个完整的 Project Stats MCP Server——它能分析当前项目的 Git 统计、文件结构、以及最近的提交记录。

功能设计

我们的 Project Stats MCP Server 将暴露三个工具:

工具名功能输入参数输出
getGitStats获取 Git 仓库统计信息无(自动检测当前目录)分支名、总提交数、contributor 数、最近活跃度
getFileCount按扩展名统计文件数量directory(可选,默认项目根目录)各扩展名的文件数量分布
getRecentCommits获取最近的提交记录count(可选,默认 10)最近 N 次提交的 hash、作者、时间、消息

第一步:初始化项目

bash
mkdir project-stats-mcp && cd project-stats-mcp
pnpm init
# 确保 package.json 中有 "type": "module"
pnpm add @modelcontextprotocol/sdk

编辑 package.json,确保包含 ES Module 声明和启动脚本:

json
{
  "name": "project-stats-mcp",
  "version": "1.0.0",
  "description": "MCP Server that provides project statistics and Git insights",
  "type": "module",
  "main": "index.js",
  "bin": {
    "project-stats-mcp": "./index.js"
  },
  "files": [
    "index.js"
  ],
  "scripts": {
    "start": "node index.js"
  },
  "keywords": ["mcp", "git", "project-stats", "claude-code"],
  "license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0"
  }
}

第二步:实现 MCP Server

以下是完整的 index.js 实现,包含三个工具的全部逻辑:

javascript
#!/usr/bin/env node

/**
 * Project Stats MCP Server
 *
 * 为 Claude Code 提供项目统计能力:
 * - getGitStats: Git 仓库统计(分支、提交数、贡献者)
 * - getFileCount: 按扩展名统计文件数量
 * - getRecentCommits: 获取最近提交记录
 */

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { execSync } from "child_process";
import { readdirSync, statSync } from "fs";
import { join, resolve } from "path";

// ============================================================
// 工具函数
// ============================================================

/**
 * 安全执行 shell 命令,返回 stdout 字符串
 * 如果命令失败,返回空字符串而不是抛出异常
 */
function execSafe(cmd, options = {}) {
  try {
    return execSync(cmd, {
      encoding: "utf-8",
      timeout: 5000,        // 5 秒超时,防止卡死
      ...options
    }).trim();
  } catch (error) {
    console.error(`[project-stats-mcp] 命令执行失败: ${cmd}`, error.message);
    return "";
  }
}

/**
 * 检查当前目录是否在 Git 仓库中
 */
function isGitRepo() {
  return execSafe("git rev-parse --is-inside-work-tree") === "true";
}

/**
 * 递归统计目录中各类文件的扩展名分布
 */
function countFilesByExtension(dirPath, maxDepth = 10) {
  const counts = {};
  const ignoredDirs = new Set([
    "node_modules", ".git", ".vitepress", "dist",
    "build", ".next", "__pycache__", ".cache", "target"
  ]);

  function walk(currentPath, depth) {
    if (depth > maxDepth) return;

    let entries;
    try {
      entries = readdirSync(currentPath, { withFileTypes: true });
    } catch {
      return; // 跳过无权限读取的目录
    }

    for (const entry of entries) {
      const fullPath = join(currentPath, entry.name);

      if (entry.isDirectory()) {
        if (!ignoredDirs.has(entry.name) && !entry.name.startsWith(".")) {
          walk(fullPath, depth + 1);
        }
      } else if (entry.isFile()) {
        // 提取扩展名
        const ext = entry.name.includes(".")
          ? "." + entry.name.split(".").pop().toLowerCase()
          : "(无扩展名)";
        counts[ext] = (counts[ext] || 0) + 1;
      }
    }
  }

  walk(dirPath, 0);
  return counts;
}

// ============================================================
// 创建 MCP Server 实例
// ============================================================

const server = new Server(
  {
    name: "project-stats-mcp",
    version: "1.0.0",
  },
  {
    capabilities: { tools: {} },
  }
);

// ============================================================
// 注册 tools/list 处理器
// ============================================================

server.setRequestHandler("tools/list", async () => ({
  tools: [
    {
      name: "getGitStats",
      description:
        "获取当前项目的 Git 仓库统计信息,包括当前分支名、总提交数、贡献者数量、" +
        "最近 7 天的提交数、以及最近一次提交的信息。" +
        "当用户询问项目概况、仓库活跃度、或 Git 统计信息时使用此工具。" +
        "注意:此工具仅在 Git 仓库中有效。",
      inputSchema: {
        type: "object",
        properties: {
          // 无必填参数,全部自动检测
        },
        required: [],
      },
    },
    {
      name: "getFileCount",
      description:
        "按文件扩展名统计项目中的文件数量分布。返回每种扩展名(如 .ts、.js、.md)的文件数。" +
        "当用户询问项目文件组成、技术栈占比、或需要了解项目规模时使用此工具。" +
        "默认统计项目根目录,可通过 directory 参数指定子目录。" +
        "注意:会自动忽略 node_modules、.git、dist 等常见忽略目录。",
      inputSchema: {
        type: "object",
        properties: {
          directory: {
            type: "string",
            description:
              "要统计的目录路径(相对于项目根目录)。不指定则统计整个项目。例如:'src'、'src/components'",
          },
        },
        required: [],
      },
    },
    {
      name: "getRecentCommits",
      description:
        "获取 Git 仓库中最近的提交记录(默认最近 10 次)。返回每次提交的 hash、作者、日期和提交消息。" +
        "当用户询问最近的代码变更、提交历史、或需要了解项目开发动态时使用此工具。",
      inputSchema: {
        type: "object",
        properties: {
          count: {
            type: "number",
            description: "要获取的最近提交数量。默认 10,范围 1-50。",
          },
        },
        required: [],
      },
    },
  ],
}));

// ============================================================
// 注册 tools/call 处理器 — 核心业务逻辑
// ============================================================

server.setRequestHandler("tools/call", async (request) => {
  const { name, arguments: args } = request.params;

  try {
    switch (name) {
      // -------------------------------------------------------
      // getGitStats — Git 仓库统计
      // -------------------------------------------------------
      case "getGitStats": {
        // 1. 检查是否在 Git 仓库中
        if (!isGitRepo()) {
          return {
            content: [
              {
                type: "text",
                text: "当前目录不在 Git 仓库中。请在 Git 项目中使用此工具。",
              },
            ],
            isError: true,
          };
        }

        // 2. 收集统计信息
        const branch = execSafe("git branch --show-current") || "(detached HEAD)";
        const totalCommits = execSafe("git rev-list --count HEAD") || "0";
        const contributors = execSafe("git shortlog -sn --all | wc -l") || "0";
        const lastCommit = execSafe(
          'git log -1 --format="%h - %an - %s (%ar)"'
        ) || "(无提交)";
        const recentActivity = execSafe(
          'git log --since="7 days ago" --oneline | wc -l'
        ) || "0";
        const repoRoot = execSafe("git rev-parse --show-toplevel") || "未知";

        // 3. 组装结果
        const result = [
          "=== Git 仓库统计 ===",
          `仓库路径: ${repoRoot}`,
          `当前分支: ${branch}`,
          `总提交数: ${totalCommits}`,
          `贡献者数: ${contributors}`,
          `最近 7 天活跃提交: ${recentActivity}`,
          `最近一次提交: ${lastCommit}`,
        ].join("\n");

        return {
          content: [{ type: "text", text: result }],
        };
      }

      // -------------------------------------------------------
      // getFileCount — 文件数量统计
      // -------------------------------------------------------
      case "getFileCount": {
        const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
        const targetDir = args.directory
          ? resolve(projectDir, args.directory)
          : projectDir;

        // 检查目录是否存在
        try {
          statSync(targetDir);
        } catch {
          return {
            content: [
              {
                type: "text",
                text: `目录不存在: ${targetDir}`,
              },
            ],
            isError: true,
          };
        }

        // 统计文件
        const extCounts = countFilesByExtension(targetDir);

        // 按文件数量降序排列
        const sorted = Object.entries(extCounts).sort(
          (a, b) => b[1] - a[1]
        );
        const totalFiles = sorted.reduce((sum, [, count]) => sum + count, 0);

        if (sorted.length === 0) {
          return {
            content: [
              {
                type: "text",
                text: `目录 ${targetDir} 中没有找到任何文件。`,
              },
            ],
          };
        }

        // 组装结果(Markdown 表格格式)
        const lines = [
          `=== 文件统计 ===`,
          `目录: ${targetDir}`,
          `文件总数: ${totalFiles}`,
          "",
          `| 扩展名 | 数量 | 占比 |`,
          `|--------|------|------|`,
          ...sorted.map(([ext, count]) => {
            const pct = ((count / totalFiles) * 100).toFixed(1);
            return `| ${ext} | ${count} | ${pct}% |`;
          }),
        ];

        return {
          content: [{ type: "text", text: lines.join("\n") }],
        };
      }

      // -------------------------------------------------------
      // getRecentCommits — 最近提交记录
      // -------------------------------------------------------
      case "getRecentCommits": {
        if (!isGitRepo()) {
          return {
            content: [
              {
                type: "text",
                text: "当前目录不在 Git 仓库中。请在 Git 项目中使用此工具。",
              },
            ],
            isError: true,
          };
        }

        // 限制 count 范围:1-50,默认 10
        const count = Math.min(Math.max(parseInt(args.count) || 10, 1), 50);

        const log = execSafe(
          `git log -${count} --format="%h|%an|%ar|%s"`
        );

        if (!log) {
          return {
            content: [
              {
                type: "text",
                text: "未找到提交记录(可能是空仓库)。",
              },
            ],
          };
        }

        const commits = log.split("\n").map((line) => {
          const [hash, author, date, ...messageParts] = line.split("|");
          return {
            hash,
            author,
            date,
            message: messageParts.join("|"), // 提交消息中可能也有 |
          };
        });

        // 组装结果(Markdown 表格)
        const lines = [
          `=== 最近 ${count} 次提交 ===`,
          "",
          `| Hash | 作者 | 时间 | 提交消息 |`,
          `|------|------|------|---------|`,
          ...commits.map(
            (c) => `| ${c.hash} | ${c.author} | ${c.date} | ${c.message} |`
          ),
        ];

        return {
          content: [{ type: "text", text: lines.join("\n") }],
        };
      }

      // -------------------------------------------------------
      // 未知工具
      // -------------------------------------------------------
      default:
        return {
          content: [
            {
              type: "text",
              text: `未知工具: ${name}。可用工具: getGitStats, getFileCount, getRecentCommits`,
            },
          ],
          isError: true,
        };
    }
  } catch (error) {
    // 顶层错误处理:返回清晰的错误信息
    console.error(`[project-stats-mcp] 工具 ${name} 执行失败:`, error);
    return {
      content: [
        {
          type: "text",
          text: `工具 ${name} 执行失败: ${error.message}。请检查项目状态后重试。`,
        },
      ],
      isError: true,
    };
  }
});

// ============================================================
// 启动 stdio 传输
// ============================================================

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("[project-stats-mcp] Project Stats MCP Server 已启动");
}

main().catch((error) => {
  console.error("[project-stats-mcp] 启动失败:", error);
  process.exit(1);
});

代码解读:关键设计决策

这个 MCP Server 中,有几个值得说明的设计决策:

1. 错误处理策略 —— 返回错误信息而非抛出异常

javascript
// ✅ 正确:返回 isError: true,让 Claude 知道出了问题并可以尝试修正
return {
  content: [{ type: "text", text: "当前目录不在 Git 仓库中。" }],
  isError: true,
};

// ❌ 错误:直接 throw,Claude 收到的只有无上下文的异常堆栈
throw new Error("Not a git repository");

第 15.7 节提到过"返回清晰的错误信息,让 Claude 能够自我纠正"。这个原则在本 Server 中被严格执行——每一个可能的失败点都返回结构化的错误描述,而不是裸异常。

2. 输入验证 —— 永远不信任参数

javascript
// count 参数做了范围限制:只允许 1-50
const count = Math.min(Math.max(parseInt(args.count) || 10, 1), 50);

即使我们信任 Claude 不会恶意传参,防御式编程也是好习惯。一个 count: 999999 可能导致 Git 命令执行超时。

3. 超时保护 —— execSafe 的 5 秒超时

MCP 工具调用应该快速响应(见第 15.7 节第 4 条最佳实践"保持工具调用快速")。5 秒超时确保即使在大型仓库中,统计操作也不会阻塞 Claude Code 太久。

4. 忽略无关目录 —— 提升性能

javascript
const ignoredDirs = new Set([
  "node_modules", ".git", ".vitepress", "dist",
  "build", ".next", "__pycache__", ".cache", "target"
]);

遍历整个项目目录时跳过这些常见的大型依赖目录,可以大幅减少统计时间。在一个有 2 万文件的 node_modules 的项目中,跳过这些目录能将统计时间从 10 秒降到 200 毫秒。

第三步:本地测试

在配置到 Claude Code 之前,先在本地验证 MCP Server 的基本功能:

bash
# 1. 确保脚本可执行
chmod +x index.js

# 2. 手动模拟 MCP 协议交互(发送 tools/list 请求)
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node index.js

# 3. 如果使用 MCP Inspector(推荐)
npx @modelcontextprotocol/inspector node index.js

MCP Inspector 会启动一个 Web UI,让你可以在浏览器中测试 MCP Server 的各个工具——这是开发阶段最有效的调试方式。

第四步:配置到 Claude Code

将以下配置添加到 ~/.claude/settings.json.claude/settings.json

json
{
  "mcpServers": {
    "project-stats": {
      "command": "node",
      "args": ["/absolute/path/to/project-stats-mcp/index.js"]
    }
  }
}

重启 Claude Code 后,可以在对话中测试:

看看这个项目的 Git 统计信息
统计一下 src/ 目录下的文件类型分布
显示最近 15 次提交

第五步:发布到 npm

开发完成并通过测试后,可以将 MCP Server 发布到 npm,让团队成员通过 npx 一键使用:

bash
# 1. 确保文件清单正确(package.json 中 files 字段只包含 index.js)
# 2. 登录 npm(如果还未登录)
npm login

# 3. 发布
npm publish --access public

# 4. 团队使用
# 配置变为:
{
  "mcpServers": {
    "project-stats": {
      "command": "npx",
      "args": ["-y", "project-stats-mcp"]
    }
  }
}

调试技巧汇总

问题排查方法
Server 启动后无响应检查 console.error 输出,确保没有语法错误导致进程崩溃
工具没有被 Claude 发现检查 tools/list 的返回格式,确保 JSON 结构正确
工具调用失败但无错误信息tools/call 处理器中加 console.error 日志,输出到 stderr
execSync 超时增加 timeout 值,或优化命令(如加 --max-count 限制)
路径相关错误使用 process.env.CLAUDE_PROJECT_DIR 获取项目根目录(而非 process.cwd()

33.3 编写自定义 Plugin:Timestamp Logger

第 16.6 节展示了最简插件(一个 /hello 命令)。本节构建一个更实用的插件——Timestamp Logger。它记录 Claude Code 每次工具调用的时间戳,帮助团队分析 Claude Code 的使用模式和效率。

插件功能设计

能力说明
/log-summarySlash Command:生成本次会话的工具调用时间线摘要
PostToolUse Hook自动记录每次工具调用的时间戳到日志文件
timestamp-logger:analyzeSkill:分析日志文件,识别效率瓶颈

第一步:创建插件目录结构

timestamp-logger/
├── plugin.json              # 插件清单(必需)
├── commands/
│   └── log-summary.md       # /log-summary 命令定义
├── hooks/
│   └── post-tool-use.sh     # PostToolUse Hook 脚本
├── skills/
│   └── analyze.md           # 日志分析 Skill
└── README.md                # 使用文档

第二步:编写 plugin.json

json
{
  "name": "timestamp-logger",
  "version": "1.0.0",
  "description": "记录 Claude Code 每次工具调用的时间戳,并提供会话分析能力。适用于希望量化 AI 辅助开发效率的团队。",
  "author": "your-team-name",
  "license": "MIT",
  "homepage": "https://github.com/your-team/timestamp-logger",
  "repository": {
    "type": "git",
    "url": "https://github.com/your-team/timestamp-logger"
  },
  "commands": [
    {
      "name": "log-summary",
      "description": "生成本次会话中所有工具调用的时间线摘要",
      "file": "commands/log-summary.md"
    }
  ],
  "hooks": [
    {
      "event": "PostToolUse",
      "matcher": "",
      "handler": "hooks/post-tool-use.sh"
    }
  ],
  "skills": [
    {
      "name": "timestamp-logger:analyze",
      "description": "分析工具调用日志,识别高频操作、耗时环节和效率瓶颈",
      "file": "skills/analyze.md"
    }
  ]
}

第三步:编写 PostToolUse Hook

hooks/post-tool-use.sh

bash
#!/bin/bash
# ============================================================
# Timestamp Logger — PostToolUse Hook
# 每次工具调用后,记录时间戳、工具名、涉及文件和耗时
# ============================================================

LOG_DIR="${CLAUDE_PROJECT_DIR}/.claude/logs"
LOG_FILE="${LOG_DIR}/tool-timestamps.jsonl"

# 创建日志目录(如果不存在)
mkdir -p "$LOG_DIR"

# 构造日志条目(JSON Lines 格式,每行一个 JSON 对象,方便追加和解析)
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
TOOL_NAME="${CLAUDE_TOOL_NAME:-unknown}"
OUTPUT_FILE="${CLAUDE_TOOL_OUTPUT_FILE:-}"
EXIT_CODE="${CLAUDE_TOOL_EXIT_CODE:-0}"
SESSION_ID="${CLAUDE_SESSION_ID:-unknown}"

# 生成并追加日志条目
cat >> "$LOG_FILE" << EOF
{"timestamp":"${TIMESTAMP}","tool":"${TOOL_NAME}","file":"${OUTPUT_FILE}","exitCode":${EXIT_CODE},"sessionId":"${SESSION_ID}"}
EOF

# 静默成功(不输出任何东西,避免污染 Claude 的上下文)
exit 0

设计说明

  • JSON Lines 格式:每行一个 JSON 对象。相比纯 JSON 数组,追加操作不需要重写整个文件;相比纯文本,解析更结构化。
  • 日志位置.claude/logs/ 放在项目目录下,随项目一起被 .gitignore 忽略(记得添加)。
  • 静默执行:Hook 不在 stdout 输出任何内容,避免干扰 Claude 的上下文。

第四步:编写 Slash Command

commands/log-summary.md

markdown
# /log-summary

生成当前会话的工具调用时间线摘要,帮助回顾本次会话中的操作。

## Steps

1. **读取日志文件**
   读取 `.claude/logs/tool-timestamps.jsonl` 文件。

2. **筛选当前会话**
   只保留 `sessionId` 匹配当前会话 ID 的日志条目。
   你可以在执行命令前运行 `echo $CLAUDE_SESSION_ID` 来获取当前会话 ID。

3. **生成摘要**
   按以下格式输出 Markdown 表格:

会话工具调用摘要

会话 ID: {sessionId} 总调用次数:

时间工具文件状态
14:23:01Writesrc/utils.ts
14:23:15Bash
14:24:02Editsrc/index.ts
............

4. **统计分布**:
在表格后追加各类工具的使用次数统计:
- 文件操作(Read/Write/Edit)
- 终端执行(Bash)
- 搜索操作(Grep/Glob)
- 其他工具

5. **效率提示**(可选):
如果某类操作占比异常高(如 Read 占比超过 60%),提示用户:
"注意到本次会话中有大量文件读取操作。建议考虑使用 CLAUDE.md 提供更多项目上下文,减少重复读取。"

第五步:编写日志分析 Skill

skills/analyze.md

markdown
---
name: timestamp-logger:analyze
description: 分析工具调用日志,识别高频操作、耗时环节和效率瓶颈
when_to_use: 用户想要了解 Claude Code 的使用效率、分析工具调用模式、或优化工作流时
---

## Instructions

你是 Claude Code 使用效率分析专家。你的工作是分析工具调用日志,帮助用户优化他们的 AI 辅助开发体验。

### 分析流程

1. **读取日志**:读取 `.claude/logs/tool-timestamps.jsonl`

2. **整体统计**
   - 总调用次数
   - 统计周期(最早到最晚的时间戳)
   - 每日平均调用次数
   - 最活跃的会话

3. **工具分布分析**
   - 各类工具的调用次数和占比
   - 识别高频操作模式
   - 对比行业基准(见下方参考数据)

4. **效率评估**
   - Read 占比过高(> 50%)→ 建议优化 CLAUDE.md 或添加上下文
   - Bash 失败率过高(> 10%)→ 建议检查环境配置
   - 单次会话调用超过 200 次 → 建议使用 `/compact` 压缩上下文

5. **输出报告**
   - 使用 Markdown 格式
   - 包含数据表格和可视化建议
   - 给出 3-5 条具体可操作的优化建议

### 行业参考基准(供对比)

| 指标 | 健康范围 | 需要关注 |
|------|---------|---------|
| Read 占比 | 30%-45% | > 50% 或 < 20% |
| Write/Edit 占比 | 20%-35% | < 10%(任务过于简单) |
| Bash 占比 | 15%-30% | > 40%(可能存在自动化不足) |
| Bash 失败率 | < 5% | > 10% |
| 搜索操作(Grep/Glob)占比 | 5%-15% | < 2%(可能缺少代码探索) |

第六步:安装与测试

bash
# 1. 将插件目录复制到项目
cp -r timestamp-logger .claude/plugins/

# 2. 在 .claude/settings.json 中注册
json
{
  "plugins": [
    {
      "name": "timestamp-logger",
      "source": "local",
      "path": ".claude/plugins/timestamp-logger"
    }
  ]
}
bash
# 3. 添加日志目录到 .gitignore
echo ".claude/logs/" >> .gitignore

重启 Claude Code 后,做几次正常操作(读取文件、编辑代码、执行命令),然后用以下命令测试:

/log-summary
用 timestamp-logger:analyze 分析我的工具使用效率

发布到 ECC 社区市场

插件开发完成并经过团队内验证后,可以发布到 ECC 社区市场:

  1. Fork ECC 仓库https://github.com/affaan-m/ECC
  2. 添加插件目录:在 ECC 仓库中创建 plugins/timestamp-logger/,放入完整插件文件
  3. 编写高质量的 README.md:包含功能介绍、安装步骤、使用示例、权限说明
  4. 提交 Pull Request:等待社区审核

在 README 中,建议包含以下内容:

markdown
# Timestamp Logger

记录 Claude Code 每次工具调用的时间戳,提供会话分析和效率洞察。

## 功能

- **自动记录**:每个工具调用后自动记录时间戳(零配置)
- **会话摘要**`/log-summary` 命令查看当前会话的操作时间线
- **效率分析**`timestamp-logger:analyze` Skill 分析使用模式和瓶颈

## 安装

// ... 安装步骤

## 权限要求

| 权限 | 原因 |
|------|------|
| Write(.claude/logs/) | 写入日志文件 |

## 文件清单

| 文件 | 用途 |
|------|------|
| `.claude/logs/tool-timestamps.jsonl` | 工具调用日志(JSON Lines 格式) |

33.4 编写自定义 Skill:API 端点生成器

第 17.3 节展示了 React 组件的 Skill。本节编写一个不同领域的 Skill——API 端点生成器。这个 Skill 指导 Claude 按照团队规范生成完整的 REST API 端点代码,包括路由、控制器、验证和测试。

Skill 设计思路

一个好的 Skill 不只是"告诉 Claude 做什么",更重要的是嵌入团队规范。对于 API 端点生成这个场景,团队规范包括:

  • 文件命名约定
  • 目录结构约定
  • 请求验证模板
  • 错误处理模式
  • 测试覆盖要求

Skill 将这些隐性知识显性化,确保每次生成的代码都是一致的。

Skill 文件

将以下文件保存为 .claude/skills/my-team:api-endpoint.md

markdown
---
name: my-team:api-endpoint
description: 按照团队规范创建 REST API 端点(Express + TypeScript + Zod 验证 + 测试)
when_to_use: 创建新的 API 端点、CRUD 资源、或 RESTful 路由时
---

## Instructions

你是遵循团队规范的 API 端点开发专家。你创建的每个端点都必须满足以下标准。

### 团队规范速查

| 规范项 | 标准 |
|--------|------|
| 语言 | TypeScript 严格模式 |
| 框架 | Express.js + express-validator(或 Zod) |
| 验证库 | Zod(推荐)或 Joi |
| 测试框架 | Vitest + Supertest |
| 文件命名 | kebab-case:`user-routes.ts``create-order.test.ts` |
| 目录结构 | `src/api/{resource}/` 下集中管理 |

### 工作流程

#### 第一步:需求确认

在创建任何文件之前,向用户确认以下信息:

1. **资源名称**(如 `user``order``product`
2. **需要实现的操作**:List / Create / Read / Update / Delete(至少选一个)
3. **数据字段**:每个字段的名称、类型、是否必填、验证规则
4. **是否关联其他资源**:如果有,列出关联关系

#### 第二步:创建目录和文件

按以下结构创建文件(假设资源名为 `{resource}`):

src/api/{resource}/ ├── {resource}-routes.ts # 路由定义 ├── {resource}-controller.ts # 控制器逻辑 ├── {resource}-schema.ts # Zod 验证模式 ├── {resource}-types.ts # TypeScript 类型定义 ├── {resource}-service.ts # 业务逻辑层(如果有复杂逻辑) └── tests/ └── {resource}.test.ts # 集成测试


#### 第三步:逐文件实现

**3.1 类型定义(`{resource}-types.ts`)**

```typescript
// 示例:用户资源的类型定义
export interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'viewer';
  createdAt: string;
  updatedAt: string;
}

export interface CreateUserInput {
  name: string;
  email: string;
  role?: 'admin' | 'user' | 'viewer';
}

export interface UpdateUserInput {
  name?: string;
  email?: string;
  role?: 'admin' | 'user' | 'viewer';
}

3.2 Zod 验证模式({resource}-schema.ts

typescript
import { z } from 'zod';

export const createUserSchema = z.object({
  name: z.string().min(1, '姓名不能为空').max(100),
  email: z.string().email('邮箱格式不正确'),
  role: z.enum(['admin', 'user', 'viewer']).default('user'),
});

export const updateUserSchema = z.object({
  name: z.string().min(1).max(100).optional(),
  email: z.string().email().optional(),
  role: z.enum(['admin', 'user', 'viewer']).optional(),
});

// 导出推断的 TypeScript 类型(与手写的 interface 完全一致)
export type CreateUserInput = z.infer<typeof createUserSchema>;
export type UpdateUserInput = z.infer<typeof updateUserSchema>;

3.3 控制器({resource}-controller.ts

typescript
import { Request, Response, NextFunction } from 'express';
// 使用 Zod 的 safeParse 进行验证
import { createUserSchema, updateUserSchema } from './user-schema';
import * as userService from './user-service';

/**
 * 创建资源
 * POST /api/users
 */
export async function create(req: Request, res: Response, next: NextFunction) {
  try {
    // 1. 请求验证
    const parsed = createUserSchema.safeParse(req.body);
    if (!parsed.success) {
      return res.status(400).json({
        error: '验证失败',
        details: parsed.error.flatten().fieldErrors,
      });
    }

    // 2. 业务逻辑
    const user = await userService.createUser(parsed.data);
    return res.status(201).json(user);
  } catch (error) {
    next(error);
  }
}

/**
 * 获取资源列表
 * GET /api/users
 */
export async function list(req: Request, res: Response, next: NextFunction) {
  try {
    const page = Math.max(1, parseInt(req.query.page as string) || 1);
    const limit = Math.min(100, Math.max(1, parseInt(req.query.limit as string) || 20));

    const result = await userService.listUsers({ page, limit });
    return res.json(result);
  } catch (error) {
    next(error);
  }
}

// getById / update / remove 省略,模式同上

3.4 路由定义({resource}-routes.ts

typescript
import { Router } from 'express';
import * as controller from './user-controller';

const router = Router();

router.post('/', controller.create);
router.get('/', controller.list);
router.get('/:id', controller.getById);
router.patch('/:id', controller.update);
router.delete('/:id', controller.remove);

export default router;

3.5 测试文件(__tests__/{resource}.test.ts

typescript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import { createApp } from '../../app'; // 项目中的应用工厂

const app = createApp();

describe('POST /api/users', () => {
  it('应该成功创建用户并返回 201', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: '张三', email: 'zhangsan@example.com' });

    expect(res.status).toBe(201);
    expect(res.body).toHaveProperty('id');
    expect(res.body.name).toBe('张三');
  });

  it('邮箱格式不正确时应该返回 400', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: '张三', email: 'invalid-email' });

    expect(res.status).toBe(400);
    expect(res.body.error).toBe('验证失败');
  });

  it('姓名为空时应该返回 400', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: '', email: 'test@example.com' });

    expect(res.status).toBe(400);
  });
});

describe('GET /api/users', () => {
  it('应该返回分页的用户列表', async () => {
    const res = await request(app).get('/api/users?page=1&limit=10');
    expect(res.status).toBe(200);
    expect(res.body).toHaveProperty('data');
    expect(res.body).toHaveProperty('total');
    expect(res.body).toHaveProperty('page', 1);
  });
});

第四步:验证和输出

完成所有文件后:

  1. 运行类型检查pnpm tsc --noEmit
  2. 运行测试pnpm vitest run src/api/{resource}/
  3. 输出变更清单:列出所有创建/修改的文件及其用途
  4. 提醒用户:如果资源需要数据库迁移,提醒用户创建迁移文件

### Skill 的关键设计要素

回顾这个 Skill,有三点值得强调:

**1. "先确认再动手"模式**

Skill 第一步要求向用户确认资源名称、操作列表、数据字段。这避免了"Claude 猜你想要什么 → 猜错了 → 重做"的浪费。第 17.5 节最佳实践中提到"先理解再调用",在 Skill 内部也应该贯彻这个原则。

**2. 嵌入团队规范作为不可协商的约束**

模板代码中的命名约定、目录结构、验证模式都是团队规范的具体体现。当 Claude 按照这个 Skill 执行时,产出的代码天然符合团队标准——不需要事后 Code Review 再纠正。

**3. 完整的验证步骤**

Skill 不是"生成代码就完了",它要求运行类型检查和测试作为完成标志。这和 `superpowers:verification-before-completion` 的理念一致(见第 17.2.1 节)——**声称完成之前,先拿出证据**。

### 安装与团队共享

将此 Skill 文件放在项目 `.claude/skills/` 下,通过 Git 随项目分发:

```bash
# 项目结构
.claude/
└── skills/
    ├── my-team:api-endpoint.md
    └── my-team:react-component.md   # 另一个团队 Skill

# 在 CLAUDE.md 中记录
cat >> CLAUDE.md << 'EOF'

## 项目 Skills

`.claude/skills/` 中定义了团队开发规范 Skills:

- `my-team:api-endpoint` — 按团队规范创建 REST API 端点
- `my-team:react-component` — 按团队规范创建 React 组件

使用方式:直接告诉 Claude "用 my-team:api-endpoint 创建用户 CRUD"。
EOF

所有 clone 项目的团队成员自动获得相同的 Skill,无需额外配置。

33.5 编写自定义 Hook:Auto Backup

第 18.3 节提供了 5 个可以直接使用的 Hook 示例(格式化、备份、危险命令拦截、环境加载、Lint 检查)。本节编写一个更完整的 Auto Backup Hook——它在 Claude 修改文件前自动备份原文件内容,并支持备份恢复和清理策略。

功能设计

特性说明
触发时机PreToolUse(Edit / Write 之前)
备份方式将原文件复制到 .claude/backups/{date}/{filename}.bak
备份保留默认保留最近 7 天的备份,自动清理过期备份
恢复能力提供恢复脚本,可以按日期和文件名恢复
日志记录记录每次备份操作,方便审计

Hook 脚本实现

将以下脚本保存为 .claude/hooks/auto-backup.sh

bash
#!/bin/bash
# ============================================================
# Auto Backup Hook — PreToolUse
#
# 在 Claude 修改文件之前自动备份原始内容。
# 备份位置:.claude/backups/YYYY-MM-DD/
# 备份保留:最近 7 天(自动清理)
#
# 环境变量(由 Claude Code 注入):
#   CLAUDE_TOOL_NAME  — 工具名称(Write / Edit)
#   CLAUDE_TOOL_INPUT — 工具输入(JSON,包含 filePath)
#   CLAUDE_PROJECT_DIR — 项目根目录
# ============================================================

set -euo pipefail

# --------------------------------------------------
# 配置
# --------------------------------------------------
BACKUP_ROOT="${CLAUDE_PROJECT_DIR}/.claude/backups"
RETENTION_DAYS=7
LOG_FILE="${BACKUP_ROOT}/backup.log"
MAX_BACKUP_SIZE_MB=50  # 单文件超过此大小不备份
TODAY=$(date +%Y-%m-%d)

# --------------------------------------------------
# 工具函数
# --------------------------------------------------

log() {
  mkdir -p "$(dirname "$LOG_FILE")"
  echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] $*" >> "$LOG_FILE"
}

# 获取工具输入中的文件路径
# Write 工具:input 中的 filePath 字段
# Edit 工具:input 中的 file_path 字段
get_file_path() {
  local tool="$1"
  local input="$2"

  # 尝试从 JSON 中提取文件路径
  # Write 工具的字段名可能是 filePath 或 file_path
  echo "$input" | python3 -c "
import sys, json
try:
    data = json.load(sys.stdin)
    path = data.get('filePath') or data.get('file_path') or data.get('path') or ''
    print(path)
except:
    print('')
" 2>/dev/null
}

# 清理超过保留期限的备份目录
cleanup_old_backups() {
  find "$BACKUP_ROOT" -maxdepth 1 -type d -name "????-??-??" 2>/dev/null | while read -r dir; do
    dir_date=$(basename "$dir")
    dir_timestamp=$(date -j -f "%Y-%m-%d" "$dir_date" "+%s" 2>/dev/null || echo "0")
    cutoff_timestamp=$(date -j -v-${RETENTION_DAYS}d "+%s")

    if [ "$dir_timestamp" -lt "$cutoff_timestamp" ] 2>/dev/null; then
      log "清理过期备份: $dir"
      rm -rf "$dir"
    fi
  done
}

# --------------------------------------------------
# 主逻辑
# --------------------------------------------------

# 1. 只处理 Edit 和 Write 工具
if [[ ! "$CLAUDE_TOOL_NAME" =~ ^(Edit|Write)$ ]]; then
  exit 0
fi

# 2. 解析文件路径
TARGET_FILE=$(get_file_path "$CLAUDE_TOOL_NAME" "$CLAUDE_TOOL_INPUT")

if [ -z "$TARGET_FILE" ]; then
  log "WARN 无法从工具输入中提取文件路径,跳过备份。工具=${CLAUDE_TOOL_NAME}"
  exit 0
fi

# 3. 处理相对路径 → 绝对路径
if [[ "$TARGET_FILE" != /* ]]; then
  TARGET_FILE="${CLAUDE_PROJECT_DIR}/${TARGET_FILE}"
fi

# 4. 检查文件是否存在
if [ ! -f "$TARGET_FILE" ]; then
  # 文件不存在 = 新创建文件,无需备份
  log "SKIP 文件不存在(新文件): $TARGET_FILE"
  exit 0
fi

# 5. 检查文件大小(大文件跳过备份,避免磁盘占用过大)
FILE_SIZE=$(stat -f%z "$TARGET_FILE" 2>/dev/null || stat -c%s "$TARGET_FILE" 2>/dev/null || echo "0")
FILE_SIZE_MB=$(( FILE_SIZE / 1048576 ))

if [ "$FILE_SIZE_MB" -gt "$MAX_BACKUP_SIZE_MB" ]; then
  log "SKIP 文件过大 (${FILE_SIZE_MB}MB > ${MAX_BACKUP_SIZE_MB}MB): $TARGET_FILE"
  exit 0
fi

# 6. 创建备份目录
BACKUP_DIR="${BACKUP_ROOT}/${TODAY}"
mkdir -p "$BACKUP_DIR"

# 7. 生成备份文件名
RELATIVE_PATH="${TARGET_FILE#${CLAUDE_PROJECT_DIR}/}"
BACKUP_NAME="${RELATIVE_PATH//\//__}"  # 用 __ 替换路径分隔符
BACKUP_FILE="${BACKUP_DIR}/${BACKUP_NAME}.bak"

# 8. 执行备份
cp "$TARGET_FILE" "$BACKUP_FILE"
log "BACKUP ${CLAUDE_TOOL_NAME} → ${BACKUP_FILE} (${FILE_SIZE_MB}MB)"

# 9. 清理过期备份(每次只清理一次,避免重复操作)
CLEANUP_FLAG="${BACKUP_ROOT}/.cleanup-${TODAY}"
if [ ! -f "$CLEANUP_FLAG" ]; then
  cleanup_old_backups
  touch "$CLEANUP_FLAG"
fi

exit 0

恢复脚本

配套的恢复脚本 .claude/hooks/restore-backup.sh

bash
#!/bin/bash
# ============================================================
# Auto Backup 恢复脚本
#
# 用法:
#   bash restore-backup.sh                           # 列出所有可恢复的备份
#   bash restore-backup.sh <日期> <文件名>              # 恢复指定备份
#   bash restore-backup.sh --latest <文件名>           # 恢复最新备份
# ============================================================

BACKUP_ROOT="${CLAUDE_PROJECT_DIR:-.}/.claude/backups"

if [ ! -d "$BACKUP_ROOT" ]; then
  echo "未找到备份目录: $BACKUP_ROOT"
  exit 1
fi

# 列出所有备份
list_backups() {
  echo "=== 可用备份 ==="
  echo ""
  find "$BACKUP_ROOT" -name "*.bak" -type f | sort -r | while read -r bak; do
    date_dir=$(basename "$(dirname "$bak")")
    file_name=$(basename "$bak" .bak | sed 's/__/\//g')
    size=$(stat -f%z "$bak" 2>/dev/null || stat -c%s "$bak" 2>/dev/null || echo "?")
    echo "  ${date_dir}  ${file_name}  (${size} bytes)"
  done
}

# 恢复指定备份
restore_backup() {
  local date_dir="$1"
  local target_file="$2"

  # 将目标文件路径转为备份文件名
  local bak_name="${target_file//\//__}.bak"
  local bak_path="${BACKUP_ROOT}/${date_dir}/${bak_name}"

  if [ ! -f "$bak_path" ]; then
    echo "错误: 未找到备份文件 $bak_path"
    echo ""
    echo "可用备份:"
    list_backups
    exit 1
  fi

  # 确认恢复
  echo "即将恢复:"
  echo "  备份: $bak_path"
  echo "  目标: $target_file"
  echo ""
  read -p "确认恢复? (y/N) " -n 1 -r
  echo
  if [[ ! $REPLY =~ ^[Yy]$ ]]; then
    echo "已取消。"
    exit 0
  fi

  cp "$bak_path" "$target_file"
  echo "已恢复: $target_file"
}

# 恢复最新备份
restore_latest() {
  local target_file="$1"
  local bak_name="${target_file//\//__}.bak"

  local latest=$(find "$BACKUP_ROOT" -name "$bak_name" -type f | sort -r | head -1)

  if [ -z "$latest" ]; then
    echo "错误: 未找到文件 '$target_file' 的任何备份"
    exit 1
  fi

  restore_backup "$(basename "$(dirname "$latest")")" "$target_file"
}

# 入口
case "${1:-}" in
  "")
    list_backups
    ;;
  "--latest")
    if [ -z "${2:-}" ]; then
      echo "用法: bash restore-backup.sh --latest <文件路径>"
      exit 1
    fi
    restore_latest "$2"
    ;;
  *)
    if [ -z "${2:-}" ]; then
      echo "用法: bash restore-backup.sh <日期> <文件路径>"
      echo "      bash restore-backup.sh --latest <文件路径>"
      exit 1
    fi
    restore_backup "$1" "$2"
    ;;
esac

Hook 配置

.claude/settings.json 中注册 Hook:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "bash .claude/hooks/auto-backup.sh"
      }
    ]
  }
}
bash
# 赋予脚本执行权限
chmod +x .claude/hooks/auto-backup.sh
chmod +x .claude/hooks/restore-backup.sh

# 将备份目录加入 .gitignore
echo ".claude/backups/" >> .gitignore

关键设计决策

1. 大文件跳过策略

bash
MAX_BACKUP_SIZE_MB=50

不是所有文件都应该备份。大型数据文件、生成的构建产物、锁文件(如 pnpm-lock.yaml)动辄几 MB 到几十 MB,备份它们会快速消耗磁盘空间。50MB 是一个合理的阈值——对大多数源代码文件(通常 < 1MB)无影响,对大文件自动跳过。你可以根据项目情况调整。

2. JSON 解析文件路径

MCP 工具的输入是 JSON 字符串。我们使用 Python 来解析 JSON,因为:

  • Bash 原生 JSON 解析能力弱(需要 jq 额外安装)
  • Python 几乎每台开发机都有
  • safeParse 模式:解析失败返回空字符串,不抛异常

如果团队统一安装了 jq,可以改为:

bash
TARGET_FILE=$(echo "$CLAUDE_TOOL_INPUT" | jq -r '.filePath // .file_path // ""' 2>/dev/null)

3. 备份命名规则 —— 用 __ 替换路径分隔符

src/api/user-routes.ts  →  src__api__user-routes.ts.bak
src/components/Button.tsx  →  src__components__Button.tsx.bak

这样做的优势:所有备份文件平铺在一个目录中,不需要在备份目录中重建完整的目录树,查找和清理都很简单。恢复时反向替换即可。

4. 每日一次清理

bash
CLEANUP_FLAG="${BACKUP_ROOT}/.cleanup-${TODAY}"
if [ ! -f "$CLEANUP_FLAG" ]; then
  cleanup_old_backups
  touch "$CLEANUP_FLAG"
fi

清理操作每天只执行一次,而不是每次 Hook 触发都执行。这避免了频繁扫描文件系统带来的性能开销。

测试 Hook

遵循第 18.6 节的最佳实践"测试先行",按以下步骤验证:

bash
# 1. 独立测试备份逻辑
CLAUDE_TOOL_NAME="Write" \
CLAUDE_TOOL_INPUT='{"filePath":"src/test.txt"}' \
CLAUDE_PROJECT_DIR="/Users/me/my-project" \
bash .claude/hooks/auto-backup.sh

# 2. 检查备份是否创建
ls -la .claude/backups/$(date +%Y-%m-%d)/

# 3. 测试恢复
bash .claude/hooks/restore-backup.sh   # 列出备份
bash .claude/hooks/restore-backup.sh --latest src/test.txt  # 恢复最新

确认脚本在独立环境中正常工作后,再配置到 settings.json 中。

33.6 分享与发布扩展

构建好扩展只是第一步。让它被团队使用、被社区发现,才能真正发挥价值。本节介绍四种扩展类型的发布渠道和最佳实践。

发布渠道一览

扩展类型主要发布渠道安装方式目标受众
MCP Servernpm / PyPI / GitHubnpx -y package-name全球开发者
PluginECC 社区市场 / GitHub/Manage plugins 或手动安装Claude Code 用户
SkillGitHub(通过 Plugin 分发)或 .claude/skills/Git 共享或复制文件团队 / 社区
Hook团队 settings.json 或分享脚本片段复制代码到项目团队

必备文档清单

无论发布哪种扩展,以下文档是必需的:

文档内容要求优先级
README.md一句话介绍 + 功能列表 + 安装步骤 + 最简示例🔴 必需
权限说明扩展需要的所有权限及其正当理由🔴 必需
配置示例完整的 settings.json 配置片段🔴 必需
CHANGELOG.md每个版本的变更记录(Breaking Changes 必须注明)🟡 推荐
CONTRIBUTING.md如何参与贡献(如果是开源项目)🟢 可选
LICENSE开源许可证🔴 必需

权限说明的示例(以 Project Stats MCP Server 为例):

markdown
## 权限说明

| 权限 | 原因 | 范围 |
|------|------|------|
| 执行 `git` 命令 | 获取 Git 仓库统计和提交记录(只读) | 当前项目目录 |
| 读取文件系统 | 统计文件数量和扩展名分布(只读) | 项目根目录(排除 node_modules 等) |
| 网络请求 | 仅在通过 npx 安装时下载包,Server 本身无网络通信 | 初始安装时 |

版本管理策略

使用语义化版本(SemVer)管理扩展的版本号:

MAJOR.MINOR.PATCH
  │     │     └─ 修复 Bug,不影响 API(如 1.0.0 → 1.0.1)
  │     └─────── 新增功能,向后兼容(如 1.0.0 → 1.1.0)
  └───────────── 不兼容的 API 变更(如 1.0.0 → 2.0.0)
扩展类型版本管理方式
MCP Serverpackage.json 中的 version 字段 + npm version 命令
Pluginplugin.json 中的 version 字段
SkillMarkdown frontmatter 中可加 version 字段,或依赖 Plugin 的版本号
Hook脚本注释中标注,或随项目的 Git 版本管理

社区贡献指南

发布到社区(ECC 或 MCP Marketplace)时,遵循以下规范:

  1. 先搜索:确认没有功能重复的已有扩展。可以基于已有扩展做改进,但不要直接复制。
  2. 保持简洁:一个扩展只解决一个问题。不要试图把 5 个不相关的功能塞进一个包。
  3. 提供测试:MCP Server 应包含测试用例;Plugin 应包含使用示例。
  4. 响应 Issue:发布后持续维护——回复 Issue、修复 Bug、合并 PR。
  5. 注明依赖:如果你的扩展依赖其他 MCP Server 或 Plugin,在文档中明确说明。

内部团队分发策略

对于团队内部的扩展(不公开发布),推荐以下分发策略:

团队共享仓库(如 team-claude-extensions)
├── mcp-servers/
│   └── internal-api-server/       # 通过私有 npm 或 Git URL 引用
├── plugins/
│   ├── team-code-review/
│   └── deploy-helper/
├── skills/
│   ├── team:react-component.md
│   ├── team:api-endpoint.md
│   └── team:commit-message.md
└── hooks/
    ├── auto-format.sh
    └── security-check.sh

团队内部引用方式:

json
// .claude/settings.json — 通过私有 npm 或 Git URL
{
  "mcpServers": {
    "internal-api": {
      "command": "npx",
      "args": ["-y", "@your-org/internal-api-mcp"]
    }
  }
}
json
// .claude/settings.json — 通过 Git submodule
{
  "plugins": [
    {
      "name": "team-code-review",
      "source": "local",
      "path": ".claude/extensions/plugins/team-code-review"
    }
  ]
}

33.7 本章小结

本章是第七篇"精通篇"中实践性最强的一章——从使用者视角切换到构建者视角,完整覆盖了四种扩展类型的开发全流程。

核心知识回顾

  1. 四种扩展的定位和选择:MCP Server 连接外部系统(npm 包,难度最高),Plugin 打包多种能力(容器,难度中等),Skill 固化工作流(Markdown 文件,难度最低),Hook 实现事件驱动的自动化(脚本,难度较低)。

  2. MCP Server 开发:以 Project Stats 为例,完整实现了 tools/listtools/call 处理器。关键设计原则包括:返回结构化错误(不抛异常)、输入验证、超时保护、性能优化(忽略无关目录)。

  3. Plugin 开发:以 Timestamp Logger 为例,展示了 plugin.json 清单 + Slash Command + Hook + Skill 的组合开发。Plugin 的核心价值在于"打包分发"——将多种能力整合为一个可安装的单元。

  4. Skill 开发:以 API 端点生成器为例,展示了如何在 Skill 中嵌入团队规范(命名约定、目录结构、验证模板、测试要求)。好的 Skill 不只是"告诉 Claude 做什么",而是"把团队规范编码为不可协商的约束"。

  5. Hook 开发:以 Auto Backup 为例,实现了完整的备份-恢复-清理机制。关键设计包括:大文件跳过、JSON 解析文件路径、扁平化备份命名、每日一次清理。

  6. 发布与分享:MCP Server → npm;Plugin → ECC 市场或 GitHub;Skill → Git 共享;Hook → 团队配置。无论哪种发布方式,都需提供 README、权限说明和配置示例。

扩展开发的"黄金法则"

从本章四个实例中可以提炼出四条通用原则:

  1. 错误处理优先:永远不要让扩展崩溃。返回清晰的错误信息,让 Claude 能够自我纠正。
  2. 性能意识:MCP 工具 < 5 秒,Hook < 1 秒,Skill 加载要快。慢扩展会严重影响用户体验。
  3. 权限透明:明确列出扩展需要的所有权限及其理由。模糊的权限需求是安全的红灯。
  4. 先验证再推广:本地测试 → 个人环境验证 → 团队推广。每个阶段都有退出和修正的机会。

下一步

掌握了扩展开发能力后,你不仅能够定制 Claude Code 来适配自己的工作流,还能为团队构建共享的基础设施。第 34 章将讨论如何将 Claude Code——包括你构建的自定义扩展——推广到整个团队,让 AI 辅助开发从个人实践升级为团队能力。


章节小结:本章从"使用者"切换为"构建者"视角,完整覆盖了 Claude Code 四种扩展类型的开发全流程。通过 Project Stats MCP Server(Node.js + MCP SDK)、Timestamp Logger Plugin(plugin.json + Commands + Hooks + Skills)、API 端点生成器 Skill(Markdown + 团队规范编码)、Auto Backup Hook(Shell 脚本 + 备份恢复机制)四个完整实例,带读者亲历了从设计到实现到发布的完整周期。掌握这些能力后,你可以构建自己的扩展生态,将团队的工作流和开发规范编码为可复用的工具。