MCP 安全与信任模型:三层防御体系
由于 MCP Server 拥有操作外部系统的能力(如删除 GitHub 仓库、读取敏感文件),Claude Code 为其设计了一套严密的安全体系,确保第三方插件不会成为系统的“后门”。
本质
MCP 安全模型本质上是 基于能力清单的细粒度访问控制(Capabilities-based ACL)。
它不信任任何新加入的插件。它通过 src/services/mcp/channelPermissions.ts,在模型与 Server 之间建立了一个双向的审计闸门。即使模型决定调用某个 MCP 工具,该调用也必须通过静态规则、动态白名单以及用户实时审批这三层考验。其核心理念是“最小特权原则”:Server 只能看到它被允许看到的路径,执行它被显式授权的操作。
从源码看实现
防御体系由以下三个核心层级组成:
静态管道审计 (Channel Allowlist): 利用
src/services/mcp/channelAllowlist.ts,系统限制了 Server 可以声明的能力范围。这在加载 Server 阶段就已生效,任何试图声明越权能力(如未在配置中注明的工具前缀)的行为都会被拦截。动态动作拦截 (Dynamic Interception): 在
src/services/mcp/channelPermissions.ts中,系统会检查每一次mcp_call:- 工具名审计:识别高危操作(如带有
destructive标记的工具)。 - 参数校验:利用 Zod 定义对传递给 Server 的数据进行严格验证,防止注入攻击。
- 作用域检查:如果工具涉及文件操作,会验证路径是否在当前工作空间或显式信任的路径列表中。
- 工具名审计:识别高危操作(如带有
用户在环审批 (User-in-the-loop): 当 MCP 工具执行涉及敏感操作(或在受限模式下)时,会触发 REPL 界面上的交互。
- 审批对话框:展示 Server 名称、调用的具体工具及参数详情。
- 持久化授权:用户可以选择“始终允许”,该选项会被记录在
settings.json的dontAsk列表中,由src/hooks/useCanUseTool.tsx在后续调用中静默通过。
进程与沙箱隔离 (Isolation): 通过 Stdio 传输层,Server 与 Client 在物理进程上完全隔离。即使 Server 内部逻辑被攻破,它也无法直接窃取 Claude Code 进程中的 API Key 或 会话历史(除非这些信息被作为参数显式传给它)。
使用时的关键约束
- 全家桶风险:如果你运行了一个恶意的、具有
Stdio权限的 MCP Server,它理论上可以尝试读取其物理进程能访问的所有本地文件。请务必只从可信源安装插件。 - 权限冒泡:MCP 的授权请求通常会冒泡到主终端。在
auto模式下,系统会尝试自动审批,但这取决于你的dontAsk设置和 Server 的信任等级。 - 配置优先级:项目级(Local)的
.mcp.json往往比全局更受限制,因为它直接继承了工作区的信任(Workspace Trust)状态。如果工作区未受信任,某些敏感的 MCP 操作将被直接阻断。 - 审计日志:所有的 MCP 调用和权限决策都会记录在诊断日志中(
utils/diagLogs.ts),这对于排查隐蔽的安全策略冲突非常有用。
继续探索
- 如果你想了解插件是如何在不同项目中被动态发现并加载的,请看 MCP 注册表与服务发现。
- 如果你关插件的登录授权流程,请看 MCP 认证与 OAuth 流程。
- 如果你想深入了解 Claude Code 的基础权限架构,请看 权限与沙箱。
源码锚点
claude-code-opensource/src/services/mcp/channelPermissions.ts— 核心权限裁决逻辑实现。
📄 src/services/mcp/channelPermissions.ts — 核心权限裁决逻辑实现。
export function isChannelPermissionRelayEnabled(): boolean {
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_harbor_permissions', false)
}claude-code-opensource/src/services/mcp/channelAllowlist.ts— 静态白名单与能力限制。
📄 src/services/mcp/channelAllowlist.ts — 静态白名单与能力限制。
export type ChannelAllowlistEntry = {
marketplace: string
plugin: string
}claude-code-opensource/src/hooks/useCanUseTool.tsx— 衔接 UI 层的审批判断钩子。
📄 src/hooks/useCanUseTool.tsx — 衔接 UI 层的审批判断钩子。
export type CanUseToolFn<Input extends Record<string, unknown> = Record<string, unknown>> = (tool: ToolType, input: Input, toolUseContext: ToolUseContext, assistantMessage: AssistantMessage, toolUseID: string, forceDecision?: PermissionDecision<Input>) => Promise<PermissionDecision<Input>>;
function useCanUseTool(setToolUseConfirmQueue, setToolPermissionContext) {
const $ = _c(3);
let t0;
if ($[0] !== setToolPermissionContext || $[1] !== setToolUseConfirmQueue) {
t0 = async (tool, input, toolUseContext, assistantMessage, toolUseID, forceDecision) => new Promise(resolve => {
const ctx = createPermissionContext(tool, input, toolUseContext, assistantMessage, toolUseID, setToolPermissionContext, createPermissionQueueOps(setToolUseConfirmQueue));
if (ctx.resolveIfAborted(resolve)) {
return;
}
const decisionPromise = forceDecision !== undefined ? Promise.resolve(forceDecision) : hasPermissionsToUseTool(tool, input, toolUseContext, assistantMessage, toolUseID);
return decisionPromise.then(async result => {
if (result.behavior === "allow") {
if (ctx.resolveIfAborted(resolve)) {
return;
}
if (feature("TRANSCRIPT_CLASSIFIER") && result.decisionReason?.type === "classifier" && result.decisionReason.classifier === "auto-mode") {
setYoloClassifierApproval(toolUseID, result.decisionReason.reason);
}
ctx.logDecision({
decision: "accept",
source: "config"
});
resolve(ctx.buildAllow(result.updatedInput ?? input, {
decisionReason: result.decisionReason
}));
return;
}
const appState = toolUseContext.getAppState();
const description = await tool.description(input as never, {claude-code-opensource/src/utils/mcpValidation.ts— 对 MCP 协议交互数据的底层校验。
📄 src/utils/mcpValidation.ts — 对 MCP 协议交互数据的底层校验。
export const MCP_TOKEN_COUNT_THRESHOLD_FACTOR = 0.5
export const IMAGE_TOKEN_ESTIMATE = 1600
const DEFAULT_MAX_MCP_OUTPUT_TOKENS = 25000
/**
* Resolve the MCP output token cap. Precedence:
* 1. MAX_MCP_OUTPUT_TOKENS env var (explicit user override)
* 2. tengu_satin_quoll GrowthBook flag's `mcp_tool` key (tokens, not chars —
* unlike the other keys in that map which getPersistenceThreshold reads
* as chars; MCP has its own truncation layer upstream of that)
* 3. Hardcoded default
*/
export function getMaxMcpOutputTokens(): number {
const envValue = process.env.MAX_MCP_OUTPUT_TOKENS
if (envValue) {
const parsed = parseInt(envValue, 10)
if (Number.isFinite(parsed) && parsed > 0) {
return parsed
}
}
const overrides = getFeatureValue_CACHED_MAY_BE_STALE<Record<
string,
number
> | null>('tengu_satin_quoll', {})
const override = overrides?.['mcp_tool']
if (
typeof override === 'number' &&
Number.isFinite(override) &&
override > 0
) {claude-code-opensource/src/types/permissions.ts— 权限相关的数据模型定义。
📄 src/types/permissions.ts — 权限相关的数据模型定义。
export const EXTERNAL_PERMISSION_MODES = [
'acceptEdits',
'bypassPermissions',
'default',
'dontAsk',
'plan',
] as const
export type ExternalPermissionMode = (typeof EXTERNAL_PERMISSION_MODES)[number]
// Exhaustive mode union for typechecking. The user-addressable runtime set
// is INTERNAL_PERMISSION_MODES below.
export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'
export type PermissionMode = InternalPermissionMode
// Runtime validation set: modes that are user-addressable (settings.json
// defaultMode, --permission-mode CLI flag, conversation recovery).
export const INTERNAL_PERMISSION_MODES = [
...EXTERNAL_PERMISSION_MODES,
...(feature('TRANSCRIPT_CLASSIFIER') ? (['auto'] as const) : ([] as const)),
] as const satisfies readonly PermissionMode[]
export const PERMISSION_MODES = INTERNAL_PERMISSION_MODES
// ============================================================================
// Permission Behaviors
// ============================================================================
export type PermissionBehavior = 'allow' | 'deny' | 'ask'