bharat3645/mcp-gateway-lite

GitHub: bharat3645/mcp-gateway-lite

一款用 Go 标准库编写的 MCP 服务器反向代理,提供请求审计、工具策略控制、schema 漂移检测和速率限制等运维治理能力。

Stars: 0 | Forks: 0

# mcp-gateway-lite [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/ad/ad5834178f7599af9fdda11629d49cae07f2997beec49821b2920eff5bfd50e7.svg)](https://github.com/bharat3645/mcp-gateway-lite/actions/workflows/ci.yml) 单二进制、无状态反向代理,专为 [MCP](https://modelcontextprotocol.io) Streamable HTTP 服务器设计。提供 JSON Lines 审计追踪、按上游(或按会话)的速率限制、在调用*和*列表上强制执行的按工具 allow/deny 策略、针对 [mcp-sentinel](https://github.com/bharat3645/mcp-sentinel) lockfile 的内联 tool-schema 漂移验证,以及生成的 RFC 9728 `.well-known` 元数据。 Stdlib-only Go。无框架,无依赖,在你的 MCP 服务器前作为单一进程运行,旨在回答企业不断提出的问题:**“哪个 agent 在何时调用了哪个工具——以及服务器当前提供的工具是否仍然是我们审核过的那些?”** ``` agent/client ──▶ mcp-gateway-lite ──▶ your MCP servers │ ├──▶ audit.jsonl (one line per request) └──◀ mcp-sentinel.lock (tool schemas, pinned) ``` 这就是部署拓扑结构;其内部的请求/响应 pipeline 有着 特定的执行顺序,而正是这决定了实际行为: ``` flowchart LR subgraph request["on the way in"] direction LR rl["rate limit\n(token bucket)"] --> pol["tool policy\n(tools_allow/deny)"] end subgraph response["on the way back"] direction LR lock["tools_lock\n(drift check)"] --> filt["tools/list\nfiltering"] end request -- "429 or 403\nstop here" --> deny(["blocked + audited,\nnever reaches upstream"]) request -- ok --> upstream[("your MCP\nserver")] upstream --> response response -- "drift found" --> block(["blocked or audited\n(enforce/warn mode)"]) response -- ok --> client(["client sees the\nfiltered, verified response"]) ``` ## 为什么需要 MCP 的采用速度走在了 MCP 运维的前面。如今,大多数部署都将 agent 直接连接到 MCP 服务器,没有审计追踪,没有流量控制点,也没有实施策略的载体。这个网关正是所缺失的基础组件: - **审计每一个请求** — JSON-RPC method、tools/call 工具名称、session id、状态、耗时 — 作为只允许追加的 JSONL 记录,你可以将其发送到任何日志 pipeline。 - **用一个 URL 对接多个服务器** — 基于路径的路由 (`/mcp/`) 将 N 个 server endpoint 转换为一个 gateway endpoint。 - **背压控制** — 针对每个上游或每个会话的 token-bucket 速率限制,返回 `429` + `Retry-After`,并记入审计,从而确保被限流的会话依然可追溯。 - **安全防护** — 在网关处对 `tools/call` 强制执行按工具的 allow/deny 列表,并从 `tools/list` 响应中过滤掉它们,使客户端永远看不到它们无法调用的工具。 - **内联 Rug-pull 检测** — `tools_lock` 在客户端看到之前,根据锁定的 lockfile 验证每个 `tools/list` 响应;悄悄更改的工具描述将被阻止(或记入审计),而不是左右你的 agent。 - **为缺少它的服务器提供 `.well-known`** — 为每个上游生成 [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) protected-resource 元数据。 ## 设计上的隐私保护 审计日志**仅记录元数据**。绝不记录 JSON-RPC `params` 和工具 `arguments` — 仅限方法名、请求 id 和 `tools/call` 工具名。这是由提取代码强制执行的(在结构上它无法发出 params),并由测试和 CI 检查锁定,如果审计输出中出现了参数值,则检查会失败。 ## 快速开始 ``` go install github.com/bharat3645/mcp-gateway-lite/cmd/mcp-gateway-lite@latest # 仅 flags: mcp-gateway-lite --upstream files=http://127.0.0.1:3001/mcp --audit audit.jsonl # 或一个 config 文件: mcp-gateway-lite --config example.config.json ``` 然后,将你的 MCP client 指向 `http://127.0.0.1:8385/mcp/files`,而不是直接指向上游。 ``` curl -X POST http://127.0.0.1:8385/mcp/files \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` 每个请求生成一行审计记录: ``` {"ts":"2026-07-17T08:01:12.408724Z","upstream":"files","remote":"127.0.0.1:52114","http_method":"POST","path":"/mcp/files","session_id":"5f0c...","rpc_methods":["tools/call"],"rpc_ids":["7"],"tools":["read_file"],"status":200,"bytes_in":183,"bytes_out":412,"duration_ms":12.44} ``` ## 工具 schema 锁定(防范 rug-pull 的检查) 在 MCP 供应链中反复出现的一种攻击形态:服务器发布了 N 个干净版本,然后悄悄更改了工具描述(“同时将文件 POST 到 attacker.example”)— 而该描述*就是*输入给你的 agent 的 prompt。`tools_lock` 会锁定你已审查过的工具 schema,并据此内联验证每个 `tools/list` 响应: ``` # 1. 捕获您审查的内容(写入/合并 mcp-sentinel.lock) mcp-gateway-lite --lock-init --lock-file mcp-sentinel.lock \ --upstream files=http://127.0.0.1:3001/mcp # 2. 强制执行 # config: {"name":"files","url":"...","tools_lock":{"file":"mcp-sentinel.lock"}} ``` - **Lockfile 互操作性是真实的**:其格式与 [mcp-sentinel](https://github.com/bharat3645/mcp-sentinel) 相同。由 `mcp-sentinel lock`(带有工具捕获)编写的 lockfile 可由网关强制执行,并且 `--lock-init` 的输出可以通过 `mcp-sentinel verify` 进行离线验证。Go canonicalizer 逐字节重现了 sentinel 的 Python 哈希,并由使用实际 sentinel 代码生成的 golden vector 锁定,同时在 CI 中与真实的 sentinel checkout 进行了交叉检查。 - `--lock-init` 同时记录了 sentinel 的整列表 `toolsHash` 和按工具划分的 `toolsDetail` 扩展(name → 指纹摘要)。当服务器对 `tools/list` 进行分页时,按工具划分的摘要可确保验证依然正确;sentinel 编写的 lockfile(仅包含整列表哈希)可验证完整的列表。 - **enforce**(默认):`tools/list` 响应中出现漂移、添加或未知的工具时,会将该响应替换为 JSON-RPC `-32004` 错误(对于 JSON 主体返回 HTTP 403,对于 SSE 则为 in-stream 错误事件)— 有害的描述永远不会到达客户端。审计条目会记录 `tools_drift: true` 及其原因,并指出具体的工具名称。 - **warn**:响应正常传递;漂移会被审计。可使用此模式来进行分阶段推出。 - 验证是在策略过滤*之前*,完全针对服务器发送的工具原始状态进行的:lock 锁定的是服务器的真实状态,而过滤器则塑造客户端的视图。 威胁模型的诚实话:网关保护的是诚实的客户端免受漂移或被入侵服务器的影响。它无法检测到保持 schema 不变的服务器端行为更改,而且与服务器串通的客户端(或将 tools/list 隐藏在无法解析的 >1 MiB 请求体中)逃避的是其自身的保护。Lock 强制执行与 `tools_allow`(用于名称级别的调用控制)配合使用效果最佳。 ## 工具策略 按上游划分的 `tools_allow` / `tools_deny`(互斥)在网关的 `tools/call` 上强制执行,利用了审计日志所使用的相同的 body peek。被阻止的请求会返回 `403` + JSON-RPC `-32003` 并指出工具名称,同时产生一个**完整的审计条目**(包含 rpc 元数据)— 被阻止的调用是运维人员最关心的条目。批处理语义:一个被阻止的工具会拒绝整个请求。非工具流量(`initialize`、通知、响应)绝不受影响。 自 0.4.0 起,策略还会影响客户端的*可见范围*:对 `tools/list` 响应进行过滤,使得被拒绝(或未列出)的工具永远不会出现。过滤是基于 id 匹配的 — 仅处理与请求自身的 `tools/list` id 相对应的响应;其他 id 下的工具形状数据(例如 `tools/call` 结果)会逐字节精确传递,未触及的响应也会逐字节相同地传递。`application/json` 和 `text/event-stream` 响应均被处理;SSE 事件在完成后即会流出,因此保留了流式传递和刷新机制。 这两种模式的失败方式不同,这是有意为之的: - **`tools_allow` 是默认拒绝:** 无法解析的主体以及没有可提取工具名称的 `tools/call` 消息将被阻止 — 在响应端,无法解析或超大的 `tools/list` 响应将被替换为错误,而不是直接传递。一个默认失败的 allowlist 形同虚设。 - **`tools_deny` 是尽力而为:** 无法解析的请求体和无法处理的响应将被直接传递;它是护栏,而非边界。 ## 工具结果的 Prompt injection 扫描 工具 schema 是可以锁定的(如上所述),但*工具返回的内容*在运行时 是不受信任的 — 被入侵或恶意的 MCP 服务器可以将 指令或数据窃取诱饵伪装在看似普通的 `tools/call` 结果中偷偷送回 agent 的上下文里。这就是经典的间接 / 二阶 prompt injection 路径,而锁定 schema 对此毫无办法。 按上游划分的 `promptproof` 配置将 [promptproof](https://github.com/bharat3645/promptproof) data-plane scanner 接入 响应路径,以确切检查这些内容: ``` { "name": "files", "url": "http://127.0.0.1:3001/mcp", "promptproof": {"enabled": true, "action": "block", "threshold": "dangerous"} } ``` 网关从每个 `tools/call` 结果中提取字符串值并 扫描它们;如果判定结果达到或超过 `threshold` (`suspicious` 或 `dangerous`),它会**阻止**该结果 — 将其替换为 JSON-RPC `-32005` 错误,使得有害内容永远无法到达 模型 — 或者对其**打标**(照常传递,设置一个 `X-PromptProof-Verdict` 响应头,并记入审计)。它同时处理 JSON 和 SSE 响应,并在扫描前对结果中 JSON 转义的隐藏字符(零宽、双向、Unicode-tag 走私)进行解码,因此原始字节所隐藏的隐蔽通道依然可见。 它**默认关闭**:没有 `promptproof` 块的上游行为 与以前完全一致。检测机制并没有在这里重新实现 — 网关运行着 一小池 `promptproof serve` 协程(promptproof ≥ 0.2.0),并将 内容流经它们,因此扫描器是唯一的真相来源。扫描器错误 **采用 fail-open 模式**(记入审计,结果照常传递),而不是让 网关宕机。审计条目仅记录元数据 — `promptproof_verdict`、`promptproof_score`、`promptproof_categories`、 `promptproof_blocked` — 绝不包含 被扫描的内容。 选项:`threshold`(`suspicious`/`dangerous`,默认 `dangerous`)、`action` (`block`/`flag`,默认 `block`)、`suspicious_at`/`dangerous_at`(调整 promptproof 的分数截断值)、`pool`(热启动的协程数,默认 2)、 `binary`(`promptproof` 的路径,默认在 `PATH` 中解析)。 ## 速率限制 按上游划分的 token bucket:`burst` 数量的请求可以连续通过,随后是持续的 `requests_per_second`。耗尽时会返回 `429` 以及 `Retry-After` header 和 JSON-RPC 错误 `-32002`,并写入审计条目(`status: 429`,`error: "rate limited"`,包含 session id,从而确保被限流的客户端依然可追溯)。 `per_session: true` 会根据 `Mcp-Session-Id` 来划分 bucket,而不是使用单个网关范围内的 bucket — 没有 session id 的请求共享同一个 bucket。诚实的框架声明:session id 是由客户端提供的,因此基于会话的限制是**诚实客户端之间的公平机制,而不是 DoS 保护**(一个不断生成新 id 的客户端会获得新的 bucket)。Bucket 表是有边界的(每个上游 4096 个会话),并采用最近最少使用(LRS)驱逐机制。对于对抗性流量,请使用网关范围内的 bucket。 两个经过深思熟虑的选择,在此如实记录: - 在代理之后,来自 `RemoteAddr` 的客户端身份是不可信的,因此没有基于 IP 的模式。 - 受限的请求会在**读取主体之前**被拒绝,因此大量超大的请求体无法消耗网关带宽。权衡点是:429 审计条目不携带 `rpc_*` 字段。 ## 示例:策略和速率限制的实战演示 上面讲的都是文字描述加上一行简单的审计日志。以下是真实的 操作:真实的编译后二进制文件、真实的 stub 上游、真实的 `curl` 请求 - 配置为 `example.config.json` 中的 `files` 上游,其中 `rate_limit` 被收紧为 `{"requests_per_second": 2, "burst": 2}`,并且为了快速演示设置了 `"tools_deny": ["delete_file"]`。 **`tools/list` 已被过滤** - 上游确实有 3 个工具,但客户端 只能看到 2 个: ``` $ curl -s http://127.0.0.1:8399/mcp/files -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' {"jsonrpc":"2.0","id":1,"result":{"tools":[ {"name":"read_file", ...}, {"name":"search", ...} ]}} ``` **被拒绝的工具在到达上游之前就会被阻止:** ``` $ curl -si http://127.0.0.1:8399/mcp/files -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"delete_file","arguments":{"path":"/etc/passwd"}}}' HTTP/1.1 403 Forbidden Content-Type: application/json {"jsonrpc":"2.0","id":null,"error":{"code":-32003,"message":"tool blocked by policy: delete_file"}} ``` **速率限制 - `burst: 2`,因此连续的第 3 个请求返回 429:** ``` $ for i in 1 2 3; do curl -si http://127.0.0.1:8398/mcp/files -d "{\"jsonrpc\":\"2.0\",\"id\":$i,\"method\":\"ping\"}" | head -1; done HTTP/1.1 200 OK HTTP/1.1 200 OK HTTP/1.1 429 Too Many Requests ``` 第 3 个请求的真实主体和 `Retry-After` header: ``` HTTP/1.1 429 Too Many Requests Retry-After: 1 {"jsonrpc":"2.0","id":null,"error":{"code":-32002,"message":"rate limited"}} ``` 以及该 3 次请求序列对应真实的 `audit.jsonl` - 注意 429 那一行 确实没有携带 `rpc_methods`/`rpc_ids`,正如上文所记录的那样: ``` {"ts":"2026-07-21T08:26:05.763Z","upstream":"files","session_id":"demo-session","rpc_methods":["ping"],"rpc_ids":["1"],"status":200,"bytes_in":40,"bytes_out":74,"duration_ms":0.972} {"ts":"2026-07-21T08:26:05.776Z","upstream":"files","session_id":"demo-session","rpc_methods":["ping"],"rpc_ids":["2"],"status":200,"bytes_in":40,"bytes_out":74,"duration_ms":0.599} {"ts":"2026-07-21T08:26:05.784Z","upstream":"files","session_id":"demo-session","status":429,"bytes_in":0,"bytes_out":0,"duration_ms":0,"error":"rate limited"} ``` 亲自复现此过程:运行 `ci/upstream_stub.py HOST PORT`,使用如上所述设置了 `rate_limit`/`tools_deny` 的配置,然后执行那三个 `curl` 调用。 ## 配置 ``` { "listen": "127.0.0.1:8385", "public_base_url": "https://mcp.example.com", "audit": { "path": "audit.jsonl" }, "upstreams": [ { "name": "files", "url": "http://127.0.0.1:3001/mcp", "rate_limit": { "requests_per_second": 10, "burst": 20, "per_session": true }, "tools_deny": ["delete_file"], "tools_lock": { "file": "mcp-sentinel.lock", "mode": "enforce" }, "authorization_servers": ["https://auth.example.com"], "resource_name": "Files MCP" }, { "name": "search", "url": "http://127.0.0.1:3002/mcp", "header_timeout_seconds": 60 } ] } ``` | 字段 | 含义 | 默认值 | |---|---|---| | `listen` | 绑定地址 | `127.0.0.1:8385`(特意设为回环地址) | | `public_base_url` | 用于生成的 `.well-known` 元数据中的外部基础 URL(在处于 TLS 终结代理后时设置) | 根据请求的 `Host`、`http` scheme 推导得出 | | `audit.path` | JSONL 目标位置;可以是文件(追加模式,0600)或 `-`/留空以输出 stdout | stdout | | `upstreams[].name` | 路由片段:`/mcp/` | 必填 | | `upstreams[].url` | 上游 MCP endpoint(http/https,无 query/fragment) | 必填 | | `upstreams[].header_timeout_seconds` | 等待上游响应*头*的最长时间。主体大小不受限,以保持 SSE 流打开 | 30 | | `upstreams[].rate_limit.requests_per_second` | 持续的 token-bucket 补充速率 | unlimited | | `upstreams[].rate_limit.burst` | Bucket 容量 | — | | `upstreams[].rate_limit.per_session` | 根据 `Mcp-Session-Id` 划分 bucket(有界表,LRS 驱逐)而不是网关范围内共享 | `false` | | `upstreams[].tools_allow` | 详尽的 `tools/call` allowlist(默认拒绝),同时也会过滤 `tools/list`。与 `tools_deny` 互斥 | no policy | | `upstreams[].tools_deny` | `tools/call` blocklist(尽力而为),同时也会过滤 `tools/list` | no policy | | `upstreams[].tools_lock.file` | 用于验证 `tools/list` 响应的 mcp-sentinel 格式 lockfile | no lock | | `upstreams[].tools_lock.server` | Lockfile 服务器条目名称 | upstream name | | `upstreams[].tools_lock.mode` | `enforce`(阻止漂移)或 `warn`(仅审计) | `enforce` | | `upstreams[].authorization_servers` | 在生成的 RFC 9728 元数据中公布 | none | | `upstreams[].resource_name` | 生成的元数据中的人类可读名称 | none | 未知的配置字段会被拒绝 — 错字会导致显式失败,而不是默默地使某个选项失效。Flags `--listen`、`--audit` 以及可重复的 `--upstream name=url` 会覆盖/扩展文件设置;`--check` 会验证配置然后退出;`--lock-init --lock-file ` 会捕获上游工具到 lockfile 中然后退出。 ## 审计条目 schema | 字段 | 备注 | |---|---| | `ts` | RFC 3339 纳秒级 UTC,请求到达时间 | | `upstream` | 路由名称;对于未知路由的尝试为空(这些也会被审计) | | `remote` | 客户端地址 | | `http_method`, `path` | 入站请求行 | | `session_id` | 请求中的 `Mcp-Session-Id` 头信息(如果存在) | | `protocol_version` | 请求中的 `MCP-Protocol-Version` 头信息(如果存在) | | `rpc_methods` | JSON-RPC 方法名(批量处理 → 按顺序包含所有) | | `rpc_ids` | 原始 JSON-RPC id(字符串 id 保留引号;通知不贡献 id) | | `tools` | 每个 `tools/call` 的 `params.name` — 这是唯一会被提取的 params 字段 | | `rpc_batch` | 主体是否为 JSON-RPC 批量数组 | | `rpc_invalid` | 主体是否为不可解析的 JSON(或超过了 1 MiB 的元数据限制)。请求无论如何都会被代理(除非应用了 allowlist 策略) | | `tools_filtered` | 因策略而从本次请求的 `tools/list` 响应中被移除的工具 | | `tools_drift` | `tools_lock` 验证失败;`error` 字段包含失败原因 | | `promptproof_verdict` | 所扫描的 `tools/call` 结果中最差的 promptproof 判定(`suspicious`/`dangerous`);未触发时此字段不存在 | | `promptproof_score` | 最差结果的 promptproof 综合得分 | | `promptproof_categories` | 发现到的分类(仅元数据,绝不包含匹配到的内容) | | `promptproof_blocked` | 一个 `tools/call` 结果是否被替换为了 `-32005` 错误 | | `promptproof_error` | 扫描器错误;结果未扫描直接通过(fail-open) | | `status` | 返回给客户端的 HTTP 状态码 | | `sse` | 响应是否为 `text/event-stream` | | `bytes_in`, `bytes_out` | 网关视角看到的主体大小(out = 发送给客户端的状态,即过滤后) | | `duration_ms` | 挂钟时间(实际经过的时间) | | `error` | 代理级别的失败或策略/lock 判定(`unknown upstream`、`rate limited`、`tool blocked by policy: `、`tool schema drifted from lock: `、传输错误) | ## JSON-RPC 错误代码 | 代码 | 含义 | |---|---| | `-32001` | 路由:未知上游 / 未知路由 / 上游不可用 | | `-32002` | 速率受限(带有 `Retry-After`) | | `-32003` | 工具策略阻止(请求或严格的响应处理) | | `-32004` | 工具 schema 漂移(强制执行 lock) | | `-32005` | `tools/call` 结果被 promptproof 阻止(注入/数据窃取判定) | ## .well-known 元数据 `GET /.well-known/oauth-protected-resource/mcp/`(RFC 9728 路径插入形式)返回为每个上游生成的 protected-resource 元数据:`resource`(客户端实际使用的网关 URL)、来自配置的 `authorization_servers` 和 `resource_name`,以及 `bearer_methods_supported: ["header"]` — 网关绝不接受包含在 URL 中的 token。当运行在 TLS 终结代理之后时,请设置 `public_base_url`,以便 `resource` 能够公布真实的外部 URL。 ## 行为说明 - **SSE / Streamable HTTP:** 带有 `Content-Type: text/event-stream` 的响应会被立即刷新并传递 — 包括通过 tools/list 重写器,它会在每个事件完成后立即发送(这已通过测试验证:在上游仍处于流中阻塞状态时接收已过滤的事件)。 - **响应重写是极其精准的:** 保留的工具、同级的结果字段、id 以及数字格式都会根据其原始字节重新发出;无需更改的响应将按字节原样传递。非目标 SSE 事件(注释、keepalives、其他 id、不可解析的数据)会逐字原样传递。 - **压缩:** 网关会在需要检查的 tools/list 交换中剥离客户端的 `Accept-Encoding`,让其自身的传输层进行协商(并透明地解压)gzip — 因此过滤和漂移检查对提供 gzip 服务的上游同样有效。 - **客户端到服务器的响应消息**(不包含 `method` 而携带 `result`/`error` 的 Streamable HTTP POST 请求)会被识别,且不会标记为无效。 - **未知路由会被审计**,而不仅仅是被拒绝 — 探测企图正是安全日志的用途所在。 - **故障诚实性:** 无法访问的上游会返回带有 HTTP 502 的 JSON-RPC `-32001` 错误,并且审计条目会记录该传输错误的字符串。 - **审计失败永远不会中断请求:** 失败的审计接收端会向 stderr 报告一次;流量会继续流动。 - `X-Forwarded-For/Host/Proto` 会在上游请求中设置;`Authorization` 及其他 header 原样传递。 - `/healthz` 和 `.well-known` 的读取不会被审计 — 审计日志记录的是 MCP 流量和路由探测,而不是存活检测的噪音。 ## 基准测试:代理开销与不使用网关的对比 `gateway/bench_test.go` 以两种方式测量同一个 `tools/call` 往返过程:直接请求 `httptest` 上游,以及通过一个真实的 `gateway.Gateway`(最小化配置 — 单个上游、无速率限制、无策略、无 lock、审计接收端被丢弃)转发至同一个上游。这两个基准测试都是通过回环接口访问真实的 `net/http` 服务器;在 HTTP 层之下没有任何模拟。 ``` go test -run '^$' -bench . -benchtime=2s ./gateway/... ``` 在本机(Apple M4,`go1.26.5 darwin/arm64`)上测得,2026-07-20: | | ns/op | B/op | allocs/op | |---|---|---|---| | 直接请求上游 | 29,266 | 7,621 | 85 | | 通过网关 | 69,852 | 53,073 | 222 | 在这种最小化配置形态下,每个请求大约增加了 40µs 的耗时和 45KB 的分配 — 这正是路由、为审计行提取 JSON-RPC 方法/工具名以及 `httputil.ReverseProxy` 跳数本身所带来的开销。这是一个按进程计算的性能微基准测试,而不是基于网络条件的基准测试:它没有体现 TLS 终结、真实的上游延迟或并发连接行为的开销,而且一旦配置了速率限制、工具策略或 `tools_lock` 验证,每个请求的成本还会进一步上升(每一项都会在请求路径上增加其自身有限的工作量)。在将这些数据用于容量规划之前,请在你自己的硬件和配置形态上重新运行上述命令。 ### promptproof 扫描开销 `BenchmarkThroughGatewayWithPromptProof` 会在开启扫描的情况下(阻止操作,热启动的协程池)重复进行通过网关的基准测试,每次调用都会将一个真实大小约 150 字节的干净工具结果推送到 promptproof。 `BenchmarkScannerScan` 仅隔离了协程的往返过程(发送帧,接收判定结果),路径中不含 HTTP。在同一台机器上测得(Apple M4,`go1.26.5`,promptproof 0.2.0 release 版本,2026-07-22): | | ns/op | B/op | allocs/op | |---|---|---|---| | 通过网关(无扫描) | ~72,000 | 50,939 | 222 | | 通过网关 + promptproof | ~120,000 | 61,419 | 333 | | 隔离的 `Scanner.Scan` 往返 | ~29,000 | 1,104 | 18 | 因此,在网关路径的基础上,扫描为每个 `tools/call` 增加了大约 **~48µs** 的开销 — 即约 0.05 ms。因为扫描器是一个热启动的协程(而不是每次调用都生成的进程),其开销仅仅是约 29µs 的帧写入/扫描/读取判定的往返时间,加上响应路径中解析结果的时间,而不是像 fork-per-scan 那样需要花费 ~1–5 ms。 开销会随着结果大小以及并发情况下的池争用而增加(提高 `pool` 值可扩大容量)。使用以下命令复现: ``` PROMPTPROOF_BIN=$(command -v promptproof) \ go test -run '^$' -bench 'PromptProof|ScannerScan|ThroughGateway' -benchtime=1s ./gateway/... ``` ## 暂不支持的功能 1. **Auth:** 网关负责转发凭证并公布元数据;它不生成或验证 token。请将其置于你的 SSO 终结代理之后。 2. **除工具外的列表过滤器:** `resources/list` 和 `prompts/list` 会直接透传;目前策略和 lock 仅涵盖工具。 3. **多租户会话粘连、负载均衡、管理 UI:** 你需要的是更重量级的网关 — 而这个网关只是一个能在下午茶时间轻松读完源码的单个静态二进制文件。 ## 开发 ``` go test -race ./... # httptest end-to-end suite, no network needed go vet ./... go build ./cmd/mcp-gateway-lite bash ci/smoke.sh # real binary + Python upstream: filtering, lock-init, # drift enforcement, and a cross-check against a real # mcp-sentinel checkout ``` CI 会运行 gofmt/vet/race 测试/构建以及冒烟测试脚本:针对真实上游(包含 rug-pull 重启)进行过滤和漂移断言,执行审计日志 grep 断言,进行参数防泄漏检查,并确保 `mcp-sentinel` 自身的代码与网关编写的 lockfile 保持一致。 ## 许可证 MIT
标签:API网关, EVTX分析, Go, JSONLines, MCP, Ruby工具, Streamlit, 反向代理, 审计日志, 日志审计, 时序数据库, 访问控制, 零信任