EssTech
Published on

最简MCP - 订票系统

Authors
  • avatar
    Name
    Heefan
    Twitter

看了MCP的介绍,一些人说MCP就像USB Protocol一样简单,但这让我很困惑,这篇文章自我梳理一下这些困惑。

从 REST booking system开始

选择Nextjs这种BFF的Web App,做一个Booking System, 这非常简单,直接Web App调用Remote Booking Rest API就可以了。

那么如何把这个模式变成MCP的模式呢?

MCP核心概念解释:

Host(宿主应用) - 发起请求的应用程序,在这里是Next.js BFF。Host是整个MCP架构的控制中心,负责:

  • 决定何时需要调用外部工具/服务
  • 管理用户交互和业务逻辑
  • 协调多个MCP服务器的调用
  • 处理最终的响应和渲染

Client(协议客户端) - MCP协议的实现层,是Host和Server之间的桥梁。Client负责:

  • 实现MCP协议的通信规范(JSON-RPC 2.0)
  • 管理与服务器的连接(stdio、SSE、WebSocket等)
  • 处理协议级别的错误和重试
  • 序列化/反序列化请求和响应

Server(MCP服务器) - 提供具体功能的服务提供者,在这里是Booking Server。Server负责:

  • 实现具体的业务工具(search_flights、search_hotels等)
  • 与外部API(航班API、酒店API)进行交互
  • 返回标准化的MCP响应格式
  • 处理认证、权限控制和数据验证

简单类比:

  • Host = 你的手机(发起需求)
  • Client = USB驱动程序(协议转换)
  • Server = U盘(提供具体功能)

改成MCP模式后,我觉得真的是脱裤子放屁多此一举,一个简单的问题搞得这么复杂。

当这个需求拓展生成,用户需要通过聊天来获取订票情况, 也需要知道天气情况,有没有打折酒店, 有没有打折租车服务。 这个时候就不一样了。 这就需要一个上线文(context)

多服务集成的MCP架构

当用户通过聊天界面询问:"我要从新加坡去Harstad,帮我查一下机票、当地天气、酒店优惠和租车服务",这时MCP的优势就体现出来了:

这个架构展示了MCP的真正价值:

  • 统一协议: 所有服务都通过相同的MCP协议通信
  • 并行处理: 可以同时调用多个服务
  • 上下文保持: AI Agent可以综合所有信息给出智能回答
  • 可扩展性: 轻松添加新的旅行服务(签证、保险等)

问题来了:完全可以通过Agent来实现,为什么要用MCP呢?

在企业级开发中,很多都是专有服务,有权限限制的。 我更愿意用AI Agent直接调用各种API来实现,那为什么还要引入MCP这个中间层呢?

1. 标准化与互操作性

// 传统Agent方式 - 每个API都有不同的接口
const flightData = await fetch('/api/flights', {
  method: 'POST',
  body: JSON.stringify({ from: 'SIN', to: 'EVE' }),
})

const weatherData = await axios.get('/weather/api', {
  params: { city: 'Harstad', format: 'json' },
})

const hotelData = await request.post('/booking/hotels', {
  location: 'Harstad',
  checkin: '2024-08-08',
})
// MCP方式 - 统一的协议接口
const flightResult = await mcpClient.callTool('flight-server', 'search_flights', {
  from: 'SIN',
  to: 'EVE',
})

const weatherResult = await mcpClient.callTool('weather-server', 'get_weather', {
  location: 'Harstad',
})

const hotelResult = await mcpClient.callTool('hotel-server', 'search_hotels', {
  location: 'Harstad',
  date: '2024-08-08',
})

2. 开发效率与维护成本

传统Agent方式的问题:

  • 每个API都需要单独的错误处理
  • 不同的认证机制
  • 各自的数据格式转换
  • API变更时需要修改多处代码

MCP的优势:

  • 统一的错误处理机制
  • 标准化的认证流程
  • 一致的数据格式
  • 插件化架构,易于扩展

3. 安全性和权限管理

4. 生态系统效应

想象一下,如果每个AI应用都需要:

  • 为每个服务编写专门的集成代码
  • 处理各种不同的API格式
  • 维护多套认证系统

这就像USB出现之前,每个设备都需要专门的驱动和接口。MCP就是AI工具的"USB协议"。

5. 实际的商业价值

# 传统方式:为新服务添加支持
- 研究API文档 (2-3天)
- 编写集成代码 (3-5天)
- 测试和调试 (2-3天)
- 总计:7-11天

# MCP方式:添加新服务
- 实现MCP Server (1-2天)
- 注册到MCP生态 (几分钟)
- 总计:1-2天

6. 真实场景对比

假设你要为ChatGPT添加100个新工具:

传统方式:

  • OpenAI需要为每个工具单独审核和集成
  • 开发者需要了解OpenAI的特定API格式
  • 每次工具更新都需要重新审核

MCP方式:

  • 任何符合MCP标准的工具都可以即插即用
  • 开发者只需要实现一次MCP接口
  • 工具更新对宿主应用透明

所以MCP的价值不在于技术上的不可替代性,而在于:

  • 降低集成成本
  • 提高开发效率
  • 建立生态标准
  • 促进工具复用

就像我们不会问"为什么要用HTTP,直接TCP不行吗?"一样,MCP提供的是更高层次的抽象和标准化。

MCP的安全性和授权机制详解

前面提到MCP提供统一的安全层,但具体是怎么实现的呢?

1. 传输层安全

MCP支持多种安全的传输方式:

2. 授权和权限控制

MCP在协议层面提供了多层安全控制:

// MCP Server 配置示例
interface MCPServerConfig {
  // 服务器基本信息
  name: string
  version: string

  // 授权配置
  authorization: {
    // 支持的授权类型
    type: 'bearer' | 'apikey' | 'oauth2' | 'custom'

    // API密钥配置
    apiKey?: {
      header: string // 如 'X-API-Key'
      required: boolean
    }

    // OAuth2配置
    oauth2?: {
      authUrl: string
      tokenUrl: string
      scopes: string[]
    }

    // 自定义授权处理器
    customAuth?: (request: MCPRequest) => Promise<boolean>
  }

  // 权限控制
  permissions: {
    // 工具级别的权限
    tools: {
      [toolName: string]: {
        allowedHosts: string[] // 允许访问的Host列表
        rateLimits: {
          requests: number // 每分钟请求数限制
          timeWindow: number // 时间窗口(秒)
        }
        requireAuth: boolean // 是否需要认证
      }
    }

    // 资源级别的权限
    resources: {
      read: boolean
      write: boolean
      delete: boolean
    }
  }
}

3. 实际的安全流程

问题来了:谁来提供Auth Provider?

这是MCP架构中的关键问题。Auth Provider可以由不同的角色提供,具体取决于部署场景:

具体实现场景:

1. 企业内部场景

// 企业内部 Auth Provider 实现
class EnterpriseAuthProvider implements AuthProvider {
  constructor(private ldapConfig: LDAPConfig) {}

  async validateCredentials(hostId: string, credentials: Credentials): Promise<AuthResult> {
    // 1. 连接企业LDAP/AD
    const ldapClient = new LDAPClient(this.ldapConfig)

    // 2. 验证Host应用身份
    const appIdentity = await ldapClient.authenticate(credentials.clientId, credentials.secret)

    // 3. 获取应用权限
    const permissions = await this.getAppPermissions(hostId)

    return {
      valid: true,
      hostId,
      permissions,
      token: this.generateJWT(appIdentity, permissions),
    }
  }
}

1.1 微软EntraID (Azure AD) 集成场景

// 微软EntraID Auth Provider 实现
class EntraIDAuthProvider implements AuthProvider {
  constructor(private entraConfig: EntraIDConfig) {}

  async validateCredentials(hostId: string, credentials: Credentials): Promise<AuthResult> {
    try {
      // 1. 使用Client Credentials Flow验证应用身份
      const tokenResponse = await this.getAccessToken(credentials)

      // 2. 验证令牌并获取应用信息
      const appInfo = await this.validateToken(tokenResponse.access_token)

      // 3. 获取应用的角色分配和权限
      const permissions = await this.getApplicationPermissions(appInfo.appId, hostId)

      return {
        valid: true,
        hostId,
        permissions,
        token: tokenResponse.access_token,
        expiresAt: new Date(Date.now() + tokenResponse.expires_in * 1000),
      }
    } catch (error) {
      return {
        valid: false,
        error: `EntraID authentication failed: ${error.message}`,
      }
    }
  }

  private async getAccessToken(credentials: Credentials): Promise<TokenResponse> {
    const tokenEndpoint = `https://login.microsoftonline.com/${this.entraConfig.tenantId}/oauth2/v2.0/token`

    const response = await fetch(tokenEndpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        grant_type: 'client_credentials',
        client_id: credentials.clientId,
        client_secret: credentials.clientSecret,
        scope: 'https://graph.microsoft.com/.default', // 或自定义scope
      }),
    })

    if (!response.ok) {
      throw new Error(`Token request failed: ${response.statusText}`)
    }

    return await response.json()
  }

  private async validateToken(accessToken: string): Promise<AppInfo> {
    // 验证JWT token并提取应用信息
    const decoded = jwt.verify(accessToken, this.entraConfig.publicKey)

    return {
      appId: decoded.appid,
      tenantId: decoded.tid,
      roles: decoded.roles || [],
      scopes: decoded.scp?.split(' ') || [],
    }
  }

  private async getApplicationPermissions(appId: string, hostId: string): Promise<string[]> {
    // 根据EntraID中的应用角色分配确定权限
    const rolePermissionMap = {
      'MCP.FlightSearch.Read': ['search_flights', 'get_prices'],
      'MCP.HotelSearch.Read': ['search_hotels', 'get_deals'],
      'MCP.Booking.Write': ['make_reservation', 'cancel_booking'],
      'MCP.Weather.Read': ['get_weather', 'get_forecast'],
      'MCP.Admin': ['*'], // 所有权限
    }

    const appRoles = await this.getAppRolesFromGraph(appId)
    const permissions = new Set<string>()

    appRoles.forEach((role) => {
      const rolePermissions = rolePermissionMap[role]
      if (rolePermissions) {
        rolePermissions.forEach((perm) => permissions.add(perm))
      }
    })

    return Array.from(permissions)
  }

  private async getAppRolesFromGraph(appId: string): Promise<string[]> {
    // 调用Microsoft Graph API获取应用角色
    const graphEndpoint = `https://graph.microsoft.com/v1.0/applications/${appId}/appRoleAssignments`

    const response = await fetch(graphEndpoint, {
      headers: {
        Authorization: `Bearer ${this.entraConfig.graphToken}`,
        'Content-Type': 'application/json',
      },
    })

    const data = await response.json()
    return data.value.map((assignment) => assignment.appRole.value)
  }
}

EntraID 配置示例:

# MCP Server 配置文件 - EntraID集成
mcp_server:
  name: 'travel-booking-server'
  version: '1.0.0'

  auth_provider:
    type: 'entraid'

    entraid:
      # 租户配置
      tenant_id: '${AZURE_TENANT_ID}'

      # 应用注册信息
      client_id: '${MCP_SERVER_CLIENT_ID}'
      client_secret: '${MCP_SERVER_CLIENT_SECRET}'

      # 权限范围
      scopes:
        - 'https://graph.microsoft.com/Application.Read.All'
        - 'api://mcp-travel-server/MCP.Access'

      # 角色到权限的映射
      role_permissions:
        'MCP.FlightSearch.Reader':
          permissions: ['search_flights', 'get_prices']
          rate_limits:
            requests_per_minute: 100
        'MCP.TravelAgent.Full':
          permissions: ['search_flights', 'search_hotels', 'make_reservation']
          rate_limits:
            requests_per_minute: 200
        'MCP.Admin':
          permissions: ['*']
          rate_limits:
            requests_per_minute: 500

  # Host应用的EntraID注册
  allowed_hosts:
    'claude.ai':
      entraid_app_id: '${CLAUDE_APP_ID}'
      required_roles: ['MCP.TravelAgent.Full']
    'openai.com':
      entraid_app_id: '${OPENAI_APP_ID}'
      required_roles: ['MCP.FlightSearch.Reader']

EntraID中的应用注册配置:

{
  "displayName": "MCP Travel Booking Server",
  "signInAudience": "AzureADMyOrg",
  "api": {
    "requestedAccessTokenVersion": 2,
    "oauth2PermissionScopes": [
      {
        "id": "12345678-1234-1234-1234-123456789abc",
        "adminConsentDescription": "允许应用访问MCP旅行预订服务",
        "adminConsentDisplayName": "访问MCP服务",
        "isEnabled": true,
        "type": "Admin",
        "userConsentDescription": "允许应用代表您访问旅行预订服务",
        "userConsentDisplayName": "访问旅行服务",
        "value": "MCP.Access"
      }
    ]
  },
  "appRoles": [
    {
      "id": "87654321-4321-4321-4321-210987654321",
      "allowedMemberTypes": ["Application"],
      "description": "只读访问航班搜索功能",
      "displayName": "Flight Search Reader",
      "isEnabled": true,
      "value": "MCP.FlightSearch.Reader"
    },
    {
      "id": "11111111-2222-3333-4444-555555555555",
      "allowedMemberTypes": ["Application"],
      "description": "完整的旅行代理权限",
      "displayName": "Travel Agent Full Access",
      "isEnabled": true,
      "value": "MCP.TravelAgent.Full"
    }
  ]
}

安全流程增强 - EntraID集成:

EntraID集成的优势:

  1. 企业级安全: 利用Microsoft的企业级身份管理
  2. 细粒度权限: 通过App Roles实现精确的权限控制
  3. 审计合规: 集成Azure Monitor进行全面审计
  4. SSO集成: 与企业现有SSO体系无缝集成
  5. 条件访问: 支持基于设备、位置的条件访问策略
  6. 多租户支持: 支持跨租户的B2B协作场景

2. 第三方服务场景

// Auth0 集成示例
class Auth0Provider implements AuthProvider {
  constructor(private auth0Config: Auth0Config) {}

  async validateCredentials(hostId: string, credentials: Credentials): Promise<AuthResult> {
    // 1. 调用Auth0 API验证
    const auth0Response = await fetch(`${this.auth0Config.domain}/oauth/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        client_id: credentials.clientId,
        client_secret: credentials.clientSecret,
        audience: 'mcp-api',
        grant_type: 'client_credentials',
      }),
    })

    const tokenData = await auth0Response.json()

    // 2. 解析权限范围
    const permissions = this.parseScopes(tokenData.scope)

    return {
      valid: true,
      hostId,
      permissions,
      token: tokenData.access_token,
    }
  }
}

3. MCP Server自建场景

// MCP Server 内置认证
class MCPServerAuthProvider implements AuthProvider {
  constructor(private apiKeyStore: APIKeyStore) {}

  async validateCredentials(hostId: string, credentials: Credentials): Promise<AuthResult> {
    // 1. 验证API Key
    const apiKeyInfo = await this.apiKeyStore.validate(credentials.apiKey)

    if (!apiKeyInfo.valid) {
      return { valid: false, error: 'Invalid API key' }
    }

    // 2. 检查Host白名单
    if (!apiKeyInfo.allowedHosts.includes(hostId)) {
      return { valid: false, error: 'Host not authorized' }
    }

    // 3. 返回权限信息
    return {
      valid: true,
      hostId,
      permissions: apiKeyInfo.permissions,
      token: this.generateSessionToken(hostId, apiKeyInfo),
    }
  }
}

实际部署配置示例:

# MCP Server 配置文件
mcp_server:
  name: 'travel-booking-server'
  version: '1.0.0'

  # Auth Provider 配置
  auth_provider:
    # 选择认证提供方
    type: 'enterprise' # 可选: enterprise, auth0, okta, builtin, aws_cognito

    # 企业内部认证配置
    enterprise:
      ldap_url: 'ldap://company.local:389'
      base_dn: 'dc=company,dc=local'
      admin_user: 'cn=admin,dc=company,dc=local'
      admin_password: '${LDAP_ADMIN_PASSWORD}'

    # 或者使用Auth0
    auth0:
      domain: 'https://company.auth0.com'
      client_id: '${AUTH0_CLIENT_ID}'
      client_secret: '${AUTH0_CLIENT_SECRET}'
      audience: 'mcp-travel-api'

    # 或者内置API Key方式
    builtin:
      api_keys:
        - key: '${CLAUDE_API_KEY}'
          name: 'Claude AI'
          permissions: ['search_flights', 'search_hotels']
          rate_limits:
            requests_per_minute: 100
        - key: '${CHATGPT_API_KEY}'
          name: 'ChatGPT'
          permissions: ['search_flights', 'get_weather']
          rate_limits:
            requests_per_minute: 50

  # 权限控制
  permissions:
    default_permissions: ['search_flights']
    host_specific:
      'claude.ai':
        permissions: ['search_flights', 'search_hotels', 'make_reservation']
        rate_limits:
          requests_per_minute: 200
      'openai.com':
        permissions: ['search_flights', 'get_weather']
        rate_limits:
          requests_per_minute: 100

总结:Auth Provider的选择

场景Auth Provider优势适用情况
企业内部LDAP/AD/SSO与现有系统集成大型企业内部应用
SaaS应用Auth0/Okta专业身份服务需要企业级认证功能
简单部署内置API Key配置简单快速小团队或原型项目
云原生AWS Cognito/Azure AD云平台深度集成已使用云服务的项目
开源项目JWT + 自建完全控制开源或完全自主可控

所以Auth Provider不是MCP协议本身提供的,而是由部署MCP Server的组织根据自己的安全要求和现有基础设施来选择和配置的。

4. 具体的安全实现

4.1 认证令牌管理

// MCP Client端的安全实现
class SecureMCPClient {
  private authTokens: Map<string, AuthToken> = new Map()

  async connectToServer(serverConfig: MCPServerConfig) {
    // 1. 验证服务器证书
    const serverInfo = await this.validateServerCertificate(serverConfig)

    // 2. 获取认证令牌
    const authToken = await this.authenticate(serverConfig.authorization)

    // 3. 建立安全连接
    const connection = await this.establishSecureConnection(serverConfig, authToken)

    return connection
  }

  private async authenticate(authConfig: AuthConfig): Promise<AuthToken> {
    switch (authConfig.type) {
      case 'oauth2':
        return await this.handleOAuth2Flow(authConfig.oauth2)
      case 'apikey':
        return await this.handleAPIKeyAuth(authConfig.apiKey)
      case 'bearer':
        return await this.handleBearerTokenAuth(authConfig.bearer)
      default:
        throw new Error('Unsupported auth type')
    }
  }
}

4.2 权限验证和速率限制

// MCP Server端的安全中间件
class MCPSecurityMiddleware {
  async validateRequest(request: MCPRequest, context: SecurityContext): Promise<boolean> {
    // 1. 验证Host身份
    const hostValid = await this.validateHost(context.hostId, context.credentials)
    if (!hostValid) return false

    // 2. 检查工具权限
    const toolPermissions = this.getToolPermissions(request.method, context.hostId)
    if (!toolPermissions.allowed) return false

    // 3. 速率限制检查
    const rateLimitOk = await this.checkRateLimit(context.hostId, request.method)
    if (!rateLimitOk) return false

    // 4. 参数安全验证
    const paramsValid = await this.validateParameters(request.params)
    if (!paramsValid) return false

    return true
  }

  private async checkRateLimit(hostId: string, method: string): Promise<boolean> {
    const key = `${hostId}:${method}`
    const current = (await this.redis.get(key)) || 0
    const limit = this.rateLimits[method] || 100

    if (current >= limit) {
      await this.logSecurityEvent('rate_limit_exceeded', { hostId, method })
      return false
    }

    await this.redis.incr(key)
    await this.redis.expire(key, 60) // 1分钟窗口
    return true
  }
}

5. 数据隐私和审计

// 安全审计系统
class MCPAuditLogger {
  async logSecurityEvent(event: SecurityEvent) {
    const auditRecord = {
      timestamp: new Date().toISOString(),
      hostId: event.hostId,
      serverId: event.serverId,
      action: event.action,
      success: event.success,
      ipAddress: event.ipAddress,
      userAgent: event.userAgent,

      // 敏感数据处理
      requestData: this.sanitizeData(event.requestData),
      responseData: this.sanitizeData(event.responseData),

      // 安全相关
      authMethod: event.authMethod,
      permissions: event.permissions,
      rateLimitStatus: event.rateLimitStatus,
    }

    // 写入安全审计日志
    await this.writeToAuditLog(auditRecord)

    // 异常检测
    if (this.detectSuspiciousActivity(auditRecord)) {
      await this.triggerSecurityAlert(auditRecord)
    }
  }

  private sanitizeData(data: any): any {
    // 移除敏感信息:密码、API密钥、个人信息等
    return this.deepClone(data, this.sensitiveFieldsFilter)
  }
}

6. 与传统API相比的安全优势

安全层面传统API调用MCP协议
认证标准化每个API不同的认证方式统一的认证协议
权限粒度通常是API级别工具级别 + 参数级别
审计追踪分散在各个服务集中式审计日志
速率控制各自实现协议层统一控制
数据隐私依赖各服务实现协议层数据脱敏
异常检测需要额外开发内建安全监控

所以MCP的安全层不仅仅是一个"包装器",而是一个完整的安全框架,提供了:

  • 标准化的认证和授权机制
  • 细粒度的权限控制
  • 统一的安全审计
  • 内建的威胁检测
  • 数据隐私保护

理论上说MCP提供了比传统API集成更安全的解决方案。

--- End ---