第 33 章:自定义扩展开发
第 15-18 章我们学习了 Claude Code 的四大扩展机制——MCP 协议、插件系统、Skills 技能系统和 Hooks 钩子系统。但那四章的核心视角是"使用者":怎么配置、怎么安装、怎么调用。本章切换视角,成为"构建者":从零开始,亲手打造自己的 Claude Code 扩展。
这是第七篇"精通篇"的核心章节之一。使用他人的扩展能让你效率翻倍,而构建自己的扩展能让你自由定制 Claude Code 的行为边界——把团队的工作流、公司的内部工具、你个人的开发偏好,全部编码为可复用的扩展。
本章目标:掌握四种扩展类型(MCP Server、Plugin、Skill、Hook)的完整开发流程,能够独立构建、测试、发布自定义扩展,理解每种扩展的适用场景和开发难度。
33.1 扩展开发生态概览
在动手之前,先建立一个全局视图:四种扩展各自的能力边界是什么?什么场景应该选择哪种扩展?每种扩展的开发门槛有多高?
四种扩展类型总览
四种扩展的核心区别可以总结为一张表:
| 扩展类型 | 本质 | 开发语言 | 运行方式 | 典型产物 | 开发难度 |
|---|---|---|---|---|---|
| MCP Server | 独立进程,暴露工具和资源 | Node.js / Python / 任意语言 | 独立子进程,stdio/HTTP 通信 | npm 包 / Python 包 / 可执行文件 | ⭐⭐⭐⭐ |
| Plugin | 打包的扩展单元,可含多种能力 | 主要 Markdown + 可选 TypeScript | 随 Claude Code 加载 | plugin.json + commands/hooks/skills 目录 | ⭐⭐⭐ |
| Skill | Markdown 文件,可复用的指令模板 | 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
理由:不需要扩展机制,一个命令或一段提示词就够了技能要求对照
| 扩展类型 | 需要掌握的技能 |
|---|---|
| Skill | Markdown 写作能力。如果你能写好一段 Prompt,你就能写 Skill |
| Hook | Shell 脚本基础。能写 if/else、理解 exit code、会用环境变量 |
| Plugin | 理解 plugin.json 结构 + Markdown(命令)+ 可选 TypeScript(复杂钩子) |
| MCP Server | Node.js/Python 编程能力。理解异步编程、JSON Schema、进程间通信 |
建议路径:如果你的团队刚开始构建自定义扩展,从 Skill 开始(成本最低 → 收益最快),然后根据实际需要逐步过渡到 Hook → Plugin → MCP Server。不要一上来就挑战最复杂的 MCP Server 开发。
四种扩展的协作关系
在实际项目中,四种扩展往往组合使用:
一个典型的组合场景: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、作者、时间、消息 |
第一步:初始化项目
mkdir project-stats-mcp && cd project-stats-mcp
pnpm init
# 确保 package.json 中有 "type": "module"
pnpm add @modelcontextprotocol/sdk编辑 package.json,确保包含 ES Module 声明和启动脚本:
{
"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 实现,包含三个工具的全部逻辑:
#!/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. 错误处理策略 —— 返回错误信息而非抛出异常
// ✅ 正确:返回 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. 输入验证 —— 永远不信任参数
// 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. 忽略无关目录 —— 提升性能
const ignoredDirs = new Set([
"node_modules", ".git", ".vitepress", "dist",
"build", ".next", "__pycache__", ".cache", "target"
]);遍历整个项目目录时跳过这些常见的大型依赖目录,可以大幅减少统计时间。在一个有 2 万文件的 node_modules 的项目中,跳过这些目录能将统计时间从 10 秒降到 200 毫秒。
第三步:本地测试
在配置到 Claude Code 之前,先在本地验证 MCP Server 的基本功能:
# 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.jsMCP Inspector 会启动一个 Web UI,让你可以在浏览器中测试 MCP Server 的各个工具——这是开发阶段最有效的调试方式。
第四步:配置到 Claude Code
将以下配置添加到 ~/.claude/settings.json 或 .claude/settings.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 一键使用:
# 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-summary | Slash Command:生成本次会话的工具调用时间线摘要 |
| PostToolUse Hook | 自动记录每次工具调用的时间戳到日志文件 |
timestamp-logger:analyze | Skill:分析日志文件,识别效率瓶颈 |
第一步:创建插件目录结构
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
{
"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:
#!/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:
# /log-summary
生成当前会话的工具调用时间线摘要,帮助回顾本次会话中的操作。
## Steps
1. **读取日志文件**:
读取 `.claude/logs/tool-timestamps.jsonl` 文件。
2. **筛选当前会话**:
只保留 `sessionId` 匹配当前会话 ID 的日志条目。
你可以在执行命令前运行 `echo $CLAUDE_SESSION_ID` 来获取当前会话 ID。
3. **生成摘要**:
按以下格式输出 Markdown 表格:会话工具调用摘要
会话 ID: {sessionId} 总调用次数:
| 时间 | 工具 | 文件 | 状态 |
|---|---|---|---|
| 14:23:01 | Write | src/utils.ts | ✅ |
| 14:23:15 | Bash | — | ✅ |
| 14:24:02 | Edit | src/index.ts | ✅ |
| ... | ... | ... | ... |
4. **统计分布**:
在表格后追加各类工具的使用次数统计:
- 文件操作(Read/Write/Edit)
- 终端执行(Bash)
- 搜索操作(Grep/Glob)
- 其他工具
5. **效率提示**(可选):
如果某类操作占比异常高(如 Read 占比超过 60%),提示用户:
"注意到本次会话中有大量文件读取操作。建议考虑使用 CLAUDE.md 提供更多项目上下文,减少重复读取。"第五步:编写日志分析 Skill
skills/analyze.md:
---
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%(可能缺少代码探索) |第六步:安装与测试
# 1. 将插件目录复制到项目
cp -r timestamp-logger .claude/plugins/
# 2. 在 .claude/settings.json 中注册{
"plugins": [
{
"name": "timestamp-logger",
"source": "local",
"path": ".claude/plugins/timestamp-logger"
}
]
}# 3. 添加日志目录到 .gitignore
echo ".claude/logs/" >> .gitignore重启 Claude Code 后,做几次正常操作(读取文件、编辑代码、执行命令),然后用以下命令测试:
/log-summary
用 timestamp-logger:analyze 分析我的工具使用效率发布到 ECC 社区市场
插件开发完成并经过团队内验证后,可以发布到 ECC 社区市场:
- Fork ECC 仓库:https://github.com/affaan-m/ECC
- 添加插件目录:在 ECC 仓库中创建
plugins/timestamp-logger/,放入完整插件文件 - 编写高质量的 README.md:包含功能介绍、安装步骤、使用示例、权限说明
- 提交 Pull Request:等待社区审核
在 README 中,建议包含以下内容:
# 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:
---
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)
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)
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)
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)
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);
});
});第四步:验证和输出
完成所有文件后:
- 运行类型检查:
pnpm tsc --noEmit - 运行测试:
pnpm vitest run src/api/{resource}/ - 输出变更清单:列出所有创建/修改的文件及其用途
- 提醒用户:如果资源需要数据库迁移,提醒用户创建迁移文件
### 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:
#!/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:
#!/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"
;;
esacHook 配置
在 .claude/settings.json 中注册 Hook:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"command": "bash .claude/hooks/auto-backup.sh"
}
]
}
}# 赋予脚本执行权限
chmod +x .claude/hooks/auto-backup.sh
chmod +x .claude/hooks/restore-backup.sh
# 将备份目录加入 .gitignore
echo ".claude/backups/" >> .gitignore关键设计决策
1. 大文件跳过策略
MAX_BACKUP_SIZE_MB=50不是所有文件都应该备份。大型数据文件、生成的构建产物、锁文件(如 pnpm-lock.yaml)动辄几 MB 到几十 MB,备份它们会快速消耗磁盘空间。50MB 是一个合理的阈值——对大多数源代码文件(通常 < 1MB)无影响,对大文件自动跳过。你可以根据项目情况调整。
2. JSON 解析文件路径
MCP 工具的输入是 JSON 字符串。我们使用 Python 来解析 JSON,因为:
- Bash 原生 JSON 解析能力弱(需要
jq额外安装) - Python 几乎每台开发机都有
safeParse模式:解析失败返回空字符串,不抛异常
如果团队统一安装了 jq,可以改为:
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. 每日一次清理
CLEANUP_FLAG="${BACKUP_ROOT}/.cleanup-${TODAY}"
if [ ! -f "$CLEANUP_FLAG" ]; then
cleanup_old_backups
touch "$CLEANUP_FLAG"
fi清理操作每天只执行一次,而不是每次 Hook 触发都执行。这避免了频繁扫描文件系统带来的性能开销。
测试 Hook
遵循第 18.6 节的最佳实践"测试先行",按以下步骤验证:
# 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 Server | npm / PyPI / GitHub | npx -y package-name | 全球开发者 |
| Plugin | ECC 社区市场 / GitHub | /Manage plugins 或手动安装 | Claude Code 用户 |
| Skill | GitHub(通过 Plugin 分发)或 .claude/skills/ | Git 共享或复制文件 | 团队 / 社区 |
| Hook | 团队 settings.json 或分享脚本片段 | 复制代码到项目 | 团队 |
必备文档清单
无论发布哪种扩展,以下文档是必需的:
| 文档 | 内容要求 | 优先级 |
|---|---|---|
| README.md | 一句话介绍 + 功能列表 + 安装步骤 + 最简示例 | 🔴 必需 |
| 权限说明 | 扩展需要的所有权限及其正当理由 | 🔴 必需 |
| 配置示例 | 完整的 settings.json 配置片段 | 🔴 必需 |
| CHANGELOG.md | 每个版本的变更记录(Breaking Changes 必须注明) | 🟡 推荐 |
| CONTRIBUTING.md | 如何参与贡献(如果是开源项目) | 🟢 可选 |
| LICENSE | 开源许可证 | 🔴 必需 |
权限说明的示例(以 Project Stats MCP Server 为例):
## 权限说明
| 权限 | 原因 | 范围 |
|------|------|------|
| 执行 `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 Server | package.json 中的 version 字段 + npm version 命令 |
| Plugin | plugin.json 中的 version 字段 |
| Skill | Markdown frontmatter 中可加 version 字段,或依赖 Plugin 的版本号 |
| Hook | 脚本注释中标注,或随项目的 Git 版本管理 |
社区贡献指南
发布到社区(ECC 或 MCP Marketplace)时,遵循以下规范:
- 先搜索:确认没有功能重复的已有扩展。可以基于已有扩展做改进,但不要直接复制。
- 保持简洁:一个扩展只解决一个问题。不要试图把 5 个不相关的功能塞进一个包。
- 提供测试:MCP Server 应包含测试用例;Plugin 应包含使用示例。
- 响应 Issue:发布后持续维护——回复 Issue、修复 Bug、合并 PR。
- 注明依赖:如果你的扩展依赖其他 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团队内部引用方式:
// .claude/settings.json — 通过私有 npm 或 Git URL
{
"mcpServers": {
"internal-api": {
"command": "npx",
"args": ["-y", "@your-org/internal-api-mcp"]
}
}
}// .claude/settings.json — 通过 Git submodule
{
"plugins": [
{
"name": "team-code-review",
"source": "local",
"path": ".claude/extensions/plugins/team-code-review"
}
]
}33.7 本章小结
本章是第七篇"精通篇"中实践性最强的一章——从使用者视角切换到构建者视角,完整覆盖了四种扩展类型的开发全流程。
核心知识回顾
四种扩展的定位和选择:MCP Server 连接外部系统(npm 包,难度最高),Plugin 打包多种能力(容器,难度中等),Skill 固化工作流(Markdown 文件,难度最低),Hook 实现事件驱动的自动化(脚本,难度较低)。
MCP Server 开发:以 Project Stats 为例,完整实现了
tools/list和tools/call处理器。关键设计原则包括:返回结构化错误(不抛异常)、输入验证、超时保护、性能优化(忽略无关目录)。Plugin 开发:以 Timestamp Logger 为例,展示了 plugin.json 清单 + Slash Command + Hook + Skill 的组合开发。Plugin 的核心价值在于"打包分发"——将多种能力整合为一个可安装的单元。
Skill 开发:以 API 端点生成器为例,展示了如何在 Skill 中嵌入团队规范(命名约定、目录结构、验证模板、测试要求)。好的 Skill 不只是"告诉 Claude 做什么",而是"把团队规范编码为不可协商的约束"。
Hook 开发:以 Auto Backup 为例,实现了完整的备份-恢复-清理机制。关键设计包括:大文件跳过、JSON 解析文件路径、扁平化备份命名、每日一次清理。
发布与分享:MCP Server → npm;Plugin → ECC 市场或 GitHub;Skill → Git 共享;Hook → 团队配置。无论哪种发布方式,都需提供 README、权限说明和配置示例。
扩展开发的"黄金法则"
从本章四个实例中可以提炼出四条通用原则:
- 错误处理优先:永远不要让扩展崩溃。返回清晰的错误信息,让 Claude 能够自我纠正。
- 性能意识:MCP 工具 < 5 秒,Hook < 1 秒,Skill 加载要快。慢扩展会严重影响用户体验。
- 权限透明:明确列出扩展需要的所有权限及其理由。模糊的权限需求是安全的红灯。
- 先验证再推广:本地测试 → 个人环境验证 → 团队推广。每个阶段都有退出和修正的机会。
下一步
掌握了扩展开发能力后,你不仅能够定制 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 脚本 + 备份恢复机制)四个完整实例,带读者亲历了从设计到实现到发布的完整周期。掌握这些能力后,你可以构建自己的扩展生态,将团队的工作流和开发规范编码为可复用的工具。