Skip to content
源码分析手册

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 构建了两条不同的装配链:

  1. 资源装配与 @ 引用 (src/services/mcp/client.ts & src/utils/attachments.ts)

    • 分发与标记:在连接建立时,Claude 会通过 resources/list 获取资源清单,并为每一项打上 server 标签。
    • 人类侧输入src/hooks/useTypeahead.tsx 将资源索引与本地文件并列。你在输入 @ 时,就能看到来自已连接 MCP Server 的资源(如 @github:issue:123)。
    • 模型侧读取:除了手动引用,Claude 还提供了一套通用的 Helper 工具(ListMcpResourcesToolReadMcpResourceTool),允许模型在对话中自主发现并读取这些只读对象。对于二进制资源,ReadMcpResourceTool 会先落盘再返回路径。
  2. 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 都会被全量刷新。
  3. 二进制内容落盘 (src/tools/ReadMcpResourceTool/ReadMcpResourceTool.ts)

    • 对于非文本资源(Blob),Claude Code 不会将其 Base64 原样塞入上下文,而是先写入本地磁盘,并在回复中告知模型文件的存储路径(blobSavedTo)。

别踩这些坑

  • 资源不是工具:它们是只读的。如果你试图通过“读资源”来修改远端状态,那找错了地方,你应该去调用对应的 MCP Tool。
  • Prompt 命令不进工具系统:默认情况下,普通的 MCP Prompt 不会被推送到 SkillTool 中供模型自主发现,它们主要设计给人类通过 Slash Command 手动触发。
  • 参数解析规则:当前版本(2.1.88)对 MCP Prompt 命令参数的处理相对朴素,通常基于空格切分。如果 Prompt 需要多个复杂参数,传递可能存在解析不准的问题。
  • 生命周期绑定:一旦 MCP Server 断开,它名下的资源 @ 引用和 / 命令会瞬间从自动补全和命令集中消失。资源与 Prompt 并不是持久化的缓存,而是随连接状态存在的。

推荐阅读路径

源码锚点

  • claude-code-opensource/src/services/mcp/client.ts — 资源列表拉取与 Prompt 命令注册。
📄 src/services/mcp/client.ts — 资源列表拉取与 Prompt 命令注册。L29-43 of 3349
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 触发点。L37-37 of 1385
tsx
const AT_TOKEN_HEAD_RE = /^@[\p{L}\p{N}\p{M}_\-./\\()[\]~:]*/u;
  • claude-code-opensource/src/utils/attachments.ts — 将 @ 引用转化为消息附件的核心逻辑。
📄 src/utils/attachments.ts — 将 `@` 引用转化为消息附件的核心逻辑。L8-12 of 3998
typescript
  type Tools,
  type ToolUseContext,
  type ToolPermissionContext,
} from '../Tool.js'
import {
  • claude-code-opensource/src/tools/ReadMcpResourceTool/ReadMcpResourceTool.ts — 处理模型发起的异步资源读取请求。
📄 src/tools/ReadMcpResourceTool/ReadMcpResourceTool.ts — 处理模型发起的异步资源读取请求。L22-26 of 159
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` 通知并刷新资源/命令索引。L616-645 of 1142
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,

基于 Claude Code v2.1.88 开源快照的深度分析