# MCP Gateway

[English](mcp.md) | [简体中文](mcp.zh-CN.md)

**相关文档**: [配置](configuration.md) | [架构](architecture.md) | [Console API](console-api.md) | [ADR-008](adr/008-mcp-gateway-architecture.zh-CN.md)

## 概述

Protoflux 内置 **MCP (Model Context Protocol) Gateway**，使 LLM 客户端能够发现和调用外部 MCP 服务器的工具。网关作为统一代理，聚合多个上游 MCP 服务器，通过单一端点暴露工具，并提供细粒度访问控制和生产级可靠性保障。

```mermaid
graph LR
    Client["LLM 客户端<br/>(Claude, Cursor)"]
    GW["Protoflux MCP Gateway<br/>• ACL 过滤<br/>• 熔断器<br/>• 速率限制<br/>• PII 脱敏<br/>• 审计日志<br/>• BM25 搜索<br/>• SSE 通知"]
    SrvA["MCP 服务器 A<br/>(GitHub)"]
    SrvB["MCP 服务器 B<br/>(Slack)"]

    Client -- "MCP" --> GW
    GW -- "MCP" --> SrvA
    GW -- "MCP" --> SrvB
```

### 使用场景

- **工具聚合**: 连接多个 MCP 服务器（GitHub、Slack、数据库），暴露统一的工具集
- **访问控制**: 通过 ACL 规则限制每个用户或组可访问的工具
- **可靠性保障**: 每服务器熔断器、并发限制和 RPS 速率限制保护上游服务
- **智能搜索**: 内置 `search_tools` 元工具，基于 BM25 排序帮助 LLM 自主发现相关工具
- **合规性**: 工具参数 PII 脱敏 + 结构化审计日志记录所有工具调用
- **实时更新**: SSE 通知流在工具增删时推送 `notifications/tools/list_changed` 事件

## 客户端接入端点

网关暴露两个端点：

| 方法 | 路径 | 说明 |
|------|------|------|
| `POST` | `/mcp` | JSON-RPC 2.0 请求/响应（Streamable HTTP 传输） |
| `GET` | `/mcp/sse` | Server-Sent Events 流，用于实时通知 |

### 认证

所有请求需要在 `Authorization` 请求头中提供有效的 Protoflux Access Key：

```bash
Authorization: Bearer your-api-key-here
```

Key 通过内存中的 SHA-256 索引验证（O(1) 查找）。禁用的 Key 在索引构建时被排除，视为无效。

### 支持的 JSON-RPC 方法

| 方法 | 说明 |
|------|------|
| `initialize` | 返回网关能力和协议版本（`2025-03-26`） |
| `tools/list` | 返回 ACL 过滤后的工具列表及内置 `search_tools` 元工具 |
| `tools/call` | 调用上游 MCP 服务器上的工具（或内置 `search_tools`） |
| `tools/search` | 编程式 BM25 全文搜索所有已注册工具 |

### 请求格式 (JSON-RPC 2.0)

**初始化**:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {}
}
```

响应包含 `"protocolVersion": "2025-03-26"`（面向客户端）。上游服务器使用 `"2024-11-05"` — 网关有意进行协议版本翻译。

**列出工具**:

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}
```

返回所有 ACL 允许的工具以及内置的 `search_tools` 元工具。

**调用工具**:

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "github.create_issue",
    "arguments": {
      "title": "Bug 报告",
      "body": "问题描述"
    }
  }
}
```

工具名称遵循 `{server_name}.{tool_name}` 约定。网关将调用路由到对应的上游服务器。

**搜索工具**（编程式 API）:

```json
{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/search",
  "params": {
    "query": "创建 github issue",
    "limit": 5
  }
}
```

### 示例: curl

```bash
curl -X POST http://localhost:7890/mcp \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list",
    "params": {}
  }'
```

### 示例: Claude Code

添加到 `.mcp.json` 或项目设置中：

```json
{
  "mcpServers": {
    "protoflux": {
      "type": "sse",
      "url": "http://localhost:7890/mcp/sse",
      "headers": {
        "Authorization": "Bearer your-api-key"
      }
    }
  }
}
```

或使用 Streamable HTTP（如果客户端支持）：

```json
{
  "mcpServers": {
    "protoflux": {
      "type": "streamable-http",
      "url": "http://localhost:7890/mcp",
      "headers": {
        "Authorization": "Bearer your-api-key"
      }
    }
  }
}
```

### 示例: Claude Desktop

在 `claude_desktop_config.json` 中添加：

```json
{
  "mcpServers": {
    "protoflux": {
      "url": "http://localhost:7890/mcp/sse",
      "headers": {
        "Authorization": "Bearer your-api-key"
      }
    }
  }
}
```

## SSE 通知

`GET /mcp/sse` 端点提供遵循 MCP SSE 传输规范的实时事件流：

1. 客户端使用 `Authorization: Bearer` 请求头连接
2. 网关发送 `endpoint` 事件，指示 POST URL（`/mcp`）
3. 当工具被添加、删除或更新时，网关推送包含 `notifications/tools/list_changed` JSON-RPC 通知的 `message` 事件
4. 每 30 秒发送 keepalive ping（`: ping`）以维持连接穿越代理

通知通过统一的 `BroadcastBackend<McpNotification>` 抽象分发。单实例模式使用内存 broadcast channel；多实例模式（Redis）通过 Redis Pub/Sub 跨实例传播，并带有防环机制：远端消息触发本地索引重建，通过 `publish_local()` 通知本地 SSE 客户端，不会重新发布回 Redis。详见[部署文档 → 统一事件广播架构](deployment.zh-CN.md#统一事件广播架构redis-pubsub)。

如果广播通道溢出（客户端消费过慢），丢失的事件会被记录日志，客户端在下一次 `tools/list` 调用时自动恢复。

## 配置

MCP 服务器通过 Web Console 或 Console API 管理。业务配置（MCP 服务器、API Key、ACL）存储在数据库中，运行时可修改，无需重启。

### Web Console

在侧边栏导航到 **MCP 服务器** 页面，添加、编辑或删除 MCP 服务器配置。

### Console API

| 方法 | 端点 | 说明 |
|------|------|------|
| `GET` | `/console/api/mcp-servers` | 列出所有 MCP 服务器 |
| `POST` | `/console/api/mcp-servers` | 创建 MCP 服务器 |
| `PUT` | `/console/api/mcp-servers/{name}` | 更新 MCP 服务器 |
| `DELETE` | `/console/api/mcp-servers/{name}` | 删除 MCP 服务器 |

### 服务器配置字段

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | string | 是 | 唯一服务器标识（用作工具名前缀） |
| `description` | string | 否 | 人类可读的描述 |
| `transport` | enum | 是 | 上游通信协议：`streamable_http` 或 `sse` |
| `endpoint` | string | 是 | 上游 MCP 服务器 URL |
| `auth_header` | string | 否 | 上游 Authorization 请求头值 |
| `extra_headers` | object | 否 | 发送到上游的额外 HTTP 请求头 |
| `defer_loading` | boolean | 否 | 跳过初始工具发现，直到首次 `tools/call` 时加载；减少启动开销 |
| `tags` | array | 否 | 标签，用于分类和 ACL 基于标签的过滤 |
| `priority` | integer | 否 | 上下文预算优先级（越高越优先，默认：100） |
| `connection_type` | enum | 是 | 会话模型：`stateful`、`stateless` 或 `rest_bridge` |
| `idle_timeout_secs` | integer | 否 | 连接空闲超时（默认：300 秒） |
| `max_concurrency` | integer | 否 | 此服务器的最大并发请求数（通过信号量强制执行） |
| `rate_limit` | object | 否 | 速率限制：`{ "rps": <每秒请求数> }` |
| `extra_config` | object | 否 | 传递给传输适配器的任意 JSON |

### Transport 与 Connection Type 的区别

这是两个独立的维度：

- **Transport**（`streamable_http` / `sse`）：与上游 MCP 服务器通信使用的线路协议。
- **Connection Type**（`stateful` / `stateless` / `rest_bridge`）：管理客户端隔离和连接池化的会话模型。

例如，一个服务器可以使用 `sse` 传输配合 `stateless` 连接类型 — 即使用 SSE 线路协议但在客户端之间共享连接。

### 示例: 添加 GitHub MCP 服务器

在 Console 中，打开 **MCP 服务器** → 点击 **添加 MCP 服务器**，填写：

- **名称**：`github`
- **描述**：`GitHub MCP 服务器，用于仓库管理`
- **传输协议**：`Streamable HTTP`
- **连接类型**：`Stateful`
- **端点地址**：`https://mcp.github.com`
- **认证头**：`Bearer ghp_xxxxxxxxxxxx`
- **标签**：`development, vcs`
- **优先级**：`100`

> **注意**：`max_concurrency`（值 `10`）与 `rate_limit`（值 `{ "rps": 50 }`）在 Console UI 中没有对应字段——仅 Console API 可设。如需每服务器并发上限或 RPS 限流，请通过 Console API（`POST /console/api/mcp-servers`）设置。

## 传输协议

### Streamable HTTP

标准 HTTP 传输，使用 JSON-RPC 负载。适用于大多数 MCP 服务器。

- **有状态 (Stateful)**: 为每个客户端维护持久会话。每个 API Key 拥有隔离的连接池和独立的 cookie jar。
- **无状态 (Stateless)**: 无会话状态。连接池化并在客户端之间共享。

### SSE (Server-Sent Events)

适用于使用 SSE 进行服务器到客户端流式传输的 MCP 服务器。网关流程：

1. 向服务器的 SSE 端点打开 GET SSE 连接
2. 接收 `endpoint` 事件，获取 POST URL
3. 通过 POST 发送 JSON-RPC 请求
4. 通过 SSE 流接收响应

### REST Bridge

使用 OpenAPI 规范将 REST API 暴露为 MCP 工具。适用于集成不支持 MCP 的传统 API。

## 访问控制 (ACL)

MCP 工具访问控制在 **Access Key Group** 级别配置。每个组可通过 `mcp_tool_acl` 字段指定三个过滤维度：

| 字段 | 说明 |
|------|------|
| `allowed_tools` | 工具名称白名单（支持通配符），为空表示不限制 |
| `denied_tools` | 工具名称黑名单，**优先级最高**，始终生效 |
| `allowed_tags` | 服务器标签白名单，非空时工具必须属于至少一个匹配的服务器 |

### 通配符语法

| 模式 | 匹配规则 | 示例 |
|------|----------|------|
| `*` | 匹配所有工具 | — |
| `server.*` | 匹配 `server` 本身及所有 `server.<tool>` | `github.*` → `github`, `github.create_issue`, `github.list_repos` |
| 精确名称 | 仅匹配该工具 | `slack.send_message` |

### 评估优先级

```
denied_tools（最高）→ allowed_tools → allowed_tags → 默认允许
```

1. 如果工具名匹配 `denied_tools` 中的任何模式 → **拒绝**
2. 如果 `allowed_tools` 非空且工具名不匹配其中任何模式 → **拒绝**
3. 如果 `allowed_tags` 非空且工具所属服务器没有任何匹配标签 → **拒绝**
4. 否则 → **允许**

当工具被 ACL 拒绝时，网关会记录审计日志（`AclViolation` 事件）并增加 `protoflux_mcp_acl_filtered_total` 指标计数。ACL 快照缓存 300 秒，配置热重载后自动失效。

### Console UI 配置

在 Web Console 中：**Access Keys → Key Groups → 编辑组 → MCP Access Control**

- **Allowed MCP Tools**: 勾选 `*`（全部）或按服务器勾选 `server.*`
- **Denied MCP Tools**: 按服务器勾选要禁止的 `server.*`
- **Allowed MCP Tags**: 输入逗号分隔的标签列表

> ⚠️ MCP ACL 区域仅在系统中存在至少一个 MCP 服务器时显示。请先在 **MCP Servers** 页面添加服务器。

### API 示例

```bash
curl -X PUT http://localhost:7890/console/api/access-key-groups \
  -H "Authorization: Bearer console-token" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "developers",
    "models": ["gpt-4", "claude-3-opus"],
    "mcp_tool_acl": {
      "allowed_tools": ["github.*", "slack.send_message"],
      "denied_tools": ["github.delete_repository"],
      "allowed_tags": ["development"]
    }
  }'
```

## Schema 清洗

来自上游 MCP 服务器和 LLM 客户端的工具 schema 经过**两层防御**清洗，防止 Provider 特定的校验错误（如 Moonshot 拒绝非字符串的 `description` 字段）。

### 第一层：入口规范化（Ingestion Normalization）

从上游 MCP 服务器接收工具时（定期同步期间），schema 在缓存前被规范化为标准 JSON Schema 格式：

- 非字符串 `description` → 强制转为字符串或删除
- 非对象属性 schema（如 `"[REDACTED]"`）→ 替换为 `{"type": "string"}`
- 非法 `type` 值 → 删除
- 深度限制（32 层）防止恶意深层嵌套导致栈溢出

这确保所有缓存的工具 schema 结构合法，不受上游质量影响。规范化是幂等的——合法 schema 不会被修改。

### 第二层：出口适配（Egress Adaptation）

向特定 LLM Provider 翻译请求时，Provider 专属的 schema profile 将规范化后的 schema 适配为该 Provider 支持的 JSON Schema 子集：

| Provider | 关键适配 |
|----------|---------|
| **Google** | `const`→`enum`、空 enum 过滤、`anyOf`/`oneOf`→取第一个变体、`allOf`→深度合并、`examples`→`example`、剥离约 20 个不支持的关键字 |
| **DashScope** | 剥离 `$` 前缀关键字（`$schema`、`$ref`、`$defs` 等）和 `additionalProperties` |
| **OpenAI** | 剥离 `$` 前缀元数据关键字和 `definitions` |
| **AWS Converse** | 剥离 `$` 前缀元数据关键字和 `definitions` |
| **Anthropic** | 透传（最丰富的 schema 支持）；将单数 `example` 转为 `examples` |

Profile 通过 Strategy 模式实现（`SchemaProfile` trait）。新增 Provider 只需添加一个 profile 文件和一个 match arm，无需修改已有代码。

> ⚠️ Profile 对不支持的特性使用 best-effort 转换（如将 Google 的 `anyOf` 展平为第一个变体）。这比 API 拒绝更好，但可能收窄 schema 语义。当 Provider 扩展其 schema 支持时，应及时更新对应 profile。

## 生产防护机制

每个 `tools/call` 请求在到达上游服务器之前，会依次经过以下生产防护机制：

### 1. 熔断器（每服务器）

每个 MCP 服务器拥有独立的三态熔断器：

- **Closed**（正常）：请求正常通过。连续失败被计数。
- **Open**（断开）：请求立即被拒绝。`reset_timeout`（默认 30 秒）过后转入 HalfOpen。
- **HalfOpen**（探测）：允许一个请求通过。成功则关闭熔断器；失败则重新断开。

默认阈值：5 次连续失败触发断开，2 次连续成功恢复闭合。

熔断器使用异步友好的 API（`pre_check()` / `record_success()` / `record_failure()`），将状态检查与异步操作分离，避免跨 await 持有互斥锁。

熔断器状态变更通过 Prometheus gauge `protoflux_mcp_circuit_breaker_state` 上报（0=Closed, 1=Open, 2=HalfOpen）。

### 2. 并发限制（每服务器）

配置 `max_concurrency` 后，通过信号量强制执行限制。超额请求立即收到错误响应，不会联系上游服务器。

### 3. RPS 速率限制器（每服务器）

配置 `rate_limit.rps` 后，原子令牌桶限制每秒请求数。使用 `tokio::time::Instant`（单调时钟，不受系统时钟回拨影响）配合 CAS 循环补充，实现无锁并发访问。

### 4. PII 脱敏

启用后，工具调用参数会被递归扫描 PII 模式（邮箱、电话号码、SSN、信用卡号、IP 地址）。仅对字符串叶子节点进行脱敏 — JSON 结构（键顺序、嵌套、非字符串类型）保持不变。

### 5. 指标记录

每次工具调用（成功或失败）后：
- 延迟直方图：`protoflux_mcp_tool_call_duration_seconds`
- 成功/失败计数器：`protoflux_mcp_tool_calls_total`
- 熔断器状态 gauge 更新

### 6. 审计日志

所有工具调用（成功和失败）及 ACL 违规均通过结构化审计日志记录，包含：access key、服务器名、工具名、结果和耗时。

## 工具搜索

网关提供两种工具搜索方式：

### 内置 `search_tools` 元工具

注入到每个 `tools/list` 响应中，作为常规工具呈现。LLM 在需要发现工具时自主调用：

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search_tools",
    "arguments": {
      "query": "创建 github issue",
      "limit": 5
    }
  }
}
```

结果经过 ACL 过滤，受限 Key 看不到被禁止的工具。

### 编程式 `tools/search` 方法

面向需要结构化搜索结果的客户端的直接 JSON-RPC 方法：

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/search",
  "params": {
    "query": "创建 github issue",
    "limit": 5
  }
}
```

搜索延迟记录在 `protoflux_mcp_search_duration_seconds` 中。

### BM25 排序参数

- **名称加权**: 工具名称权重是描述的 2 倍
- **标签匹配**: 服务器标签匹配查询词的工具获得加分
- **Top-K 限制**: 仅返回指定数量内的最相关结果

## 后台同步

网规定期从上游 MCP 服务器同步工具定义：

- **同步间隔**: 可配置（默认：60 秒）
- **延迟加载**: 设置了 `defer_loading: true` 的服务器跳过初始工具发现；工具在首次 `tools/call` 时加载，减少启动开销
- **变更通知**: 每次同步导致工具集变化后，向所有已连接的 SSE 客户端广播 `notifications/tools/list_changed` 事件

## 指标

MCP Gateway 指标通过 `/metrics` 端点与核心网关指标一起暴露：

| 指标 | 类型 | 说明 |
|------|------|------|
| `protoflux_mcp_connections_total` | Counter | 建立的 MCP 连接总数 |
| `protoflux_mcp_connections_active` | Gauge | 当前活跃的连接数 |
| `protoflux_mcp_tool_calls_total` | Counter | 工具调用总数（标签：server, tool, status） |
| `protoflux_mcp_tool_call_duration_seconds` | Histogram | 工具调用延迟 |
| `protoflux_mcp_context_budget_used` | Gauge | 上下文使用的 token 数 |
| `protoflux_mcp_context_tools_loaded` | Gauge | 上下文中加载的工具数 |
| `protoflux_mcp_context_lru_evictions_total` | Counter | LRU 驱逐次数 |
| `protoflux_mcp_search_queries_total` | Counter | 处理的搜索查询数 |
| `protoflux_mcp_search_duration_seconds` | Histogram | 搜索延迟 |
| `protoflux_mcp_acl_filtered_total` | Counter | ACL 过滤的工具数 |
| `protoflux_mcp_circuit_breaker_state` | Gauge | 每服务器熔断器状态（0=Closed, 1=Open, 2=HalfOpen） |
| `protoflux_mcp_errors_total` | Counter | 错误总数 |

## 架构

详细设计决策参见 [ADR-008: MCP Gateway 架构](adr/008-mcp-gateway-architecture.zh-CN.md)。

### 请求流程 (tools/call)

```mermaid
flowchart TD
    A["客户端 POST /mcp"] --> B["认证<br/>Bearer token → SHA-256 索引查找"]
    B --> C["加载上下文（按 API Key）<br/>• 检查 ACL 快照 TTL<br/>• 如果过期则刷新 ACL<br/>• 根据预算加载/驱逐工具"]
    C --> D["解析 JSON-RPC 请求"]
    D --> E["ACL 过滤<br/>• allowed_tools / denied_tools<br/>• allowed_tags / denied_tags<br/>• 违规时记录审计 + 指标"]
    E --> F["生产防护机制<br/>1. 熔断器 pre_check<br/>2. 并发信号量<br/>3. RPS 令牌桶<br/>4. PII 参数脱敏"]
    F --> G{"路由到上游"}
    G --> H1["HttpMcpServer<br/>(Streamable HTTP)"]
    G --> H2["SseMcpServer<br/>(SSE)"]
    G --> H3["RestBridgeServer<br/>(REST Bridge)"]
    H1 --> I["调用后处理<br/>• CB record_success/failure<br/>• 指标记录<br/>• 审计日志"]
    H2 --> I
    H3 --> I
    I --> J["返回 JSON-RPC 响应"]
```

### 连接隔离

对于有状态服务器，网关维护客户端隔离：

```mermaid
graph LR
    KA["API Key A"] --> CA["客户端 A<br/>• 隔离的 HTTP 客户端<br/>• 隔离的 cookie jar<br/>• 隔离的连接池<br/>• 隔离的上下文状态"]
    KB["API Key B"] --> CB["客户端 B<br/>• 隔离的 HTTP 客户端<br/>• 隔离的 cookie jar<br/>• 隔离的连接池<br/>• 隔离的上下文状态"]
    CA --> Srv["MCP 服务器"]
    CB --> Srv
```

## 限制

- **协议范围**: 当前仅支持 tools。Resources、Prompts 和 Sampling 扩展尚未实现。
- **REST Bridge**: 需要 OpenAPI 3.x 规范。不支持 Swagger 2.0。
- **上下文预算**: Token 估算是近似值（基于 tiktoken-rs）。实际使用量可能有所不同。
- **多实例部署**: 上下文状态是本地的。多实例部署建议使用动态绑定或 Redis 支持的状态。
- **速率限制粒度**: 仅支持每服务器 RPS 和 max_concurrency。每 Key 或全局限速尚未实现。

## 故障排除

### "Tool not found" 错误

- 检查工具是否被 Access Key Group 的 ACL 允许
- 验证工具名称格式：`{server_name}.{tool_name}`
- 使用 `search_tools` 元工具发现可用工具
- 如果使用基于标签的 ACL，检查服务器标签

### "Circuit breaker open" 错误

- 上游服务器已超过失败阈值（默认：5 次连续失败）
- 等待 reset_timeout（默认 30 秒）— 熔断器会自动探测
- 检查上游服务器健康状态和网络连通性
- 监控 `protoflux_mcp_circuit_breaker_state` 指标查看状态转换

### "Concurrency limit exceeded" / "Rate limit exceeded"

- 在服务器配置中增加 `max_concurrency` 或 `rate_limit.rps`
- 将负载分散到多个服务器实例
- 检查是否有单个客户端占用了过多容量

### 连接超时

- 在服务器配置中增加 `idle_timeout_secs`
- 检查上游服务器健康状态
- 验证到上游端点的网络连接

### 上下文预算超出

- 在网关配置中增加全局上下文预算
- 将低优先级工具标记为 `defer_loading: true`
- 减少配置的 MCP 服务器数量
