六、AI Agent 设计模式:MCP 协议集成

4 阅读4分钟

上一篇我们分析了子 Agent 架构。今天深入第五个主题:MCP 协议集成。MCP(Model Context Protocol)是 Anthropic 的开放协议,让外部工具可以接入 Claude。

MCP 是什么?

MCP 是一个标准化协议,定义了:

  • Resources:文件、数据库记录等数据源
  • Tools:可执行的函数
  • Prompts:预定义的提示模板
  • Sampling:请求 LLM 生成内容

Claude Code 主要使用 ToolsResources

Transport 类型

type Transport =
  | 'stdio'      // 标准输入输出(本地进程)
  | 'sse'        // Server-Sent Events
  | 'sse-ide'    // IDE SSE
  | 'http'       // HTTP
  | 'ws'         // WebSocket
  | 'sdk'        // SDK 内部(进程内)
Transport用途
stdio本地命令行工具(如 uvx mcp-server-git)
sse远程服务器(如 api.example.com/mcp)
httpHTTP API
wsWebSocket 连接
sdk内部 SDK 集成

Server Config Schema

// stdio 配置
type McpStdioServerConfig = {
  type: 'stdio'
  command: string      // 如 'uvx', 'npx'
  args: string[]       // 如 ['mcp-server-git']
  env?: Record<string, string>
  cwd?: string
}

// SSE 配置
type McpSSEServerConfig = {
  type: 'sse'
  url: string
  headers?: Record<string, string>
  oauth?: OAuthConfig
}

// HTTP 配置
type McpHTTPServerConfig = {
  type: 'http'
  url: string
  headers?: Record<string, string>
  oauth?: OAuthConfig
}

// SDK 配置(内部)
type McpSdkServerConfig = {
  type: 'sdk'
  name: string
}

Config Scope

配置来源有多个层级:

type ConfigScope =
  | 'user'       // ~/.claude/settings.json
  | 'project'    // .claude/settings.json
  | 'local'      // .claude/settings.local.json
  | 'enterprise' // MDM/企业配置
  | 'claudeai'   // Claude.ai 配置
  | 'managed'    // 远程管理配置
  | 'dynamic'    // 动态添加

优先级:local > project > user > enterprise。

Connection States

type MCPServerConnection =
  | ConnectedMCPServer
  | FailedMCPServer
  | NeedsAuthMCPServer
  | PendingMCPServer
  | DisabledMCPServer

type ConnectedMCPServer = {
  type: 'connected'
  client: Client
  name: string
  capabilities: ServerCapabilities
  serverInfo?: { name: string; version: string }
  instructions?: string
  tools: MCPTool[]
  resources?: ServerResource[]
}

type FailedMCPServer = {
  type: 'failed'
  name: string
  error: string
  config: ScopedMcpServerConfig
}

type NeedsAuthMCPServer = {
  type: 'needs-auth'
  name: string
  config: ScopedMcpServerConfig
}

连接流程

async function connectMcpServer(config: McpServerConfig, scope: ConfigScope): MCPServerConnection {
  const name = config.name ?? generateServerName(config)

  // === 阶段 1: 创建 Client ===
  const client = new Client({ name: 'claude-code', version: CLI_VERSION })

  // === 阶段 2: 选择 Transport ===
  let transport: Transport

  if (config.type === 'stdio') {
    // 启动子进程
    const childProcess = spawn(config.command, config.args, {
      env: { ...process.env, ...config.env },
      cwd: config.cwd,
      stdio: ['pipe', 'pipe', 'pipe'],
    })

    transport = new StdioClientTransport({
      stdin: childProcess.stdin,
      stdout: childProcess.stdout,
    })
  }

  if (config.type === 'sse') {
    transport = new SSEClientTransport({
      url: config.url,
      headers: config.headers,
    })
  }

  if (config.type === 'http') {
    transport = new HTTPClientTransport({
      url: config.url,
      headers: config.headers,
    })
  }

  // === 阶段 3: 连接 ===
  try {
    await client.connect(transport, {
      timeout: 30_000,  // 30 秒超时
    })
  } catch (error) {
    return {
      type: 'failed',
      name,
      error: error.message,
      config: { config, scope },
    }
  }

  // === 阶段 4: 获取 Capabilities ===
  const capabilities = client.getServerCapabilities()

  // === 阶段 5: 获取 Tools ===
  let tools: MCPTool[] = []
  if (capabilities?.tools) {
    const toolsResult = await client.listTools()
    tools = toolsResult.tools.map(normalizeMcpTool)
  }

  // === 阶段 6: 获取 Resources ===
  let resources: ServerResource[] = []
  if (capabilities?.resources) {
    const resourcesResult = await client.listResources()
    resources = resourcesResult.resources
  }

  // === 阶段 7: 获取 Server Info ===
  const serverInfo = client.getServerInfo()

  // === 阶段 8: OAuth 检查 ===
  if (config.oauth && needsAuthentication(client)) {
    return {
      type: 'needs-auth',
      name,
      config: { config, scope },
    }
  }

  return {
    type: 'connected',
    client,
    name,
    capabilities,
    serverInfo,
    tools,
    resources,
    config: { config, scope },
    cleanup: async () => {
      await client.close()
      if (childProcess) {
        childProcess.kill()
      }
    },
  }
}

8 个阶段,完整的连接生命周期。

OAuth 认证

type OAuthConfig = {
  clientId?: string
  callbackPort?: number
  authServerMetadataUrl?: string
  xaa?: boolean  // Cross-App Access
}

async function authenticateMcpServer(server: NeedsAuthMCPServer): ConnectedMCPServer {
  const config = server.config.config

  // === 阶段 1: 获取 OAuth 元数据 ===
  const metadata = await fetchOAuthMetadata(config.oauth.authServerMetadataUrl)

  // === 阶段 2: 生成授权 URL ===
  const authUrl = generateAuthUrl({
    authorizationEndpoint: metadata.authorization_endpoint,
    clientId: config.oauth.clientId,
    redirectUri: `http://localhost:${config.oauth.callbackPort}/callback`,
    scope: metadata.scopes_supported ?? ['openid'],
    state: generateState(),
  })

  // === 阶段 3: 启动回调监听 ===
  const codePromise = waitForAuthCode(config.oauth.callbackPort)

  // === 阶段 4: 打开浏览器 ===
  openBrowser(authUrl)

  // === 阶段 5: 获取 Code ===
  const code = await codePromise

  // === 阶段 6: 交换 Token ===
  const tokens = await exchangeCodeForTokens({
    tokenEndpoint: metadata.token_endpoint,
    clientId: config.oauth.clientId,
    code,
    redirectUri: `http://localhost:${config.oauth.callbackPort}/callback`,
  })

  // === 阶段 7: 保存 Token ===
  saveMcpOAuthToken(server.name, tokens)

  // === 阶段 8: 重新连接 ===
  const newConfig = {
    ...config,
    headers: {
      ...config.headers,
      Authorization: `Bearer ${tokens.access_token}`,
    },
  }

  return connectMcpServer(newConfig, server.config.scope)
}

完整的 OAuth 流程——浏览器授权 + 本地回调。

MCP Tool 动态创建

function createMcpTool(mcpTool: MCPToolDefinition, server: ConnectedMCPServer): Tool {
  const toolName = `mcp__${server.name}__${mcpTool.name}`

  return buildTool({
    name: toolName,
    isMcp: true,
    mcpInfo: {
      serverName: server.name,
      toolName: mcpTool.name,
    },

    // Schema 转换
    inputSchema: convertMcpSchemaToZod(mcpTool.inputSchema),

    // 执行
    async call(args, context) {
      const result = await server.client.callTool({
        name: mcpTool.name,
        arguments: args,
      })

      return processMcpToolResult(result)
    },

    // 权限
    async checkPermissions(args, context) {
      // MCP 工具权限检查
      return checkMcpPermission(server, mcpTool, args, context)
    },

    // 属性
    isConcurrencySafe: () => mcpTool.annotations?.safeToRunConcurrently ?? false,
    isReadOnly: () => mcpTool.annotations?.readOnly ?? false,
    isDestructive: () => mcpTool.annotations?.destructive ?? false,

    // 描述
    description: mcpTool.description,

    // 最大结果大小
    maxResultSizeChars: 100_000,
  })
}

每个 MCP tool 被转换为 Claude Code 的 Tool 接口。

Schema 转换

function convertMcpSchemaToZod(mcpSchema: MCPInputSchema): z.ZodType {
  // MCP 使用 JSON Schema
  // Claude Code 使用 Zod

  if (mcpSchema.type === 'object') {
    const shape: Record<string, z.ZodType> = {}

    for (const [key, prop] of Object.entries(mcpSchema.properties ?? {})) {
      shape[key] = convertMcpPropertyToZod(prop)

      if (mcpSchema.required?.includes(key)) {
        // 必须字段
      } else {
        // 可选字段
        shape[key] = shape[key].optional()
      }
    }

    return z.object(shape)
  }

  // 其他类型...
}

function convertMcpPropertyToZod(prop: MCPProperty): z.ZodType {
  if (prop.type === 'string') {
    let schema = z.string()
    if (prop.description) schema = schema.describe(prop.description)
    return schema
  }

  if (prop.type === 'number') {
    return z.number()
  }

  if (prop.type === 'boolean') {
    return z.boolean()
  }

  if (prop.type === 'array') {
    return z.array(convertMcpPropertyToZod(prop.items))
  }

  // ...
}

JSON Schema → Zod 转换。

Resources 访问

// MCP Resources 可以作为文件读取
async function readMcpResource(uri: string, server: ConnectedMCPServer): MCPResourceContent {
  const result = await server.client.readResource({ uri })

  return {
    uri,
    mimeType: result.contents[0].mimeType,
    text: result.contents[0].text,
    blob: result.contents[0].blob,
  }
}

// 某些 Resources 可以订阅更新
async function subscribeMcpResource(uri: string, server: ConnectedMCPServer): void {
  if (server.capabilities?.resources?.subscribe) {
    await server.client.subscribeResource({ uri })

    // 监听更新
    server.client.on('resource_updated', (event) => {
      if (event.uri === uri) {
        notifyResourceChanged(uri)
      }
    })
  }
}

Resources 作为数据源——可以读取、订阅更新。

Channel Permissions

// MCP 工具权限控制
type ChannelPermission = {
  allowedTools?: string[]      // 允许的工具列表
  deniedTools?: string[]       // 禁止的工具列表
  allowedResources?: string[]  // 允许的资源列表
  readPermission?: 'allow' | 'ask' | 'deny'
  writePermission?: 'allow' | 'ask' | 'deny'
}

function checkMcpPermission(
  server: ConnectedMCPServer,
  tool: MCPTool,
  args: unknown,
  context: ToolUseContext,
): PermissionResult {
  // 检查 channel permissions
  const permissions = getChannelPermissions(server.name)

  if (permissions.deniedTools?.includes(tool.name)) {
    return { behavior: 'deny', message: `Tool ${tool.name} is denied for ${server.name}` }
  }

  if (permissions.allowedTools && !permissions.allowedTools.includes(tool.name)) {
    return { behavior: 'ask', message: `Tool ${tool.name} is not in allowed list` }
  }

  // 检查读写权限
  if (tool.annotations?.destructive && permissions.writePermission === 'deny') {
    return { behavior: 'deny', message: 'Write operations denied' }
  }

  // 默认通过 MCP 权限系统
  return { behavior: 'passthrough' }
}

MCP server 可以配置工具白名单/黑名单。

Cross-App Access (XAA)

XAA 是 SEP-990 规范,允许 MCP servers 通过 IdP 认证访问跨应用资源:

// XAA 配置
type XaaIdpConfig = {
  issuer: string
  clientId: string
  callbackPort: number
}

// Server 配置中启用 XAA
const mcpConfig = {
  type: 'sse',
  url: 'https://api.example.com/mcp',
  oauth: {
    xaa: true,  // 启用 XAA
  },
}

// XAA 认证流程
async function authenticateWithXaa(server: NeedsAuthMCPServer): ConnectedMCPServer {
  // 1. 获取 IdP 配置(全局配置)
  const idpConfig = getXaaIdpConfig()

  // 2. 使用 IdP 认证
  const tokens = await authenticateWithIdp(idpConfig)

  // 3. Token 可用于所有 XAA-enabled servers
  saveXaaToken(tokens)

  // 4. 重新连接
  return connectMcpServerWithXaaToken(server.config, tokens)
}

一次 IdP 认证,所有 XAA server 共享。

环境变量展开

// MCP 配置支持环境变量
const mcpConfig = {
  type: 'stdio',
  command: 'uvx',
  args: ['mcp-server-git'],
  env: {
    API_KEY: '${API_KEY}',           // 引用环境变量
    CUSTOM_URL: 'https://${HOST}',   // 组合使用
  },
}

function expandEnvVars(config: McpServerConfig): McpServerConfig {
  if (config.env) {
    const expandedEnv: Record<string, string> = {}

    for (const [key, value] of Object.entries(config.env)) {
      expandedEnv[key] = expandEnvVarString(value)
    }

    return { ...config, env: expandedEnv }
  }

  return config
}

function expandEnvVarString(value: string): string {
  // ${VAR} 格式
  return value.replace(/\$\{(\w+)\}/g, (_, name) => {
    return process.env[name] ?? ''
  })
}

配置中引用环境变量——安全地传递敏感信息。

MCP Reconnection

// MCP server 断开后自动重连
type PendingMCPServer = {
  type: 'pending'
  name: string
  config: ScopedMcpServerConfig
  reconnectAttempt?: number
  maxReconnectAttempts?: number
}

async function handleMcpDisconnection(server: ConnectedMCPServer): MCPServerConnection {
  const maxAttempts = server.config.config.reconnect?.maxAttempts ?? 3
  const delayMs = server.config.config.reconnect?.delayMs ?? 5000

  // 尝试重连
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    await sleep(delayMs * attempt)  // 递增延迟

    try {
      const reconnected = await connectMcpServer(
        server.config.config,
        server.config.scope,
      )

      if (reconnected.type === 'connected') {
        return reconnected
      }
    } catch (error) {
      // 继续尝试
    }
  }

  // 重连失败
  return {
    type: 'failed',
    name: server.name,
    error: 'Reconnection failed after max attempts',
    config: server.config,
  }
}

自动重连,递增延迟。

In-Process Transport

// SDK 内部传输(进程内)
class InProcessTransport implements Transport {
  private server: MCPServer
  private client: Client

  async connect(): void {
    // 直接连接,无网络
    this.server = createInProcessServer()
    await this.client.connect(this.server.transport)
  }

  async send(message: JSONRPCMessage): void {
    this.server.handleMessage(message)
  }

  onMessage(handler: (message: JSONRPCMessage) => void): void {
    this.server.transport.onMessage(handler)
  }
}

进程内传输——无网络开销,适合内置 MCP servers。

VSCode SDK MCP

// VSCode SDK MCP 集成
type VscodeSdkMcpConfig = {
  type: 'sdk'
  name: 'vscode'
}

async function connectVscodeSdkMcp(): ConnectedMCPServer {
  // VSCode 提供内置 MCP server
  // 通过 SDK transport 连接

  const client = new Client({ name: 'claude-code', version: CLI_VERSION })
  const transport = new SdkControlTransport()

  await client.connect(transport)

  return {
    type: 'connected',
    client,
    name: 'vscode',
    tools: extractVscodeTools(client),
    capabilities: client.getServerCapabilities(),
  }
}

VSCode SDK 提供内置 MCP server——无需配置,自动连接。

Elicitation Handler

// MCP server 可以请求用户输入
type ElicitationRequest = {
  type: 'elicitation'
  message: string
  buttons?: Array<{ text: string; action: string }>
  input?: { type: 'text' | 'select'; options?: string[] }
}

async function handleElicitation(
  request: ElicitationRequest,
  server: ConnectedMCPServer,
): ElicitationResponse {
  // 显示用户提示
  const response = await askUserQuestion({
    message: request.message,
    buttons: request.buttons,
    input: request.input,
  })

  // 执行 ElicitationResult Hook
  await executeElicitationResultHooks({
    serverName: server.name,
    request,
    response,
  })

  return response
}

MCP server 可以请求用户输入——Elicitation 协议。

Summary

MCP 协议集成展示了开放协议设计:

设计点实现
Transportstdio/sse/http/ws/sdk 五种传输
Config Scopeuser/project/local/enterprise/claudeai
Connection Statesconnected/failed/needs-auth/pending/disabled
OAuth浏览器授权 + 本地回调
Tool CreationMCP tool → Claude Tool 转换
SchemaJSON Schema → Zod 转换
Permissionschannel permissions + read/write 控制
XAACross-App Access,一次认证多处使用
Reconnection自动重连,递增延迟
ElicitationMCP server 请求用户输入

核心设计:多 Transport + OAuth + 动态 Tool + 权限控制

下一篇,我们将深入对话压缩——Token 超限时如何压缩信息。