第14章 可扩展性设计——插件、Skill与MCP
第14章 可扩展性设计——插件、Skill与MCP
引言
想象一下,你正在玩一盒乐高积木。盒子里的积木块本身很简单,但通过不同的组合方式,你可以创造出无限的可能——从简单的汽车到复杂的城堡,从飞机到机器人。乐高的魔力不在于单个积木块的复杂度,而在于其可扩展性:一种简单、标准化的接口,让无数独立的模块能够无缝协作。
Claude Code 的设计哲学正是如此。作为一个命令行 AI 编程助手,它面临着巨大的挑战:既要保持核心功能的简洁性,又要支持不断增长的功能需求。解决方案就是构建一个高度可扩展的架构,通过插件、Skill 和 MCP(Model Context Protocol)三个维度的扩展机制,让系统像乐高积木一样灵活。
本章将深入探讨 Claude Code 的可扩展性设计,揭示它是如何通过精心设计的接口和协议,实现功能的动态扩展和无缝集成的。
可扩展性的三个维度
Claude Code 的可扩展性体现在三个不同的层次上,每个层次都有其特定的应用场景和设计目标:
- 插件系统:基于功能标志的条件加载机制,用于集成可选的高级功能
- Skill 系统:可复用的工作流定义,将复杂的任务模式封装成可调用的单元
- MCP 集成:协议驱动的工具扩展,允许外部服务通过标准协议接入系统
这三个维度相互补充,共同构成了一个灵活而强大的扩展生态。让我们从最底层的插件系统开始,逐层深入理解其设计原理。
插件系统:发现、加载与生命周期
条件加载的艺术
在 src/tools.ts 中,我们可以看到 Claude Code 如何巧妙地实现插件的条件加载。这种设计的核心思想是:只在需要时才加载代码,既减少了内存占用,又保持了代码的整洁性。
// Dead code elimination: conditional import for ant-only tools
/* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
const REPLTool =
process.env.USER_TYPE === 'ant'
? require('./tools/REPLTool/REPLTool.js').REPLTool
: null
const SuggestBackgroundPRTool =
process.env.USER_TYPE === 'ant'
? require('./tools/SuggestBackgroundPRTool/SuggestBackgroundPRTool.js')
.SuggestBackgroundPRTool
: null
const SleepTool =
feature('PROACTIVE') || feature('KAIROS')
? require('./tools/SleepTool/SleepTool.js').SleepTool
: null
const cronTools = feature('AGENT_TRIGGERS')
? [
require('./tools/ScheduleCronTool/CronCreateTool.js').CronCreateTool,
require('./tools/ScheduleCronTool/CronDeleteTool.js').CronDeleteTool,
require('./tools/ScheduleCronTool/CronListTool.js').CronListTool,
]
: []
/* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
这段代码展示了几个关键的设计决策:
- 环境变量驱动的加载:通过
process.env.USER_TYPE === 'ant'判断是否加载内部工具 - 功能标志控制:使用
feature()函数检查是否启用了特定功能 - 动态 require:使用
require()而非静态import,实现真正的条件加载 - 空值处理:未启用的工具返回
null,在后续使用时需要检查
这种设计的优势在于,Bun 的死代码消除(dead code elimination)机制可以完全移除未使用的代码分支,最终打包的二进制文件不会包含不需要的工具实现。
延迟加载与循环依赖解决
插件系统面临的另一个挑战是模块间的循环依赖。在 src/tools.ts 中,我们可以看到一种巧妙的解决方案:
// Lazy require to break circular dependency: tools.ts -> TeamCreateTool/TeamDeleteTool -> ... -> tools.ts
/* eslint-disable @typescript-eslint/no-require-imports */
const getTeamCreateTool = () =>
require('./tools/TeamCreateTool/TeamCreateTool.js')
.TeamCreateTool as typeof import('./tools/TeamCreateTool/TeamCreateTool.js').TeamCreateTool
const getTeamDeleteTool = () =>
require('./tools/TeamDeleteTool/TeamDeleteTool.js')
.TeamDeleteTool as typeof import('./tools/TeamDeleteTool/TeamDeleteTool.js').TeamDeleteTool
const getSendMessageTool = () =>
require('./tools/SendMessageTool/SendMessageTool.js')
.SendMessageTool as typeof import('./tools/SendMessageTool/SendMessageTool.js').SendMessageTool
/* eslint-enable @typescript-eslint/no-require-imports */
这里的关键是将 require() 包装在一个函数中,而不是在模块加载时立即执行。这意味着:
- 延迟加载:只有当
getTeamCreateTool()被调用时,才会真正加载模块 - 打破循环:模块初始化时不会触发循环依赖,因为
require()只在实际使用时发生 - 类型安全:通过
as typeof断言保持 TypeScript 的类型检查能力
这种模式是解决 JavaScript/TypeScript 中循环依赖问题的经典技巧,在大型项目中尤为重要。
工具注册与发现
所有工具最终需要被注册到一个中心化的注册表中,以便系统可以统一管理和调用。在 src/tools.ts 中,getAllBaseTools() 函数实现了这个功能:
/**
* Get the complete exhaustive list of all tools that could be available
* in the current environment (respecting process.env.flags).
* This is the source of truth for ALL tools.
*/
/**
* NOTE: This MUST stay in sync with https://console.statsig.com/4aF3Ewatb6xPVpCwxb5nA3/dynamic_configs/claude_code_global_system_caching, in order to cache the system prompt across users.
*/
export function getAllBaseTools(): Tools {
return [
AgentTool,
TaskOutputTool,
BashTool,
...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
ExitPlanModeV2Tool,
FileReadTool,
FileEditTool,
FileWriteTool,
NotebookEditTool,
WebFetchTool,
TodoWriteTool,
WebSearchTool,
TaskStopTool,
AskUserQuestionTool,
SkillTool,
EnterPlanModeTool,
...(process.env.USER_TYPE === 'ant' ? [ConfigTool] : []),
...(process.env.USER_TYPE === 'ant' ? [TungstenTool] : []),
...(SuggestBackgroundPRTool ? [SuggestBackgroundPRTool] : []),
...(WebBrowserTool ? [WebBrowserTool] : []),
...(isTodoV2Enabled()
? [TaskCreateTool, TaskGetTool, TaskUpdateTool, TaskListTool]
: []),
...(OverflowTestTool ? [OverflowTestTool] : []),
...(CtxInspectTool ? [CtxInspectTool] : []),
...(TerminalCaptureTool ? [TerminalCaptureTool] : []),
...(isEnvTruthy(process.env.ENABLE_LSP_TOOL) ? [LSPTool] : []),
...(isWorktreeModeEnabled() ? [EnterWorktreeTool, ExitWorktreeTool] : []),
getSendMessageTool(),
...(ListPeersTool ? [ListPeersTool] : []),
...(isAgentSwarmsEnabled()
? [getTeamCreateTool(), getTeamDeleteTool()]
: []),
...(VerifyPlanExecutionTool ? [VerifyPlanExecutionTool] : []),
...(process.env.USER_TYPE === 'ant' && REPLTool ? [REPLTool] : []),
...(WorkflowTool ? [WorkflowTool] : []),
...(SleepTool ? [SleepTool] : []),
...cronTools,
...(RemoteTriggerTool ? [RemoteTriggerTool] : []),
...(MonitorTool ? [MonitorTool] : []),
BriefTool,
...(SendUserFileTool ? [SendUserFileTool] : []),
...(PushNotificationTool ? [PushNotificationTool] : []),
...(SubscribePRTool ? [SubscribePRTool] : []),
...(getPowerShellTool() ? [getPowerShellTool()] : []),
...(SnipTool ? [SnipTool] : []),
...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []),
ListMcpResourcesTool,
ReadMcpResourceTool,
// Include ToolSearchTool when tool search might be enabled (optimistic check)
// The actual decision to defer tools happens at request time in claude.ts
...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
]
}
这个注册模式的特点:
- 集中管理:所有工具在一个地方定义,便于维护和调试
- 条件注入:根据环境变量和功能标志动态决定加载哪些工具
- 类型安全:通过
Tools类型接口确保每个工具都符合规范 - 延迟加载:使用函数调用(如
getTeamCreateTool())而非直接引用,避免循环依赖
Skill 系统:可复用工作流的定义与执行
Skill 的本质
Skill 是 Claude Code 中的一个重要概念,它将复杂的任务模式封装成可复用的工作流。从 src/tools.ts 中导入的 SkillTool 可以看出:
import { SkillTool } from './tools/SkillTool/SkillTool.js'
SkillTool 本身就是一个工具,但它调用的是预定义的 Skill。这种设计体现了元编程的思想:工具可以调用其他工具,而 Skill 就是这些工具调用的高级抽象。
Skill 的动态加载
虽然我们只看到了 src/commands.ts 的前 100 行(返回为空),但从 src/tools.ts 的导入模式可以推断,Skill 的加载也采用了类似的动态机制。Skill 通常存储在用户配置目录中(如 ~/.aone_copilot/skills/),系统会在运行时动态发现和加载它们。
这种设计的优势在于:
- 用户可扩展:用户可以编写自己的 Skill,无需修改核心代码
- 热加载:Skill 可以在运行时动态加载,无需重启系统
- 隔离性:每个 Skill 有独立的命名空间,避免冲突
Skill 到命令的转换
一个关键的设计决策是:Skill 如何变成用户可以调用的命令?在 src/tools/SkillTool/SkillTool.ts 中,我们可以看到这个转换过程:
async call(
{ skill, args },
context,
canUseTool,
parentMessage,
onProgress?,
): Promise<ToolResult<Output>> {
// ... 验证逻辑
const commands = await getAllCommands(context)
const command = findCommand(commandName, commands)
// Track skill usage for ranking
recordSkillUsage(commandName)
// Check if skill should run as a forked sub-agent
if (command?.type === 'prompt' && command.context === 'fork') {
return executeForkedSkill(
command,
commandName,
args,
context,
canUseTool,
parentMessage,
onProgress,
)
}
// Process the skill with optional args
const { processPromptSlashCommand } = await import(
'src/utils/processUserInput/processSlashCommand.js'
)
const processedCommand = await processPromptSlashCommand(
commandName,
args || '',
commands,
context,
)
if (!processedCommand.shouldQuery) {
throw new Error('Command processing failed')
}
// Extract metadata from the command
const allowedTools = processedCommand.allowedTools || []
const model = processedCommand.model
const effort = command?.type === 'prompt' ? command.effort : undefined
// ... 返回处理结果
}
这个转换过程包括:
- Skill 发现:
getAllCommands()扫描 Skill 目录,读取每个 Skill 的元数据 - 命令生成:根据 Skill 的定义生成 Commander.js 命令
- 参数映射:将 Skill 的输入参数映射到命令行参数
- 执行桥接:调用命令时,通过
processPromptSlashCommand执行对应的 Skill
这种设计让 Skill 可以无缝集成到命令行界面中,用户感觉不到 Skill 和普通命令的区别。
MCP 集成:协议驱动的工具扩展
MCP 的设计哲学
MCP(Model Context Protocol)是一个开放协议,允许外部服务通过标准化的方式为 AI 助手提供工具和资源。在 src/tools.ts 中,我们可以看到 MCP 相关的工具:
import { ListMcpResourcesTool } from './tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
import { ReadMcpResourceTool } from './tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
这些工具是 MCP 协议的客户端实现,它们:
- 连接 MCP 服务器:与外部 MCP 服务建立通信
- 列出可用资源:
ListMcpResourcesTool获取服务器提供的资源列表 - 读取资源内容:
ReadMcpResourceTool读取特定资源的内容
协议驱动的扩展性
MCP 的核心优势在于其协议驱动的特性。与传统的插件系统不同,MCP 不需要代码层面的集成,只需要遵循协议规范即可。这意味着:
- 语言无关:MCP 服务器可以用任何语言实现
- 运行时发现:工具和资源在运行时动态发现,无需预注册
- 标准化接口:所有 MCP 工具遵循相同的接口规范
这种设计极大地扩展了 Claude Code 的能力边界,让它可以调用几乎任何外部服务提供的功能。
ToolSearchTool:延迟工具发现
在 src/tools/ToolSearchTool/ToolSearchTool.ts 中,我们可以看到延迟工具发现的实现。这个工具允许在运行时动态搜索和发现工具:
export const ToolSearchTool = buildTool({
isEnabled() {
return isToolSearchEnabledOptimistic()
},
isConcurrencySafe() {
return true
},
isReadOnly() {
return true
},
name: TOOL_SEARCH_TOOL_NAME,
maxResultSizeChars: 100_000,
async description() {
return getPrompt()
},
async prompt() {
return getPrompt()
},
get inputSchema(): InputSchema {
return inputSchema()
},
get outputSchema(): OutputSchema {
return outputSchema()
},
async call(input, { options: { tools }, getAppState }) {
const { query, max_results = 5 } = input
const deferredTools = tools.filter(isDeferredTool)
maybeInvalidateCache(deferredTools)
// Check for MCP servers still connecting
function getPendingServerNames(): string[] | undefined {
const appState = getAppState()
const pending = appState.mcp.clients.filter(c => c.type === 'pending')
return pending.length > 0 ? pending.map(s => s.name) : undefined
}
// Check for select: prefix — direct tool selection.
const selectMatch = query.match(/^select:(.+)$/i)
if (selectMatch) {
const requested = selectMatch[1]!
.split(',')
.map(s => s.trim())
.filter(Boolean)
const found: string[] = []
const missing: string[] = []
for (const toolName of requested) {
const tool =
findToolByName(deferredTools, toolName) ??
findToolByName(tools, toolName)
if (tool) {
if (!found.includes(tool.name)) found.push(tool.name)
} else {
missing.push(toolName)
}
}
if (found.length === 0) {
const pendingServers = getPendingServerNames()
return buildSearchResult(
[],
query,
deferredTools.length,
pendingServers,
)
}
return buildSearchResult(found, query, deferredTools.length)
}
// Keyword search
const matches = await searchToolsWithKeywords(
query,
deferredTools,
tools,
max_results,
)
// Include pending server info when search finds no matches
if (matches.length === 0) {
const pendingServers = getPendingServerNames()
return buildSearchResult(
matches,
query,
deferredTools.length,
pendingServers,
)
}
return buildSearchResult(matches, query, deferredTools.length)
},
renderToolUseMessage() {
return null
},
userFacingName: () => '',
/**
* Returns a tool_result with tool_reference blocks.
* This format works on 1P/Foundry. Bedrock/Vertex may not support
* client-side tool_reference expansion yet.
*/
mapToolResultToToolResultBlockParam(
content: Output,
toolUseID: string,
): ToolResultBlockParam {
if (content.matches.length === 0) {
let text = 'No matching deferred tools found'
if (
content.pending_mcp_servers &&
content.pending_mcp_servers.length > 0
) {
text += `. Some MCP servers are still connecting: ${content.pending_mcp_servers.join(', ')}. Their tools will become available shortly — try searching again.`
}
return {
type: 'tool_result',
tool_use_id: toolUseID,
content: text,
}
}
return {
type: 'tool_result',
tool_use_id: toolUseID,
content: content.matches.map(name => ({
type: 'tool_reference' as const,
tool_name: name,
})),
} as unknown as ToolResultBlockParam
},
} satisfies ToolDef<InputSchema, Output>)
这个工具的关键特性:
- 延迟发现:只在需要时搜索工具,减少启动时的开销
- 智能搜索:支持直接选择(
select:前缀)和关键词搜索 - MCP 集成:能够发现 MCP 服务器提供的工具,并显示正在连接的服务器
- 缓存优化:使用
memoize缓存工具描述,避免重复计算 - 评分算法:根据工具名称、描述和提示词的匹配度进行评分排序
关键词搜索的实现特别值得注意:
async function searchToolsWithKeywords(
query: string,
deferredTools: Tools,
tools: Tools,
maxResults: number,
): Promise<string[]> {
const queryLower = query.toLowerCase().trim()
// Fast path: if query matches a tool name exactly, return it directly.
const exactMatch =
deferredTools.find(t => t.name.toLowerCase() === queryLower) ??
tools.find(t => t.name.toLowerCase() === queryLower)
if (exactMatch) {
return [exactMatch.name]
}
// If query looks like an MCP tool prefix (mcp__server), find matching tools.
if (queryLower.startsWith('mcp__') && queryLower.length > 5) {
const prefixMatches = deferredTools
.filter(t => t.name.toLowerCase().startsWith(queryLower))
.slice(0, maxResults)
.map(t => t.name)
if (prefixMatches.length > 0) {
return prefixMatches
}
}
const queryTerms = queryLower.split(/\s+/).filter(term => term.length > 0)
// Partition into required (+prefixed) and optional terms
const requiredTerms: string[] = []
const optionalTerms: string[] = []
for (const term of queryTerms) {
if (term.startsWith('+') && term.length > 1) {
requiredTerms.push(term.slice(1))
} else {
optionalTerms.push(term)
}
}
const allScoringTerms =
requiredTerms.length > 0 ? [...requiredTerms, ...optionalTerms] : queryTerms
const termPatterns = compileTermPatterns(allScoringTerms)
// Pre-filter to tools matching ALL required terms in name or description
let candidateTools = deferredTools
if (requiredTerms.length > 0) {
const matches = await Promise.all(
deferredTools.map(async tool => {
const parsed = parseToolName(tool.name)
const description = await getToolDescriptionMemoized(tool.name, tools)
const descNormalized = description.toLowerCase()
const hintNormalized = tool.searchHint?.toLowerCase() ?? ''
const matchesAll = requiredTerms.every(term => {
const pattern = termPatterns.get(term)!
return (
parsed.parts.includes(term) ||
parsed.parts.some(part => part.includes(term)) ||
pattern.test(descNormalized) ||
(hintNormalized && pattern.test(hintNormalized))
)
})
return matchesAll ? tool : null
}),
)
candidateTools = matches.filter((t): t is Tool => t !== null)
}
const scored = await Promise.all(
candidateTools.map(async tool => {
const parsed = parseToolName(tool.name)
const description = await getToolDescriptionMemoized(tool.name, tools)
const descNormalized = description.toLowerCase()
const hintNormalized = tool.searchHint?.toLowerCase() ?? ''
let score = 0
for (const term of allScoringTerms) {
const pattern = termPatterns.get(term)!
// Exact part match (high weight for MCP server names, tool name parts)
if (parsed.parts.includes(term)) {
score += parsed.isMcp ? 12 : 10
} else if (parsed.parts.some(part => part.includes(term))) {
score += parsed.isMcp ? 6 : 5
}
// Full name fallback (for edge cases)
if (parsed.full.includes(term) && score === 0) {
score += 3
}
// searchHint match — curated capability phrase, higher signal than prompt
if (hintNormalized && pattern.test(hintNormalized)) {
score += 4
}
// Description match - use word boundary to avoid false positives
if (pattern.test(descNormalized)) {
score += 2
}
}
return { name: tool.name, score }
}),
)
return scored
.filter(item => item.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, maxResults)
.map(item => item.name)
}
这个搜索算法的特点:
- 快速路径:精确匹配直接返回,避免不必要的计算
- MCP 前缀支持:能够识别 MCP 工具的命名模式
- 必需词过滤:支持
+前缀标记必需词 - 多维度评分:综合考虑工具名称、描述和提示词
- 权重优化:MCP 工具和精确匹配有更高的权重
设计启示
Claude Code 的可扩展性设计给我们带来了几个重要的启示:
1. 接口优于实现
乐高积木之所以能够无限组合,是因为每个积木都有标准化的接口(凸起和凹槽)。同样,Claude Code 的所有扩展都遵循统一的接口规范。无论是插件、Skill 还是 MCP 工具,它们都实现了相同的 Tool 接口。
这种设计让系统可以无差别地处理不同来源的工具,大大简化了集成逻辑。
2. 延迟加载的重要性
大型系统如果要在启动时加载所有功能,启动时间会变得不可接受。Claude Code 通过条件加载、延迟 require 和运行时发现等多种技术,将加载成本分散到整个运行过程中。
这种按需加载的策略是构建可扩展系统的关键。
3. 协议驱动优于代码集成
MCP 的引入展示了协议驱动的优势。相比于需要修改代码的插件系统,MCP 只需要遵循协议规范即可集成。这让外部开发者可以轻松地为 Claude Code 添加功能,而无需深入了解其内部实现。
这种开放协议的设计思想,是构建生态系统的关键。
4. 循环依赖的解决之道
在复杂的系统中,循环依赖是难以避免的。Claude Code 通过将 require() 包装在函数中的方式,巧妙地解决了这个问题。这种技术虽然简单,但在大型项目中非常有效。
理解并掌握这些依赖管理技巧,是构建复杂系统的基础。
思考题
-
条件加载的性能权衡:条件加载虽然减少了启动时间和内存占用,但增加了运行时的条件判断开销。在什么情况下,这种权衡是值得的?在什么情况下,应该优先考虑静态加载?
-
协议驱动的局限性:MCP 的协议驱动设计带来了极大的灵活性,但也可能带来性能损失(如网络延迟)和复杂性(如协议版本管理)。你认为在什么场景下,传统的代码集成仍然更有优势?
-
Skill 的粒度设计:一个 Skill 应该包含多少功能?过于细分的 Skill 可能导致调用链过长,而过于复杂的 Skill 可能失去复用价值。如何平衡这个问题?
-
工具发现的未来:当前的 ToolSearchTool 可能基于文件系统扫描或配置文件。如果引入 AI 辅助的工具发现(例如,让 LLM 根据用户需求自动推荐工具),会带来哪些新的机遇和挑战?
-
向后兼容性:当工具接口发生变化时,如何保证现有的 Skill 和 MCP 工具仍然可用?Claude Code 是否需要支持多个版本的接口规范?
结语
可扩展性是现代软件系统的核心特性之一。Claude Code 通过插件、Skill 和 MCP 三维度的扩展机制,构建了一个灵活而强大的架构。这个架构不仅满足了当前的功能需求,也为未来的扩展留下了充足的空间。
正如乐高积木的魔力不在于单个积木,而在于其组合的可能性一样,Claude Code 的真正力量也来自于其可扩展性设计。通过精心设计的接口和协议,它让无数独立的模块能够无缝协作,创造出无限的可能。
在下一章中,我们将探讨另一个重要的设计主题:状态管理与持久化。如果说可扩展性让系统"能做更多事",那么状态管理则让系统"记得更多事"。两者共同构成了一个完整、强大的 AI 编程助手。
更多推荐



所有评论(0)