MCP 资源与 Prompt:能力的静态展现与动态模板
除了可执行的“工具”,MCP 协议还定义了“资源” (Resources) 和“Prompt” (Prompts) 两大核心平面。Claude Code 将它们巧妙地适配进了 @ 引用和 / 命令系统。
它解决了什么问题
MCP 资源与 Prompt 本质上是 MCP Server 暴露给 Claude Code 的只读数据面与模板化指令 (Read-only Data Plane & Templated Instructions)。
- 资源 (Resources):是 Server 托管的对象(如代码库的文件内容、API 文档),它们不是为了“执行”,而是为了“被引用”。
- Prompt (Prompts):是 Server 定义的预设指令(如代码审查模板),它们在 Claude Code 中被适配成了 Session 级的
/Slash Command。其本质是远程指令分发,执行时才拉取内容。
运行时的真相
为了将这些外部能力无缝接入,Claude Code 构建了两条不同的装配链:
资源装配与 @ 引用 (src/services/mcp/client.ts & src/utils/attachments.ts):
- 分发与标记:在连接建立时,Claude 会通过
resources/list获取资源清单,并为每一项打上server标签。 - 人类侧输入:
src/hooks/useTypeahead.tsx将资源索引与本地文件并列。你在输入@时,就能看到来自已连接 MCP Server 的资源(如@github:issue:123)。 - 模型侧读取:除了手动引用,Claude 还提供了一套通用的 Helper 工具(
ListMcpResourcesTool和ReadMcpResourceTool),允许模型在对话中自主发现并读取这些只读对象。对于二进制资源,ReadMcpResourceTool会先落盘再返回路径。
- 分发与标记:在连接建立时,Claude 会通过
Prompt 适配与 / 命令 (src/services/mcp/client.ts & src/utils/slashCommandParsing.ts):
- 命令封装:
fetchCommandsForClient并不把 Prompt 注册为工具,而是将其转为type: 'prompt'的命令对象(isMcp: true)。其内部名称通常形如mcp__<server>__<prompt>。 - 执行流转发:当你输入
/server:prompt时,src/utils/processUserInput/processSlashCommand.tsx会解析参数,并反向调用 Server 的prompts/get。返回的内容(Content Block)会作为元数据和用户消息直接注入当前会话回合。 - 实时动态性:Prompt 命令集不是静态的。每当 Server 重连或收到
prompts/list_changed通知时,appState.mcp.commands都会被全量刷新。
- 命令封装:
二进制内容落盘 (src/tools/ReadMcpResourceTool/ReadMcpResourceTool.ts):
- 对于非文本资源(Blob),Claude Code 不会将其 Base64 原样塞入上下文,而是先写入本地磁盘,并在回复中告知模型文件的存储路径(
blobSavedTo)。
- 对于非文本资源(Blob),Claude Code 不会将其 Base64 原样塞入上下文,而是先写入本地磁盘,并在回复中告知模型文件的存储路径(
别踩这些坑
- 资源不是工具:它们是只读的。如果你试图通过“读资源”来修改远端状态,那找错了地方,你应该去调用对应的 MCP Tool。
- Prompt 命令不进工具系统:默认情况下,普通的 MCP Prompt 不会被推送到
SkillTool中供模型自主发现,它们主要设计给人类通过 Slash Command 手动触发。 - 参数解析规则:当前版本(2.1.88)对 MCP Prompt 命令参数的处理相对朴素,通常基于空格切分。如果 Prompt 需要多个复杂参数,传递可能存在解析不准的问题。
- 生命周期绑定:一旦 MCP Server 断开,它名下的资源
@引用和/命令会瞬间从自动补全和命令集中消失。资源与 Prompt 并不是持久化的缓存,而是随连接状态存在的。
推荐阅读路径
- 如果你想了解工具(Actions)是如何与资源协同工作的,请看 MCP 协议与客户端架构。
- 如果你关大量工具存在时的装载性能,请看 MCP 注册表与服务发现(含 Tool Search)。
- 如果你对 Claude Code 的命令解析感兴趣,请看 CLI 启动模式与关键参数:交互与非交互执行。
源码锚点
claude-code-opensource/src/services/mcp/client.ts— 资源列表拉取与 Prompt 命令注册。
📄 src/services/mcp/client.ts — 资源列表拉取与 Prompt 命令注册。
typescript
type ListPromptsResult,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListRootsRequestSchema,
type ListToolsResult,
ListToolsResultSchema,
McpError,
type PromptMessage,
type ResourceLink,
} from '@modelcontextprotocol/sdk/types.js'
import mapValues from 'lodash-es/mapValues.js'
import memoize from 'lodash-es/memoize.js'
import zipObject from 'lodash-es/zipObject.js'
import pMap from 'p-map'
import { getOriginalCwd, getSessionId } from '../../bootstrap/state.js'claude-code-opensource/src/hooks/useTypeahead.tsx—@与/自动补全的 UI 触发点。
📄 src/hooks/useTypeahead.tsx — `@` 与 `/` 自动补全的 UI 触发点。
tsx
const AT_TOKEN_HEAD_RE = /^@[\p{L}\p{N}\p{M}_\-./\\()[\]~:]*/u;claude-code-opensource/src/utils/attachments.ts— 将@引用转化为消息附件的核心逻辑。
📄 src/utils/attachments.ts — 将 `@` 引用转化为消息附件的核心逻辑。
typescript
type Tools,
type ToolUseContext,
type ToolPermissionContext,
} from '../Tool.js'
import {claude-code-opensource/src/tools/ReadMcpResourceTool/ReadMcpResourceTool.ts— 处理模型发起的异步资源读取请求。
📄 src/tools/ReadMcpResourceTool/ReadMcpResourceTool.ts — 处理模型发起的异步资源读取请求。
typescript
export const inputSchema = lazySchema(() =>
z.object({
server: z.string().describe('The MCP server name'),
uri: z.string().describe('The resource URI to read'),
}),claude-code-opensource/src/services/mcp/useManageMCPConnections.ts— 响应list_changed通知并刷新资源/命令索引。
📄 src/services/mcp/useManageMCPConnections.ts — 响应 `list_changed` 通知并刷新资源/命令索引。
typescript
// Register notification handlers for list_changed notifications
// These allow the server to notify us when tools, prompts, or resources change
if (client.capabilities?.tools?.listChanged) {
client.client.setNotificationHandler(
ToolListChangedNotificationSchema,
async () => {
logMCPDebug(
client.name,
`Received tools/list_changed notification, refreshing tools`,
)
try {
// Grab cached promise before invalidating to log previous count
const previousToolsPromise = fetchToolsForClient.cache.get(
client.name,
)
fetchToolsForClient.cache.delete(client.name)
const newTools = await fetchToolsForClient(client)
const newCount = newTools.length
if (previousToolsPromise) {
previousToolsPromise.then(
(previousTools: Tool[]) => {
logEvent('tengu_mcp_list_changed', {
type: 'tools' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
previousCount: previousTools.length,
newCount,
})
},
() => {
logEvent('tengu_mcp_list_changed', {
type: 'tools' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,