连载12- Cluade code 的MCP 到底还用不用

MCP 深度解析:从源码理解模型上下文协议的设计、争议与未来

AI Coding 系列第 11 篇 · 外部集成


这篇文章讲到最后只有一句话:MCP 不是"AI 的 USB 接口",它是一个把外部工具包装成 Claude 统一工具池成员的 JSON-RPC 协议。 你在后面几节遇到的每个"为什么 MCP 这样做"、"为什么社区对它又爱又恨",答案都不在 Anthropic 的市场材料里------在 services/mcp/client.tsconnect() 逻辑和 tools/MCPTool/MCPTool.tsbuildTool() 包装器里。带着这句话往下读,这篇一万多字的源码分析会变成它的验证过程。


如果你刚接触 MCP,2026 年的言论环境足够让你困惑:官方说"能用 CLI 就直接用",安全圈说协议有设计级漏洞,Perplexity 因为 token 消耗放弃 MCP,企业团队却说 MCP 是多 Agent 治理的必选项。每种声音都有道理,但拼在一起像吵架------新手很容易得出一个错误的结论:"这东西是不是快死了,我学它干嘛?"

不要焦虑。这些矛盾不是 MCP 快死了的信号,是它从"新玩具"变成"基础设施"的过程中必然经历的应力测试。 每一种批评背后都有具体的技术原因------token 为什么吃得多?漏洞出在协议哪一层?官方为什么说不一定要用?------这些问题的答案不在那些文章的标题里,在 MCP 的源码里。

这篇文章从源码层面拆解 MCP 的运行机制,让你穿透噪音,看到它到底是什么、怎么运作、什么时候该用、什么时候不该用。读完你会有一个确定的判断框架------不是"MCP 好"或"MCP 坏",而是"我的场景下用 MCP 划不划算"。

结构是先建立技术地基,再基于地基做判断。 前四节讲 MCP 怎么传、怎么跑、怎么配------你理解这些之后,第五节的安全争议和社区风暴就不再是"又一个安全漏洞新闻",而是"这个设计选择必然导致的后果"。最后三节(决策框架、失败模式、实战案例)是你在项目里做选择时可以翻阅的参考手册。


一、先建立正确的心智模型

很多人把 MCP 理解成"AI 应用的 USB 协议"------这是 Anthropic 官方的比喻。它打动人心,但不准确。

更准确的说法是:MCP 是一个基于 JSON-RPC 2.0 的客户端-服务器协议,Claude Code 作为客户端通过五种传输通道之一连接到 MCP Server,获取该 Server 提供的工具(Tools)、资源(Resources)、提示(Prompts),然后将这些工具以 mcp__{server}__{tool} 的命名格式注入到 Claude 的统一工具池中。

看一眼 client.ts 最核心的依赖导入就能验证这个模型:
📂 展开源码:MCP 客户端导入------协议的本质

typescript 复制代码
// src/services/mcp/client.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import {
  SSEClientTransport,
} from '@modelcontextprotocol/sdk/client/sse.js'
import {
  StreamableHTTPClientTransport,
} from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import {
  CallToolResultSchema,
  ListToolsResultSchema,
  ListPromptsResultSchema,
  ListResourcesResultSchema,
} from '@modelcontextprotocol/sdk/types.js'

关键细节:

  • 所有 MCP 通信的"语言"都是 JSON-RPC------CallToolResultSchemaListToolsResultSchema 这些 Zod Schema 定义了 MCP Server 和 Client 之间消息的格式契约。
  • 四种 ClientTransport 各解决一类通信场景:子进程管道(stdio)、HTTP 流(StreamableHTTP)、服务器推送(SSE)、IDE 桥接(WebSocket)。再加上第五种 In-Process Transport(见第二节),一共五种。
  • MCP Client 的职责是"翻译"------把 JSON-RPC 消息翻译成 Claude API 兼容的 tool 定义格式,再把工具调用结果翻译回模型能消费的文本块。

MCP 不是魔法,是一层薄薄的适配。它解决的问题是:"每个外部系统都有自己的 API 格式,Claude Code 能不能用统一的语言和它们对话?"------答案就是 JSON-RPC 加命名约定。

这层薄适配有一个核心张力贯穿全文:标准化的收益随复用次数增长,而中间层的成本随调用链长度增长。 MCP 的所有争议------从"该不该用"到"安全不安全"------根源都在这个张力上。前四节把这个架构拆开,你自然会看到张力从哪里来。


二、五种传输方式:从源码看各有什么取舍

MCP 协议本身只定义了 JSON-RPC 消息格式,不管消息怎么传输。Claude Code 支持五种传输方式,选择逻辑在 types.ts 的 Zod Schema 里就可以看出来。理解传输层是理解 MCP 安全争议的前提------2026 年 4 月的 OX Security 漏洞(第五节详述)就出在 Stdio 的设计上。

Stdio------最经典,也最有争议

typescript 复制代码
// src/services/mcp/types.ts
export const McpStdioServerConfigSchema = lazySchema(() =>
  z.object({
    type: z.literal('stdio').optional(), // 可省略,向后兼容
    command: z.string().min(1),          // 启动命令
    args: z.array(z.string()).default([]),
    env: z.record(z.string(), z.string()).optional(),
  }),
)

Stdio 启动一个子进程,通过标准输入/输出管道传递 JSON-RPC 消息。

arduino 复制代码
Claude Code  →  stdin  →  MCP Server 子进程
Claude Code  ←  stdout ←  MCP Server 子进程
Claude Code  ←  stderr ←  MCP Server 日志

这是最古老、最简单、社区最常用的方式------约 70% 的 MCP server 配置都是 Stdio。但正是这种方式存在一个架构级隐患,详见第五节。

SSE------服务器推送

typescript 复制代码
export const McpSSEServerConfigSchema = lazySchema(() =>
  z.object({
    type: z.literal('sse'),
    url: z.string(),
    headers: z.record(z.string(), z.string()).optional(),
  }),
)

SSE(Server-Sent Events)是 HTTP 的单向流:客户端发 HTTP 请求,服务器通过长连接持续推送事件。适合"结果可能需要等很久"的场景,但单向设计意味着客户端必须通过另一条 HTTP 连接发送请求。逐步被 Streamable HTTP 取代。

Streamable HTTP------双向流,2026 路线图主力

typescript 复制代码
export const McpHTTPServerConfigSchema = lazySchema(() =>
  z.object({
    type: z.literal('http'),
    url: z.string(),
    headers: z.record(z.string(), z.string()).optional(),
    oauth: McpOAuthConfigSchema().optional(),
  }),
)

Streamable HTTP 支持 OAuth 2.1 认证和双向流式传输,是 Anthropic 2026 路线图的重点方向。远程部署的 MCP server 首选这种传输。

WebSocket------IDE 桥接

WebSocket 用于 VS Code SDK MCP server------通过长连接把 IDE 能力(获取诊断、打开文件、终端操作)暴露给 Claude Code。支持的 TLS 客户端证书和 HTTP 代理配置让它适合企业内网环境。

In-Process------零延迟的进程内通信

这是最值得细品的传输方式:
📂 展开源码:InProcessTransport 完整实现(57行)

typescript 复制代码
// src/services/mcp/InProcessTransport.ts
class InProcessTransport implements Transport {
  private peer: InProcessTransport | undefined
  private closed = false

  onclose?: () => void
  onerror?: (error: Error) => void
  onmessage?: (message: JSONRPCMessage) => void

  _setPeer(peer: InProcessTransport): void {
    this.peer = peer
  }

  async start(): Promise<void> {}

  async send(message: JSONRPCMessage): Promise<void> {
    if (this.closed) {
      throw new Error('Transport is closed')
    }
    // 异步投递到对端,避免同步请求/响应导致的栈溢出
    queueMicrotask(() => {
      this.peer?.onmessage?.(message)
    })
  }

  async close(): Promise<void> {
    if (this.closed) return
    this.closed = true
    this.onclose?.()
    if (this.peer && !this.peer.closed) {
      this.peer.closed = true
      this.peer.onclose?.()
    }
  }
}

export function createLinkedTransportPair(): [Transport, Transport] {
  const a = new InProcessTransport()
  const b = new InProcessTransport()
  a._setPeer(b)
  b._setPeer(a)
  return [a, b]
}

createLinkedTransportPair() 创建一对互相连接的内存传输通道------A 的 send() 投递到 B 的 onmessage,反之亦然。零网络开销,零序列化延迟,零进程启动时间。

queueMicrotask 的选择很精妙。如果 send() 直接同步调用 peer.onmessage(),而 onmessage 的回调里又立即 send() 响应------这会形成同步递归,栈溢出。queueMicrotask 把每次投递放到下一个微任务队列里打断了调用链。为什么不是 setTimeout(fn, 0)?微任务在事件循环的同一次迭代中执行,延迟远小于宏任务。

传输方式 延迟 适用场景 安全风险
Stdio 低(进程启动有开销) 本地 MCP server 命令注入面(第五节)
SSE 远程单向流(逐步淘汰) HTTP 劫持
Streamable HTTP 远程双向流 + OAuth 2.1 依赖 TLS 配置
WebSocket IDE 桥接、企业内网 长连接劫持
In-Process SDK 嵌入、插件内嵌 无,不跨进程

三、MCP 工具的完整生命周期:从 .mcp.json 到模型调用结果

一个 MCP 工具从你在 .mcp.json 里写下一行配置,到 Claude 模型实际调用它,中间经历了五个阶段。每个阶段都有对应的源码锚点。

复制代码
配置解析 → 客户端连接 → 工具发现与包装 → 工具池合并 → 模型调用与结果返回

阶段 1:配置解析

typescript 复制代码
// src/services/mcp/config.ts
export async function getClaudeCodeMcpConfigs(): Promise<{
  servers: Record<string, ScopedMcpServerConfig>
}> {
  // 企业配置独占检查
  if (doesEnterpriseMcpConfigExist()) {
    return { servers: filtered, errors: [] }
  }

  // 按优先级合并:插件 < 用户 < 项目 < 本地
  const configs = Object.assign(
    {},
    dedupedPluginServers,
    userServers,
    approvedProjectServers,
    localServers,
  )
}

七种配置作用域的完整说明见第四节。

阶段 2:客户端连接

配置文件里的 command/url 被翻译成对应的 Transport 实例。Stdio 启动子进程,HTTP 发起连接请求,In-Process 创建配对的传输通道。连接后,MCP Client 和 Server 完成 JSON-RPC 握手------Client 发送 initialize 请求,Server 返回能力和协议版本。

连接状态有五种:PendingConnectedFailedNeedsAuthDisabled。状态转换由 JSON-RPC 的响应码和异常驱动,MCPConnectionManager(React Context)在 UI 层暴露 reconnectMcpServertoggleMcpServer/mcp 命令和设置面板。

阶段 3:工具发现与包装

连接成功后,Client 调用 listTools() 获取 Server 提供的工具列表:

typescript 复制代码
// 连接后立即列出工具
const toolsResult: ListToolsResult = await client.listTools()

每个工具的原始名称(如 create_issue)被规范化为 mcp__github__create_issue

typescript 复制代码
// src/services/mcp/mcpStringUtils.ts
export function buildMcpToolName(serverName: string, toolName: string): string

这个 mcp__ 前缀不是装饰------它让 MCP 工具和内置工具在同一个命名空间里不冲突,也让权限过滤规则能一刀切地识别所有 MCP 工具。

然后,每个 MCP 工具被包装成 MCPTool 实例:

typescript 复制代码
// src/tools/MCPTool/MCPTool.ts
export const MCPTool = buildTool({
  isMcp: true,
  name: 'mcp',       // 运行时动态覆盖为真实工具名
  maxResultSizeChars: 100_000,
  // description(), prompt(), call(), userFacingName() 都在 mcpClient.ts 中运行时覆盖
})

namedescriptionpromptcalluserFacingName 这五个属性全部标了 Override in mcpClient.ts。MCPTool 本身只是一个"骨架"------真正的内容(工具描述、输入 JSON Schema、实际调用逻辑)由 mcpClient.ts 在连接后动态注入。这不是设计缺陷,是 MCP 的"动态发现"特征决定的:连接前你不知道一个 MCP server 提供了哪些工具,所以只能用骨架占位,连接后再填肉。

阶段 4:工具池合并

typescript 复制代码
// src/tools.ts
export function assembleToolPool(
  permissionContext: ToolPermissionContext,
  mcpTools: Tools,
): Tools {
  const builtInTools = getTools(permissionContext)
  const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)

  // 排序保证 prompt cache 稳定性
  const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
  return uniqBy(
    [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
    'name',
  )
}

内置工具和 MCP 工具统一排序、去重后合并。名称冲突时 uniqBy 保留先出现的------内置工具优先(因为它在数组前面)。排序用 localeCompare 而非简单的字符比较,保证了工具列表的可预测顺序,进而保证了 prompt cache 的稳定性。

阶段 5:模型调用与结果返回

当 Claude 决定调用 mcp__github__create_issue 时,Claude Code 解析出 server 名和工具名,通过 MCP Client 发送 JSON-RPC tools/call 请求,Server 执行后返回结果。结果经过截断(默认 100,000 字符上限)、格式化、持久化(超大结果写入磁盘而非塞进上下文),最终以 tool_result 类型的 content block 返回给模型。
📂 展开源码:MCP 工具结果截断逻辑

typescript 复制代码
// src/utils/mcpValidation.ts
export function truncateMcpContentIfNeeded(
  content: MCPToolResult,
  maxSizeChars: number,
): MCPToolResult {
  // 超过上限的内容写入磁盘,返回文件路径而非内容
  if (mcpContentNeedsTruncation(content, maxSizeChars)) {
    return persistBinaryContent(content)
  }
  return content
}

这个设计避免了 MCP 工具返回 500KB 的数据库查询结果塞满 Claude 的上下文窗口。

Agent 专属 MCP:子代理为什么需要自己的 MCP Server

生命周期章节里有一个容易被忽略但非常关键的源码细节------Sub-agent 可以有自己的 MCP server,独立于父 Agent:
📂 展开源码:Agent frontmatter 中的 mcpServers 字段

typescript 复制代码
// src/tools/AgentTool/loadAgentsDir.ts
const AgentMcpServerSpecSchema = lazySchema(() =>
  z.union([
    z.string(),                              // 按名称引用已有 MCP server
    z.record(z.string(), McpServerConfigSchema()), // 内联定义新 MCP server
  ]),
)

// 在 Agent 定义中
mcpServers: z.array(AgentMcpServerSpecSchema()).optional(),

一个 Agent 的 frontmatter 可以写:

yaml 复制代码
mcpServers:
  - github           # 引用父 Agent 已有的 server(共享连接)
  - my-custom-db:     # 内联定义 Agent 专属 server(新建连接,Agent 结束自动清理)
      command: node
      args: [".mcp/db-server.js"]

initializeAgentMcpServers() 在 Agent 启动时执行------如果 agent 只引用已有 server 名称,就复用父 Agent 的连接;如果是内联定义,就创建新连接并在 Agent 清理阶段自动关闭。这个设计让"代码审查 Agent 能访问 GitHub"、"数据分析 Agent 能访问数据库"各自有各自的工具集,互不污染。


四、MCP 配置体系:七种作用域,谁覆盖谁

Claude Code 的 MCP 配置不是只有一个 .mcp.json 文件那么简单。源码定义了七种配置作用域:

typescript 复制代码
// src/services/mcp/types.ts
export const ConfigScopeSchema = lazySchema(() =>
  z.enum([
    'local',       // .claude/settings.local.json(不入 git)
    'user',        // ~/.claude.json(全局用户配置)
    'project',     // .mcp.json(项目共享,入 git)
    'dynamic',     // 运行时动态注入
    'enterprise',  // 企业管理策略(独占控制权)
    'claudeai',    // Claude.ai 连接器
    'managed',     // 托管配置
  ]),
)

优先级:插件 < 用户 < 项目 < 本地 < 企业独占

Object.assign({}, plugins, user, project, local) 这条链中,后写入覆盖先写入。但企业配置是特例:如果存在企业 MCP 配置文件(managed-mcp.json),所有其他来源直接被忽略------不是优先级最高,而是独占控制权。这个设计背后的意图是:企业 IT 管理员需要一个"锁死"机制,阻止员工绕过公司安全策略自行添加 MCP server。

Denylist/Allowlist:双重过滤

配置加载后,还要经过安全策略过滤:

  • Denylist(黑名单)优先检查:被禁用的服务器直接拒绝连接
  • Allowlist(白名单)二次检查:如果有白名单,只允许名单内的服务器连接

三种匹配维度:按服务器名称、按命令(['npx', '@scope/server-name'])、按 URL 模式(https://*.company.com/* 支持通配符)。urlPatternToRegex 函数把通配符 * 翻译成正则 .*,既支持精确匹配也支持域名级控制。

去重机制

同一个 MCP server 可能出现在多个配置来源中(插件装了,用户又手动配了)。去重通过"服务器指纹"实现:

typescript 复制代码
// stdio 类型:命令数组的 JSON 序列化
`stdio:${jsonStringify(cmd)}`

// 远程类型:URL(CCR 代理 URL 先解包再指纹计算)
`url:${unwrapCcrProxyUrl(url)}`

// SDK 类型:无指纹(不参与去重)

规则:手动配置 > 插件配置;先注册 > 后注册;启用的 > 禁用的。CCR 代理 URL 需要先"解包"(unwrapCcrProxyUrl)再计算指纹------否则同一个远程 server 经过代理和直连的指纹不同,去重失效。


五、安全模型与 2026 社区风暴:一个设计选择引发的连锁反应

这一节是全文最长的一节,因为它把安全架构、漏洞披露、社区分裂三条线合成一个叙事:MCP 的安全设计意图是什么 → 这个设计怎么被现实攻破 → 各方怎么反应。 这篇不是安全新闻综述------每条分析都对应前面四节的技术地基。

五层安全控制的意图

从源码看,MCP 有五层安全控制:

  1. 配置级------哪些服务器被允许存在(denylist/allowlist)
  2. 工具级 ------filterToolsByDenyRules 按工具名禁止敏感操作
  3. 执行级 ------canUseTool 运行时检查权限
  4. 通道级 ------channelPermissions 控制服务器的通道访问
  5. OAuth 级 ------services/mcp/auth.ts 处理远程服务器的认证

OAuth 认证流程是一条完整的序列:连接请求 → 服务器返回 401 → Claude Code 发起 OAuth 流程 → 获取令牌 → 带令牌重连。

一个微妙的豁免:MCP 工具在 Agent 过滤中被放行

📂 展开源码:filterToolsForAgent 中的 MCP 工具豁免

typescript 复制代码
// src/tools/AgentTool/agentToolUtils.ts
export function filterToolsForAgent({ tools, isBuiltIn, isAsync }): Tools {
  return tools.filter(tool => {
    // MCP 工具(mcp__ 前缀)不受工具禁用列表限制------始终可用
    if (tool.name.startsWith('mcp__')) return true
    if (ALL_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false
    if (!isBuiltIn && CUSTOM_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false
    if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) return false
    return true
  })
}

MCP 工具(mcp__ 前缀)被豁免 于所有内置工具禁用规则。即使你配置了 disallowedTools: [Bash, Write],MCP server 提供的工具依然可以通过。这不是漏洞------这是设计选择:MCP server 的权限应该由 MCP 层的 deny 规则管理,不走内置工具过滤通道。

但它留下了隐患:如果你配置了一个社区 MCP server 但没有在 MCP 层额外设置 deny 规则,这个 server 的所有工具相当于拥有白名单通行权。

2026 年 4 月:OX Security 的"协议级"攻击披露

2026 年 4 月中旬,安全公司 OX Security 披露了一个系统性漏洞,影响所有官方 MCP SDK(Python、TypeScript、Java、Rust),波及约 1.5 亿次下载量、7000+ 公开暴露的服务器实例。

漏洞的根因恰好出在第二节讲到的 Stdio 传输设计上 :命令执行在先,协议验证在后。StdioClientTransport 的构造函数启动子进程时,命令就已经在宿主系统上执行了------不管这个子进程后续是否成功完成 MCP 初始化握手。攻击者只需要在 .mcp.jsoncommand 字段或 args 数组里注入恶意命令。
为什么五层安全控制全都没拦住?

因为漏洞不在任何一层"安全控制"的范围内------它发生在第零层:进程创建时。 Stdio 的 command 字段执行本质上等同于用户在终端里跑 eval(command)。之后五层安全全部正常工作------只是它们检查的是一个已经被攻破了的进程。

研究人员编目了四种利用方式:

  • 未认证的 UI 注入------通过 MCP 配置面板注入恶意命令
  • 安全强化绕过------利用有效的 MCP 配置绕过企业 denylist(denylist 用的是命令指纹匹配,恶意命令的指纹和合法命令可以不同)
  • 零点击 prompt 注入------在 IDE 环境中通过 MCP 工具描述注入恶意指令
  • 恶意市场分发------发布带有后门的 MCP server 包到 npm/PyPI

Anthropic 的回应引爆了更大争议:他们拒绝修复协议层面的根因,称这是"预期行为",将安全责任放在了 MCP server 开发者和下游用户的肩上。 OX Security 的研究人员评论道:"协议层面的一次架构变更本可以保护每一个下游项目、每一位开发者、每一位依赖 MCP 的终端用户。"这句话击中了要害------Stdio 的 command 字段可以用沙箱子进程或 OCI 容器隔离来限制攻击面,但 Anthropic 选择不作为。

到目前为止已分配 10+ 个高危/严重 CVE,其中包括 CVSS 10.0 和 9.6 评分。

所以你应该怎么做 :审查 .mcp.json 中每一个 command 的内容------这是进入你系统的入口点。优先使用官方维护的 MCP server;对远程 HTTP MCP server 启用 OAuth 认证;在企业环境用 managed-mcp.json 锁定允许的服务器列表。

Perplexity 放弃 MCP(2026 年 3 月)

AI 搜索引擎 Perplexity 宣布放弃 MCP 集成,核心数据:MCP 工具定义消耗了 72% 的上下文窗口。 在一个 200K token 的窗口里,140K tokens 被工具描述、参数 JSON Schema、Server 元数据吃掉了,留给实际对话和推理的空间仅剩 60K。

Scalekit 的独立分析印证了这个数字:MCP 方案比 CLI 等价方案消耗 4-32 倍的 token

这里需要联系第三节的阶段 3(工具发现与包装)来理解:MCP server 在 listTools() 时返回的每个工具定义,包含完整的 descriptioninputJSONSchema。如果 JSON Schema 嵌套很深(比如一个"创建订单"的工具需要的参数结构),仅这个工具的 token 开销就可能超过 2000 tokens。三四个功能丰富的 MCP server,轻松让工具定义成为上下文最大消费者。

这不是 MCP 协议本身有问题------这是"通用协议"的代价。CLI 命令只是一个字符串在 prompt 里,MCP 工具定义则是一套完整的形式化接口描述。形式化在"多 Agent 共享"时的收益,在"单人单次调用"时变成了纯粹的开销。

国家黑客利用 MCP 进行 AI 编排攻击(2025 年 11 月)

Anthropic 披露了一个被标记为 GTG-1002 的攻击组织,使用 Claude Code + MCP 工具对约 30 个组织实施了 AI 编排入侵。AI 自主完成了 80-90% 的战术工作------侦察、横向移动、凭证收集------人类只在 4-6 个关键决策点介入。世界经济论坛确认这是首例"生产级自主 AI 网络攻击"

这个案例里 MCP 的角色是攻击面放大器------一个被攻破的 MCP server 不是泄露一个工具,是泄露一套完整的外部系统访问能力。如果企业配置了允许访问数据库、CI/CD 流水线、云基础设施的 MCP server,攻击者拿到一个入口就能编排所有这些系统。

三个阵营的分裂

2026 年的 MCP 社区呈现清晰的阵营分化:

  • 安全研究者:"MCP 的信任模型从根本上就坏了。工具定义、API 响应、用户提示都在上下文窗口里,Agent 默认全信。补丁修不了架构问题。"
  • 企业平台团队:"MCP 是必选项。没有统一的工具协议,50 个 Agent 管不动。治理、网关、审计跟踪都在路上。"
  • 独立开发者 :"MCP 太重了。配置烦、调试难、token 烧得心疼。我一个人用 gh 十秒钟搞定的事,MCP 要搞半小时。"

这场分裂的本质不是技术路线的分歧,而是使用场景的不重叠。MCP 的设计偏向解决企业平台团队的需求(数十个 Agent、几百个工具、需要审计和治理),独立开发者的痛苦是附带伤害。

MCP vs A2A 与路线图

Google 的 Agent-to-Agent(A2A)协议在 2026 年出现,解决的是和 MCP 不同但互补的问题:

  • MCP = Agent ↔ 工具("让 Agent 能用同一个接口调不同工具")
  • A2A = Agent ↔ Agent("让不同 Agent 之间能互相说话、协商、传递任务")

大多数企业未来可能需要两者共存。Anthropic 的 2026 MCP 路线图包括:OAuth 2.1 正式规范、Streamable HTTP 正式规范、工具治理(Tool Governance)API、可观测性标准。

MCP 远未死亡------但它的"青少年期"结束了。接下来几年是它证明自己是"有用的基础设施"还是"过度设计的中间层"的关键期。


六、决策框架:把前面五节的地基变成选择能力

前面五节建立了技术地基。这一节是地基的产物------你理解 MCP 怎么传、怎么跑、怎么配、怎么出事之后,"该不该用"不再是一个需要背的规则,而是从技术理解中自然得出的判断。

问题一:这个任务有团队级重复性吗?

这个判断直接来源于第三节的阶段 3(工具发现与包装):每个 MCP 工具的完整定义都会被注入到每次 API 请求中。如果 5 个人用同一个 MCP server,5 个人都受益于同一个标准化接口。但如果只有你一个人偶尔用一次,你为 100 个工具定义支付的 token 成本永远得不到摊薄。

MCP 的经济学是规模经济。 固定成本(配置、调试、维护、token 开销)需要足够多的调用频次和使用者来均摊。

问题二:已有成熟 CLI 工具吗?

这个判断来源于第二节的传输层分析:每多一层中间代理,就多一层的失败模式。GitHub MCP server 的工具调用失败率约 15%,而直接调 gh CLI 是约 2%。为什么?

arduino 复制代码
MCP 路径(5层):
Claude Code → MCP Client → JSON-RPC → MCP Server → GitHub API → 数据库

CLI 路径(4层):
Claude Code → Shell → gh CLI → GitHub API → 数据库

少一层就少一层失败概率。GitHub 有 gh、Stripe 有 stripe、AWS 有 aws、Vercel 有 vercel------这些 CLI 经过了成千上万用户的验证,错误处理完善。除非你需要多个 AI 应用共享同一套工具接口,CLI 通常更可靠。

问题三:你是否需要企业级的权限和审计?

CLI 工具没有统一的认证框架、操作审计、权限模型。MCP server 层可以统一管理这些。第四节讲的七种配置作用域、Denylist/Allowlist 双重过滤、OAuth 认证------这些东西只有在"多人多 Agent 多系统"的规模下才有存在意义。

快速决策速查

场景 推荐方案 对应的技术原因
个人偶尔调用 GitHub API gh CLI 4 层调用链 vs 5 层,失败率低
团队共享内部数据库查询 MCP server 工具定义一次性注入,全员复用
CI/CD 流水线自动化 CLI MCP server 运行状态不可控
多 AI 应用共享工具 MCP server MCP 的核心价值场景
安全敏感操作 两者配合 CLI 执行 + MCP 记录 + 人工审批

核心规则:如果你要在 prompt 里反复解释同一个工具的用法,MCP 值得考虑。如果工具用法一目了然,CLI 更快。


七、常见失败模式与源码级诊断

参照 Sub-agents 篇的写法,每个失败模式不仅有技术原因分析,还有"心理根因"------读者为什么会掉进这个坑。因为源码里它是机械规则,不会"觉得该总结"、"理解权限为什么被拒"。

失败模式 1:MCP server 突然不可用,AI 反复重试

症状:Claude Code 反复尝试调用同一个 MCP 工具,每次都失败,但你看到错误信息后不知道问题出在哪。

心理根因:你以为"工具在列表里 = 工具能用"。这个心理预判来自于日常使用 UI 的习惯------APP 里的按钮点了没反应,你会觉得是 APP 卡了,不会觉得是按钮背后的服务器挂了你没收到通知。

源码级原因 :MCP Client 的连接状态有五种。当 Server 从 Connected 转入 Failed 时,工具依然在 assembleToolPool() 合并后的统一工具池里。Claude 基于工具池做决策,不知道某个工具的背后连接已经断了------它收到的工具列表中 mcp__server__tool 看起来和其他工具一模一样,没有"健康状态"标注。

解决方案:在 prompt 里建立明确的回退规则(见第八节案例 2)。

失败模式 2:MCP 工具定义占用过多上下文

症状:启用了三四个 MCP server 后,Claude Code 的"变笨"现象明显------回答质量下降,容易忘记你之前说过的话。

心理根因:你以为"多装几个 MCP server = 多几个能力",但没意识到每个能力都会静态占用 token 配额。这是典型的"附加功能"思维------在传统软件里,装 10 个插件对系统性能的影响通常很小。但在 LLM 上下文里,每增加一个工具定义就是直接从推理空间里扣减 token 预算。

源码级原因:每个 MCP server 的工具定义(名称 + 描述 + 参数 JSON Schema)都会在每次 LLM API 请求中发送------不管这个工具本次对话是否真的会被用到。这是第三节阶段 3 的"动态发现"机制的代价:MCP server 汇报的所有工具全量注入,Claude Code 不做按需裁剪。

解决方案 :按需启用/禁用 MCP server。/mcp disable server-name 不删除配置,只是断开连接。任务需要时再 enable

失败模式 3:假设 MCP 工具的行为和文档描述一致

症状:你配置了一个社区 MCP server,文档说它支持某功能,但实际调用时总是出错。

心理根因:你把 MCP server 的工具描述当成了 API 文档里的"契约"------以为别人写的就是对的。在传统 REST API 开发中,OpenAPI/Swagger 文档通常是从代码生成或经过严格测试的。而 MCP 的工具描述是 MCP server 代码里的字符串,Claude Code 不验证它和实际实现的一致性。

源码级原因 :MCP 的工具描述(descriptioninputJSONSchema)是 MCP server 自己声明的,Claude Code 无条件信任。没有"工具描述的验证"机制------如果一个 MCP server 声称它的 create_issue 接受 assignee 参数但实际不支持,Claude 会在调用时才发现。MCPToolbuildTool({...}) 骨架不做任何服务端校验。

解决方案 :新的 MCP server 先用只读工具测试(listgetsearch),确认稳定性后再用写操作。如果行为与描述不符,用 CLAUDE.md 中的说明覆盖 Claude 的默认理解。


八、实战案例

案例 1:团队内部"活文档"MCP server

场景:团队维护微服务集群,15 个服务、3 个数据库、一堆内部 API。新人入职要读几百页 Wiki。

方案 :写一个内部 MCP server,把常用操作封装成 query_usersget_order_statuscheck_deployment 等工具。工具描述就是活的文档------新人 git clone 后自动能用,不用读 Confluence。

json 复制代码
// .mcp.json(项目级别,入 git)
{
  "mcpServers": {
    "internal-platform": {
      "command": "node",
      "args": [".mcp/internal-server.js"],
      "env": {
        "API_KEY": "${INTERNAL_API_KEY}"
      }
    }
  }
}

MCP server 的 listTools() 返回示例:

json 复制代码
{
  "tools": [
    {
      "name": "query_users",
      "description": "查询用户表。支持按部门、角色、活跃状态筛选。返回 JSON 数组,最多 500 条。",
      "inputSchema": {
        "type": "object",
        "properties": {
          "department": { "type": "string", "description": "部门名,精确匹配" },
          "role": { "type": "string", "description": "角色:admin / developer / viewer" },
          "is_active": { "type": "boolean" }
        }
      }
    }
  ]
}

注意这里的关键设计:工具描述直接写明了"最多 500 条"------Claude 知道结果不会撑爆上下文,用户不用额外说明。参数名和描述就是 API 文档,不需要另一套 Confluence 页面。

案例 2:MCP 失败时的 CLI 回退设计

场景:你的核心工作流依赖 GitHub MCP server 管理 Issue。某天 MCP server 挂了怎么办?

方案:在 CLAUDE.md 里写一个确定性的回退规则,不给 Claude 纠结的空间:

markdown 复制代码
## GitHub 操作回退策略

使用 GitHub MCP (`mcp__github__*`) 进行 Issue 操作。
规则:
1. MCP 调用优先
2. 如果 MCP 调用连续失败 2 次 → 立即回退到 gh CLI,不尝试第三次
3. 回退后使用以下命令:
   - 列 Issue:gh issue list --state all --json title,body,labels --limit 100
   - 创建 Issue:gh issue create --title "..." --body "..."
   - 查看 Issue:gh issue view <number>
4. 回退时在回复中说"已从 MCP 回退到 gh CLI"

这不是让 Claude 在 MCP 和 CLI 之间"选择"------是给它一个确定的决策规则。失败两次就切,不纠结。这个设计的来源是第七节的失败模式 1(工具在池里但连接断了)。

案例 3:Stripe 集成------MCP 的 100 个工具 vs CLI 的 3 个

场景:你经常需要让 Claude Code 查询 Stripe 交易数据。

方案 A(MCP):用 Stripe 官方 MCP server。官方维护、稳定性好。但 Stripe API 有 100+ 个端点,MCP server 的工具集也庞大------Claude 每次请求都要消耗 token 去理解 80 个你从来不用的工具。按第三节的 token 开销逻辑,100 个 Stripe 工具的定义可能吃掉 15-20K tokens。

方案 B(CLI + 薄包装)

bash 复制代码
#!/bin/bash
# stripe-query.sh ------ 只暴露你真正需要的 3 个操作
case $1 in
  transactions) stripe transactions list --limit ${2:-10} --json | jq '.data[] | {id, amount, status}' ;;
  customer)     stripe customers retrieve $2 --json | jq '{id, email, balance}' ;;
  invoice)      stripe invoices list --customer $2 --limit ${3:-10} --json | jq '.data[] | {id, amount_due, status}' ;;
esac

对比

维度 方案 A(MCP) 方案 B(CLI 薄包装)
工具定义 token ~15K(100 个工具) ~200(3 行用法说明)
失败调试 需理解 MCP 协议全栈 直接重跑脚本看输出
Stripe API 变更 等 MCP server 更新 改一行 jq 过滤器
多 AI 复用 可以 不可以(只有 Claude Code 能用)
适合规模 团队 个人/小团队

对个人使用,方案 B 更实际。但如果团队有 3 个工程师都在用 Claude Code 调 Stripe,方案 A 的 15K token 开销被 3 人分摊,标准化收益开始显现。选择取决于规模------又一次回到第六节问题一的逻辑。

案例 4:给 Sub-agent 配专属 MCP------避免权限泄露

场景 :你有一个"数据库查询 Agent"和一个"代码审查 Agent"。数据库 Agent 应该能用 mcp__postgres__*,代码审查 Agent 不应该。

方案 :在 .claude/agents/ 的两个 Agent 定义中,分别配各自的 mcpServers

yaml 复制代码
# .claude/agents/db-query.md
---
name: db-query
description: 只读数据库查询 Agent
tools: [Read, Grep, Glob]
mcpServers:
  - postgres-readonly
---

# .claude/agents/code-review.md
---
name: code-review
description: 代码审查 Agent,不需要数据库访问
tools: [Read, Grep, Glob]
# 没有 mcpServers 字段 ------ 继承父 Agent 的 MCP 连接但不增加新的

db-query Agent 启动时,initializeAgentMcpServers() 会连接 postgres-readonlycode-review Agent 不配 mcpServers,只能访问父 Agent 已有的 MCP 连接。数据库工具对审查 Agent 完全不可见。


本篇实践任务

任务一:审计你当前的 MCP 配置

检查 ~/.claude.json 和项目的 .mcp.json,列出你启用了哪些 MCP server。逐一评估:这个 server 的 command/url 是什么?真的常用吗?有没有 CLI 替代方案?根据评估,禁用不必要的 server。

任务二:写一个 MCP 回退策略

CLAUDE.md 里为你最依赖的 MCP server 写一段回退规则。格式参考第八节案例 2:明确触发回退的条件(连续失败 N 次)、回退到的 CLI 命令、回退后要不要汇报。

任务三:读一个 MCP server 的入口文件

在 npm 上找一个你正在用或想用的 MCP server,花 30 分钟读它的入口文件。关注:它暴露了哪些工具?工具描述写得清不清楚?inputSchema 是否准确?有没有把用户输入直接拼接到 shell 命令里?


下篇预告

第 13 篇:内置工具深度解析------Claude Code 的 "手" 是怎么设计的

MCP 讲的是"外部工具怎么接入 Claude Code 的统一工具池"。但工具池的另一半------Bash、Read、Write、Edit、Glob、Grep、Agent(Sub-agent)、Task、Skill 这十几个内置工具------各有自己的设计取舍:Bash 的沙箱策略、Edit 的精确替换 vs Write 的全量覆盖、Agent 工具的上下文 fork 机制。下一篇从源码层面对比每个内置工具的设计决策,以及它们和 MCP 工具的镜像关系------一个是"外部统一接入",一个是"内部各自分工"。


AI Coding 系列持续更新。MCP 不是银弹,也不是废铁------它是一个有明确边界和具体取舍的工程协议。理解它的传输、生命周期、配置和安全模型,你就能不被人云亦云牵着走。

相关推荐
古韵1 小时前
告别手写分页逻辑:usePagination 从 50 行到 3 行
java·前端
IT_陈寒1 小时前
Vite静态资源引用差点把我逼疯,原来要这样处理
前端·人工智能·后端
子兮曰1 小时前
WSL 配 GPU 用 Docker 的折腾指南(2026 年版)
linux·前端·后端
Nturmoils2 小时前
从 mysql 命令切到 ksql,第一步先把连接搞明白
后端
lantian2 小时前
TypeScript 三斜线指令完全指南:从入门到理解为什么不再需要它
前端·javascript·vue.js
鹏多多2 小时前
锐评CSDN最近上线的AI数字营销:烂完之前最后再捞一笔
前端·后端·程序员
先吃饱再说2 小时前
从 WeUI 按钮组件学 BEM 命名规范:让 CSS 不再难维护
前端·代码规范
槑有老呆2 小时前
从前端 HTTP 请求到 LLM 接口调用:一篇文章带你彻底搞懂
前端
ZengLiangYi2 小时前
AI 编程工具的数据格式为什么不能统一
javascript·后端·架构