本章总览
MCP server 暴露的 prompts 可转换为 Claude Code slash skill(loadedFrom: mcp)。mcpSkillBuilders.ts 提供无环依赖的 builder 注册表;mcpSkills.ts 在 client 连接后 fetch 并构建 Command。fetchMcpSkillsForClient 在当前树中 stub 返回 [],但架构接缝完整。本章重点解释 builder 模式与 SkillTool 对 mcp skill 的过滤。
学完本章你应该能
- 解释 mcpSkillBuilders 为何存在(Bun bunfs + dependency-cruiser)
- 说明 registerMCPSkillBuilders 与 getMCPSkillBuilders 契约
- 理解 MCP prompt → createSkillCommand 字段映射
- 掌握 SkillTool getAllCommands 中 loadedFrom===mcp 过滤
- 区分 MCP prompt 与 MCP tool 在 SkillTool 可达性
核心概念(先读懂这些)
MCP skill 进入 AppState.mcp.commands
MCP client 连接后 discovery 写入 AppState;prompt 类型经 mcpSkills 路径转为 PromptCommand 并标记 loadedFrom=mcp。getCommands() 只 merge local+bundled;SkillTool 额外合并 mcp.commands 中 prompt 且 loadedFrom===mcp 的项。
builder registry 是 dependency-graph leaf
mcpSkillBuilders.ts 只 import type from loadSkillsDir,零 runtime 依赖 loadSkillsDir。loadSkillsDir 模块 eval 末尾 register 实现函数指针。client.ts 可 import mcpSkills 而不形成环。
建议学习步骤
- 阅读 mcpSkillBuilders 头注释(源码块 A)
- 阅读 register/getMCPSkillBuilders(源码块 B)
- 阅读 fetchMcpSkillsForClient stub(源码块 C)
- 阅读 SkillTool getAllCommands mcp 过滤(源码块 D)
- 阅读 loadSkillsDir register 末尾(源码块 E)
- grep loadedFrom mcp 赋值点(源码块 F)
- 确认 disconnect 时 AppState.mcp.commands 清理
常见误区
注意
MCP plain prompt 不应被 SkillTool 猜测名调用——filter 是为收窄可达性
注意
动态 import 非字面量在 bundled binary 失败——勿改 builder 注册方式
注意
mcpSkills stub 不代表生产无逻辑——阅读 client discovery 路径
注意
AppState.mcp.commands 与 getCommands 列表 intentionally 不同
注意
hot-plug MCP server 后下一 turn SkillTool 才见新 skill
注意
createSkillCommand 保证 MCP 与 disk skill frontmatter 语义一致
MCP skill 在命令体系中的位置
MCP Server (external)
→ services/mcp/client connect
→ list prompts / tools
→ AppState.mcp.commands[]
mcpSkills.ts + getMCPSkillBuilders()
→ createSkillCommand({ ..., loadedFrom: 'mcp' })
→ merge into command list for UI / SkillTool
用户 /mcp__server__promptname
模型 SkillTool(command: '...')
LoadedFrom 枚举(loadSkillsDir)含 mcp 第五源。
commands.ts 静态 import loadSkillsDir → 保证 registerMCPSkillBuilders 在任意 MCP 连接前完成。
源码引用: src/skills/loadSkillsDir.ts · 第 67–73 行(共 1087 行)
67| export type LoadedFrom =
68| | 'commands_DEPRECATED'
69| | 'skills'
70| | 'plugin'
71| | 'managed'
72| | 'bundled'
73| | 'mcp'
源码缺失
File not found: c:\Users\123\Desktop\jd\Claude code源码\claude-code-complete\claude-code-complete\src-readable\services\mcp-client.mjs
mcpSkillBuilders 设计
文件头注释(必读)总结三类失败方案:
- 非字面量 dynamic import — Bun bunfs 路径解析失败
- 字面量 dynamic import loadSkillsDir — dependency-cruiser 报大量环
- 静态 import 互引 — client → mcpSkills → loadSkillsDir → … → client
解决方案:write-once registry
export type MCPSkillBuilders = {
createSkillCommand: typeof createSkillCommand
parseSkillFrontmatterFields: typeof parseSkillFrontmatterFields
}
mcpSkills 调用 getMCPSkillBuilders().createSkillCommand(...) 无需 import loadSkillsDir。
源码引用: src/skills/mcpSkillBuilders.ts · 第 6–44 行(共 45 行)
6| /**
7| * Write-once registry for the two loadSkillsDir functions that MCP skill
8| * discovery needs. This module is a dependency-graph leaf: it imports nothing
9| * but types, so both mcpSkills.ts and loadSkillsDir.ts can depend on it
10| * without forming a cycle (client.ts → mcpSkills.ts → loadSkillsDir.ts → …
11| * → client.ts).
12| *
13| * The non-literal dynamic-import approach ("await import(variable)") fails at
14| * runtime in Bun-bundled binaries — the specifier is resolved against the
15| * chunk's /$bunfs/root/… path, not the original source tree, yielding "Cannot
16| * find module './loadSkillsDir.js'". A literal dynamic import works in bunfs
17| * but dependency-cruiser tracks it, and because loadSkillsDir transitively
18| * reaches almost everything, the single new edge fans out into many new cycle
19| * violations in the diff check.
20| *
21| * Registration happens at loadSkillsDir.ts module init, which is eagerly
22| * evaluated at startup via the static import from commands.ts — long before
23| * any MCP server connects.
24| */
25|
26| export type MCPSkillBuilders = {
27| createSkillCommand: typeof createSkillCommand
28| parseSkillFrontmatterFields: typeof parseSkillFrontmatterFields
29| }
30|
31| let builders: MCPSkillBuilders | null = null
32|
33| export function registerMCPSkillBuilders(b: MCPSkillBuilders): void {
34| builders = b
35| }
36|
37| export function getMCPSkillBuilders(): MCPSkillBuilders {
38| if (!builders) {
39| throw new Error(
40| 'MCP skill builders not registered — loadSkillsDir.ts has not been evaluated yet',
41| )
42| }
43| return builders
44| }
registerMCPSkillBuilders 时机
loadSkillsDir.ts 模块末尾(约 1083 行):
registerMCPSkillBuilders({
createSkillCommand,
parseSkillFrontmatterFields,
})
注册发生在 loadSkillsDir.ts module init,通过 commands.ts 静态 import 在 startup 早期 eager eval。
getMCPSkillBuilders() 若 builders 为 null throw Error——诊断 load order regression。
MCP server 连接发生在 REPL 运行期,远晚于 registration。
源码引用: src/skills/loadSkillsDir.ts · 第 65–65 行(共 1087 行)
65| import { registerMCPSkillBuilders } from './mcpSkillBuilders.js'
源码引用: src/skills/mcpSkillBuilders.ts · 第 33–44 行(共 45 行)
33| export function registerMCPSkillBuilders(b: MCPSkillBuilders): void {
34| builders = b
35| }
36|
37| export function getMCPSkillBuilders(): MCPSkillBuilders {
38| if (!builders) {
39| throw new Error(
40| 'MCP skill builders not registered — loadSkillsDir.ts has not been evaluated yet',
41| )
42| }
43| return builders
44| }
fetchMcpSkillsForClient
当前 mcpSkills.ts 全文极短:
export async function fetchMcpSkillsForClient() {
return []
}
表明 v2.1.88 反编译树中 MCP skill fetch 可能:
- 移至 services/mcp/client.ts inline
- 或 feature gate 剥离
架构接缝保留:export 函数名、builder registry、SkillTool mcp merge 仍完整。
阅读生产逻辑应 grep loadedFrom === 'mcp' 与 createSkillCommand 调用点。
源码引用: src/skills/mcpSkills.ts · 第 1–3 行(共 4 行)
1| export async function fetchMcpSkillsForClient() {
2| return []
3| }
SkillTool 对 MCP skill 的合并
getAllCommands(context)(SkillTool.ts):
const mcpSkills = context.getAppState().mcp.commands.filter(
cmd => cmd.type === 'prompt' && cmd.loadedFrom === 'mcp',
)
if (mcpSkills.length === 0) return getCommands(getProjectRoot())
return uniqBy([...localCommands, ...mcpSkills], 'name')
注释:Before this filter, model could invoke MCP prompts via SkillTool if it guessed mcp__server__prompt name。
仅 loadedFrom === 'mcp' 的 prompt 进入 SkillTool 发现列表;plain MCP prompt 不可达。
源码引用: src/tools/SkillTool/SkillTool.ts · 第 77–94 行(共 1109 行)
77| /**
78| * Gets all commands including MCP skills/prompts from AppState.
79| * SkillTool needs this because getCommands() only returns local/bundled skills.
80| */
81| async function getAllCommands(context: ToolUseContext): Promise<Command[]> {
82| // Only include MCP skills (loadedFrom === 'mcp'), not plain MCP prompts.
83| // Before this filter, the model could invoke MCP prompts via SkillTool
84| // if it guessed the mcp__server__prompt name — they weren't discoverable
85| // but were technically reachable.
86| const mcpSkills = context
87| .getAppState()
88| .mcp.commands.filter(
89| cmd => cmd.type === 'prompt' && cmd.loadedFrom === 'mcp',
90| )
91| if (mcpSkills.length === 0) return getCommands(getProjectRoot())
92| const localCommands = await getCommands(getProjectRoot())
93| return uniqBy([...localCommands, ...mcpSkills], 'name')
94| }
createSkillCommand 统一 MCP 与 disk
MCP prompt 转 skill 时复用 createSkillCommand,保证:
- 相同 frontmatter 语义(allowed-tools、model、context:fork)
- 相同 getPromptForCommand 执行链(argument substitution、shell)
- 相同 telemetry skillLoadedEvent
parseSkillFrontmatterFields 允许 MCP metadata 映射到 skill 字段而不重复 parser。
MCP server 断开 → client 从 AppState 移除 commands → SkillTool 下次 getAllCommands 不再包含。
源码引用: src/skills/loadSkillsDir.ts · 第 270–285 行(共 1087 行)
270| export function createSkillCommand({
271| skillName,
272| displayName,
273| description,
274| hasUserSpecifiedDescription,
275| markdownContent,
276| allowedTools,
277| argumentHint,
278| argumentNames,
279| whenToUse,
280| version,
281| model,
282| disableModelInvocation,
283| userInvocable,
284| source,
285| baseDir,
源码引用: src/utils/telemetry/skillLoadedEvent.ts · 第 1–20 行(共 40 行)
1| import { getSkillToolCommands } from '../../commands.js'
2| import {
3| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
4| type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
5| logEvent,
6| } from '../../services/analytics/index.js'
7| import { getCharBudget } from '../../tools/SkillTool/prompt.js'
8|
9| /**
10| * Logs a tengu_skill_loaded event for each skill available at session startup.
11| * This enables analytics on which skills are available across sessions.
12| */
13| export async function logSkillsLoaded(
14| cwd: string,
15| contextWindowTokens: number,
16| ): Promise<void> {
17| const skills = await getSkillToolCommands(cwd)
18| const skillBudget = getCharBudget(contextWindowTokens)
19|
20| for (const skill of skills) {
AppState.mcp.commands 生命周期
MCP server connect → discovery 写入 commands;disconnect → 移除该 server 条目。
SkillTool 每次 call 读 fresh AppState,故 hot-plug MCP server 后模型下一 turn 可见新 skill。
loadedFrom === 'mcp' 标记是 SkillTool 与 slash 补全分界的唯一可靠字段——勿仅用 name 前缀 mcp__ 判断。
源码引用: src/state/AppStateStore.ts · 第 1–30 行(共 570 行)
1| import type { Notification } from 'src/context/notifications.js'
2| import type { TodoList } from 'src/utils/todo/types.js'
3| import type { BridgePermissionCallbacks } from '../bridge/bridgePermissionCallbacks.js'
4| import type { Command } from '../commands.js'
5| import type { ChannelPermissionCallbacks } from '../services/mcp/channelPermissions.js'
6| import type { ElicitationRequestEvent } from '../services/mcp/elicitationHandler.js'
7| import type {
8| MCPServerConnection,
9| ServerResource,
10| } from '../services/mcp/types.js'
11| import { shouldEnablePromptSuggestion } from '../services/PromptSuggestion/promptSuggestion.js'
12| import {
13| getEmptyToolPermissionContext,
14| type Tool,
15| type ToolPermissionContext,
16| } from '../Tool.js'
17| import type { TaskState } from '../tasks/types.js'
18| import type { AgentColorName } from '../tools/AgentTool/agentColorManager.js'
19| import type { AgentDefinitionsResult } from '../tools/AgentTool/loadAgentsDir.js'
20| import type { AllowedPrompt } from '../tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
21| import type { AgentId } from '../types/ids.js'
22| import type { Message, UserMessage } from '../types/message.js'
23| import type { LoadedPlugin, PluginError } from '../types/plugin.js'
24| import type { DeepImmutable } from '../types/utils.js'
25| import {
26| type AttributionState,
27| createEmptyAttributionState,
28| } from '../utils/commitAttribution.js'
29| import type { EffortValue } from '../utils/effort.js'
30| import type { FileHistoryState } from '../utils/fileHistory.js'
client.ts discovery 接缝
MCP client 连接 server 后 listPrompts/listTools 写入 AppState.mcp。mcpSkills 层(或 inline client 逻辑)应把 prompt 转为 createSkillCommand 输出。
当前树 fetchMcpSkillsForClient stub 返回 [],但 SkillTool getAllCommands 仍读 AppState——说明 fetch 可能在 client connect handler 内联。
排查:grep loadedFrom: 'mcp' 赋值点与 mcp.commands.push。
源码引用: src/services/mcp/client.ts · 第 1–30 行(共 3349 行)
1| import { feature } from 'bun:bundle'
2| import type {
3| Base64ImageSource,
4| ContentBlockParam,
5| MessageParam,
6| } from '@anthropic-ai/sdk/resources/index.mjs'
7| import { Client } from '@modelcontextprotocol/sdk/client/index.js'
8| import {
9| SSEClientTransport,
10| type SSEClientTransportOptions,
11| } from '@modelcontextprotocol/sdk/client/sse.js'
12| import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
13| import {
14| StreamableHTTPClientTransport,
15| type StreamableHTTPClientTransportOptions,
16| } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
17| import {
18| createFetchWithInit,
19| type FetchLike,
20| type Transport,
21| } from '@modelcontextprotocol/sdk/shared/transport.js'
22| import {
23| CallToolResultSchema,
24| ElicitRequestSchema,
25| type ElicitRequestURLParams,
26| type ElicitResult,
27| ErrorCode,
28| type JSONRPCMessage,
29| type ListPromptsResult,
30| ListPromptsResultSchema,
MCP prompt 命名与 mcp__ 前缀
磁盘 skill 与 MCP skill 在 Command.name 命名空间共存。uniqBy name 以 local 优先。
MCP server prompt 名常带 server 前缀;SkillTool 过滤 loadedFrom===mcp 确保仅「skill 化」prompt 可模型调用。
用户 slash 补全由 PromptInput 查询 getCommands,MCP prompt 若未 skill 化可能不可见——与 SkillTool 列表 intentionally 不同。
与 mcp-entrypoint server 侧对比
entrypoints/mcp.ts — Claude Code 作为 MCP server 暴露 tools。
skills/mcpSkills — Claude Code 作为 MCP client 消费 外部 server prompts。
方向相反,共用 MCP SDK 类型(agentSdkTypes CallToolResult 等)。
REPL useMergedClients 管理 MCP 连接;skills 模块消费连接产物,不处理 transport。
源码引用: src/entrypoints/mcp.ts · 第 1–10 行(共 197 行)
1| import { Server } from '@modelcontextprotocol/sdk/server/index.js'
2| import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3| import {
4| CallToolRequestSchema,
5| type CallToolResult,
6| ListToolsRequestSchema,
7| type ListToolsResult,
8| type Tool,
9| } from '@modelcontextprotocol/sdk/types.js'
10| import { getDefaultAppState } from 'src/state/AppStateStore.js'
源码引用: src/screens/REPL.tsx · 第 727–727 行(共 7050 行)
727| // cost — otherwise setSearchQuery fills the cache first and warm
本章小结与延伸
mcp-skills = MCP prompt 与 disk skill 的桥梁。builder registry 稳定;fetch 实现随 client 演进。SkillTool 仅 merge loadedFrom===mcp 的 prompt,防止猜测名调用 raw MCP prompt。disconnect 应移除 AppState 条目。 继续学习: