Skip to main content
Glama
dwsitproject-hub

MCP Gateway

MCP Gateway — 阶段 1(KLIP,只读)

一个单一的安全服务,让经授权的 Energi-Up 员工通过 Claude 以自然语言查询 KLIP, 而无需打开应用程序。严格只读。

部署目标(PRD Q3,现已关闭):<gateway-hostname> -> <gateway-public-ip> (ECS-MCP,ap-southeast-5)。请注意主机名是 mcp-gw,而不是 v0.9 文档所假设的 mcp.example.com;PRD/TSD 应相应更新。

文档: PRD v0.9 · TSD v0.9 · 实施指南 · 设计评审 · 部署手册


1. 固定版本(T-1)

MCP 规范修订版和 SDK 版本在此固定。当 SDK 自己的文档与设计文档不一致时,以 SDK 为准——它定义了确切的 API。

组件

固定版本

备注

@modelcontextprotocol/sdk

1.30.0(精确版本,无脱字符)

发布于 2026 年 7 月 27 日。提供 OAuth AS、Streamable HTTP 传输和 requireBearerAuth。

MCP 协议修订版

2025-11-25 已实现;线格式与 2025-06-18 客户端兼容

修订版 2026-07-28(草案)移除了协议级会话、GET 流和 Last-Event-ID。此服务器是无状态的,因此与该变更向前兼容,并对 /mcp 上的旧版 GET/DELETE 返回 405。

Node.js

22 LTS (node:22-slim)

TypeScript

5.9.3, strict + exactOptionalPropertyTypes

express

5.2.1

从 TSD 的 4.x 提升而来:SDK 依赖 express@^5.2.1,将 express-5 路由器挂载到 express-4 应用会混用 path-to-regexp 的主版本。

zod

4.4.3

从 TSD 的 3.x 提升而来:SDK 的类型定义面向 zod 4。注意 .default() 现在提供输出类型。

jose

6.2.9

RS256 网关令牌。

@node-rs/argon2

2.1.0

替代 argon2:使用预构建二进制文件,因此 node:22-slim 中无需 node-gyp 工具链。

axios

1.19.0

KLIP 客户端,位于方法守卫之后。

pg

8.23.0

PostgreSQL

16 (container)

nginx

nginx.org mainline

配置位于 deploy/nginx/mcp.conf。

Related MCP server: Snowflake MCP Server

2. 布局

src/
  core/       config, logger, db, audit, cache, rateLimit, semaphore, migrate, errors
  adapters/klip/  routes(APPENDIX A) · fields(APPENDIX A) · client(guard) · session · paginate · normalize
  tools/klip/ 9 tool definitions + shared parameter plumbing
  mcp/        server, envelope, runner
  auth/       keys, hub(OIDC RP), users, clients, tokens, provider, loginPage
  http/       app, consent(Hub + break-glass), health, origin, clientIp
migrations/   idempotent SQL (001 schema, 002 Hub OIDC)
deploy/       nginx config, backup sidecar
test/         120 tests + mock KLIP and mock Hub fixtures

分层规则(T-3):tools → adapters → core。除入口点外,没有任何模块导入 http/。所有业务规范化都位于 adapters/klip/normalize.ts 中,该文件不导入任何内容,因此可以独立进行单元测试。

3. 身份验证 — Downstream Hub OIDC

试点用户使用 Downstream Hub(OIDC)登录。网关仍然是 Claude 与之通信的授权服务器;Hub 是其自身 /authorize 流程中的一个步骤。

我们特意不使用 SDK 的 ProxyOAuthServerProvider。代理会把 Hub 令牌交给 Claude,这会破坏 RFC 8707 的受众绑定,让 Claude 获得比 klip:read 更广泛的 Hub 作用域,并将令牌签发移出我们的控制,从而使 S8 紧急停止开关无法再使活动会话失效。

Claude ──/authorize──▶ gateway ──302──▶ Downstream Hub ──302──▶ /authorize/hub/callback
                          │                                              │
                          │        validate id_token (sig/iss/aud/nonce)  │
                          │        check the pilot ALLOWLIST              │
                          ◀──────────────────────────────────────────────┘
                          └──302 code──▶ Claude ──/token──▶ gateway token (klip:read)

身份验证不是授权。 Hub 证明某人的身份;users 表决定他们是否可以使用连接器。阶段 1 使用一个共享的 KLIP 服务账户,因此每个被接纳的用户都可以读取 MCP_READONLY 能读取的所有内容(评审 H8)——试点成员资格就是数据访问控制。不在列表中的 Hub 账户会收到 403,<= 15 的上限由 user:add 强制执行。

该流程中运行两次 PKCE 交换;不要混淆它们。Claude 自己的 code_challenge 保护 Claude→网关这一段(由 SDK 处理);网关在服务端保存的单独验证器保护网关→Hub 这一段。

使用哪个 Hub 客户端?网关自己的——而不是 KLIP 的

为 MCP Gateway 注册一个新的 OIDC 客户端。不要复用 KLIP 的 Hub 注册,即使 KLIP 已经在测试 DWS Hub 中注册。

Hub 只涉及两个信任边界之一:

信任边界

凭据

是否涉及 Hub?

Claude → gateway(谁在提问)

网关自己的 Hub 客户端 + 试点允许列表

是

gateway → KLIP(读取数据)

svc-mcp 针对 KLIP 的 /api/auth/login

否——PRD §7 明确排除

复用 KLIP 的客户端会以具体方式破坏第一个边界。网关根据其自身的 HUB_CLIENT_ID 验证 ID 令牌的 aud;共享 KLIP 的客户端 ID 意味着在 KLIP 登录期间签发的 ID 令牌会被网关接受,这正是 confused-deputy 的形态。独立的客户端才能使两个依赖方可区分。这还使重定向 URI 允许列表、客户端密钥、轮换计划、Hub SSO 审计条目和禁用开关相互独立——关闭连接器的 Hub 客户端绝不能导致 KLIP 登录中断。

这些都不需要在 KLIP 侧做任何更改。K1–K4 不受影响。

两个 Hub 实例,两次注册

在每个 Hub 中分别注册网关,并将其与匹配的 KLIP 配对:

阶段

KLIP_ENV

HUB_ISSUER

客户端

4–6(构建、暂存 UAT)

staging

testing DWS Hub

testing Hub 中的网关客户端

7+(生产切换)

production

production Hub

production Hub 中一个独立的网关客户端

该配对在启动时强制执行,因为在一个方向上弄错是危险的,而不仅仅是混乱:

  • KLIP_ENV=production + testing HUB_ISSUER → 网关拒绝启动。 否则,任何能够创建测试 Hub 账户的人都可能接触到真实的商业数据。

  • KLIP_ENV=staging + production HUB_ISSUER → 发出警告并继续。

hub:check 会打印它检测到的配对,因此切换是可验证的:

pairing:       KLIP staging  <->  Hub testing

DWS Hub 的要求(它不是标准 OIDC)

依据 Docs/SSO-TARGET-APP-INTEGRATION.md。其中四项与 OIDC 客户端库默认假设不同,三项会直接失败:

DWS Hub

客户端类型

public,PKCE S256 — token_endpoint_auth_methods_supported: ["none"],不存在客户端密钥

发现

/api/sso/.well-known/openid-configuration — 不是 issuer 的 RFC 8414 路径

令牌正文

JSON;表单编码返回 unsupported_grant_type

作用域

仅 openid profile email — 没有 groups 声明,因此 HUB_REQUIRED_GROUP 不可用

redirect_uri

在令牌请求中为必填,且必须逐字节完全匹配

因此,HUB_DISCOVERY_URL 被显式配置,HUB_CLIENT_SECRET 是可选的,HUB_TOKEN_BODY 默认为 json,并在 unsupported_grant_type 时一次性回退到表单(记录哪种方式有效,以便固定下来)。

设置

  1. 在 Hub 中将网关注册为 OIDC 客户端,重定向 URI 为 <PUBLIC_URL>/authorize/hub/callback。它是一个公共客户端——不要索取密钥。

  2. 将 HUB_ISSUER、HUB_DISCOVERY_URL 和 HUB_CLIENT_ID 放入 /opt/mcp/.env。

  3. 在任何试点用户尝试之前进行验证:

docker compose exec -T gateway node dist/cli.js hub:check

这会打印要注册的重定向 URI,运行发现,并在 Hub 未公布 S256 PKCE 时发出警告。网关还会在启动时探测发现,并在 /healthz 中将其报告为 hub_oidc。

  1. 添加试点用户(无密码——由 Hub 进行身份验证):

docker compose exec -T gateway node dist/cli.js user:add someone@example.com "Their Name"

应急账户

只允许一个本地密码账户,用于 Hub 宕机或配置错误时:

docker compose exec -T gateway node dist/cli.js user:add-break-glass it-emergency@example.com

它隐藏在登录页面的披露项后面,首次使用时强制更改密码,并且每次通过它登录都会以高严重级别记录审计,标记为 break_glass: true。Hub 认证的用户完全无法使用密码路径,因此关闭 Hub 并不是回退到无人设置的密码的方式。

一旦 Hub 路径在生产环境中得到验证,请设置 BREAK_GLASS_ENABLED=false,以彻底移除密码面。

可选组门控 — DWS Hub 上不可用

HUB_REQUIRED_GROUP 会针对 groups 声明增加第二重检查。DWS Hub 不签发该声明(它只通告 openid profile email),因此设置它将拒绝所有用户。hub:check 会在您这样做时发出警告。users 表中的试点白名单仍然是授权控制手段。

IdP 发起的登录

Hub 可以从其仪表板磁贴将用户直接推送到回调。这对连接器来说行不通:回调的存在是为了完成 Claude 发起的授权请求,因此没有授权请求到达时,就没有可据以签发 code 的对象。网关会检测到这种情况并提示"请从 Claude 开始",而不是以"登录已过期"的方式失败。

4. 快速开始(本地)

npm ci
docker run -d --name mcpgw-devdb -e POSTGRES_DB=gateway -e POSTGRES_USER=gateway \
  -e POSTGRES_PASSWORD=devpassword -p 127.0.0.1:55432:5432 postgres:16-alpine
cp .env.example .env.dev   # then edit: PUBLIC_URL=http://localhost:8787, DATABASE_URL=...55432...
npx tsx test/fixtures/mockKlip.ts 5099 &      # mock KLIP, behaves like the real one
set -a; . ./.env.dev; set +a
npm run migrate
npx tsx src/index.ts

创建试点用户(适用于 TTY 或管道输入):

printf 'a-strong-password\na-strong-password\n' | npx tsx src/cli.ts user:add you@example.com "Your Name"

5. 测试

npm test

测试套件

覆盖范围

normalize.spec.ts (33)

Incoterm × 状态 × null 矩阵、kg→MT、舍入顺序、负未结数量、WIB 时间戳

guard.spec.ts (9)

T-6 的穷举方法/路径表、目录穿越与源逃逸

envelope.spec.ts (12)

T-5 信封、截断 next_step、注入载荷拆解

truncation.spec.ts (4)

有界抓取发布 totals_partial,绝不发布 totals

integration.spec.ts (23)

针对模拟 KLIP 的全部 9 个工具;非 GET 请求绝不触达 KLIP;401 重新登录;AUTH_DEGRADED;类型化错误;单元纪律

resource.spec.ts (6)

RFC 8707 受众绑定:仅接受此服务器的规范资源

audit.spec.ts (8)

S5 脱敏 — 对字符串激进,对数字无作用

hub.spec.ts (20)

针对模拟提供方的 Hub OIDC:发现文档签发者不匹配、外部签名密钥、错误的 issuer/audience、过期令牌、缺失和重放的 nonce、无 email 声明

hubGroupGate.spec.ts (5)

HUB_REQUIRED_GROUP 准入,包括近似组名

hubPairing.spec.ts (5)

生产 KLIP 配测试 Hub 拒绝启动;正常配对不受影响

hubTokenAuth.spec.ts (7)

从发现文档中选择令牌端点认证方法,包括仅 POST 和公共客户端 Hub

hubDws.spec.ts (16)

精确建模 DWS Hub:/api/sso 发现、公共客户端、仅 JSON 令牌体、无 groups 作用域,以及双向编码回退

模拟 Hub 是一个可工作的迷你 OIDC 提供方 — 真实的发现文档、真实的 JWKS、真实的 RS256 ID 令牌,以及授权码上的 PKCE 验证 — 并配备伪造坏令牌所需的每一个旋钮,因为负面用例才是重点。

模拟 KLIP 夹具刻意复现了真实系统的怪癖:以 MT 标注的千克、混合语言状态、标准四种之外的 incoterm、null 数量、超量交付的合同、被静默钳制为 100 的 limit,以及携带提示注入载荷的合同备注。

6. 部署

完整的主机特定流程,包含真实 IP、主机名和安全组表:deploy/RUNBOOK.md。

# on ECS-MCP
cd /opt/mcp && git pull
docker compose build gateway && docker compose up -d
curl -fsS http://127.0.0.1:8787/healthz

管理 CLI — 请注意它在容器内部运行,因为主机仅安装 Docker,没有 Node.js:

docker compose exec -T gateway node dist/cli.js user:list
docker compose exec -T gateway node dist/cli.js audit:summary --days 7
docker compose exec -T gateway node dist/cli.js audit:export --from 2026-08-01 --to 2026-09-01 --out /tmp/audit.csv
docker compose exec -T gateway node dist/cli.js routes:verify        # probes KLIP, reports Appendix A gaps

紧急开关(S8) — 目标在 5 分钟内:

docker compose exec -T gateway node dist/cli.js tokens:revoke-all --reason "incident 2026-xx"
docker compose stop gateway

应用容器不健康时的破窗操作:

docker compose exec -T db psql -U gateway -d gateway -c "UPDATE oauth_tokens SET revoked_at=now() WHERE revoked_at IS NULL;"

7. 附录 A 是硬性门禁

src/adapters/klip/routes.ts 和 src/adapters/klip/fields.ts 保存着适配器所依赖的每一个 KLIP 路径、查询参数名、页大小上限、响应字段名和枚举值。当前每一项均未验证。

该门禁是可执行的,而非文书性的:当 KLIP_ENV=production 时,只要有任何路由未验证,进程拒绝启动。针对 staging 运行 routes:verify,记录结果,为每条路由设置 verified: true 并设置 enums.verified = true。

有两个字段比其他字段更重要:

  • maxLimit — KLIP 实际接受的最大 limit。如果它静默钳制为 100,那么 KLIP_PAGE_SIZE 为 1000 时一页会变成十页,从而破坏延迟目标。

  • enums.* — 规范的状态和 incoterm 值。任何未映射的值都会从总计中排除并附上数据质量说明,绝不使用默认值。

8. 与 TSD v0.9 的偏差

每一项都来自设计评审,并在其调用点处有注释。

#

变更

原因

B3

仅当 Origin 存在且无效时才拒绝

规范仅要求对存在且无效的 Origin 返回 403。Claude 以服务器对服务器方式调用连接器,可能不发送 Origin;若对缺失的 Origin 也拒绝,则每次工具调用都会返回 403。

B4

aud = <PUBLIC_URL>/mcp,而非裸主机名

RFC 8707 audience 绑定。在 PRM 文档中增加了 authorization_servers + resource,在 401 挑战中增加了 scope,以及 RFC 9207 iss。

B5

无状态传输;身份信息来自每个请求中的令牌

2026-07-28 修订版移除了协议会话。T-4 通过构造即满足——不存在可劫持的会话。

B6

熔断开关运行 docker compose exec

宿主机没有 Node.js,因此 cd /opt/mcp && node cli.js 永远无法工作。

B7

nginx 是反滥用基线;按用户限流以 OAuth sub 为键

所有流量均来自 Anthropic 的共享出口网段,因此按 IP 限流会把整个试点项目放入同一个桶中。增加了 limit_req_status 429(默认值为 503)。

H1

OAuth 基于 SDK 的 mcpAuthRouter + OAuthServerProvider 构建

SDK 自带 AS,包括吊销和默认限流。只有存储和用户认证是我们自己的。

H2

下游 Hub OIDC 是登录路径,另设一个应急本地账户

从阶段 2 提前引入。Hub 负责认证;用户表仍作为试点白名单。ProxyOAuthServerProvider 被有意否决——见 §3。

H3

为 Anthropic 的 160.79.104.0/21 搭建 nginx 白名单脚手架

Anthropic 发布稳定的出口网段并建议白名单;/authorize 保持对公司出口开放,因为它在用户浏览器中运行。在阶段 6 确认真实源地址之前保持注释状态。

H4

截断的结果发布 totals_partial;聚合以整数 kg 进行;未映射的枚举排除;负的 outstanding 保留

四条不同的路径都可能导致一个看似正确实则错误的数字。

H5

短 TTL 缓存;第 2..N 页并发获取;页大小限制为 maxLimit

十次串行往返无法满足 P95 ≤ 5 s。

H6

新增第 9 个工具 klip_reference,以及类型化的 UNKNOWN_FILTER_VALUE

没有它,拼写错误的工厂名称会返回空集合,而这会被解读为"没有任何 outstanding"。

H7

environment 和 source 从 KLIP_ENV 派生

规范中硬编码的 "KLIP production" 字符串会导致每次预演 UAT 的答案都声称自己是生产环境。

H9

audit_events 采用按月分区;增加 audit:export;遵循 X-Forwarded-For

通过删除分区进行保留是仅追加触发器唯一允许的方式;U5 的导出没有实现;客户端 IP 原本都会记录为 127.0.0.1。

H10

备份侧车、容器健康检查、/healthz 详情仅限内部调用者访问

在 TSD 中有规定,但指南中从未实现,因此上线时不会存在。

—

未知工具参数通过 z.strictObject 拒绝

PRD 8.1 要求拒绝;普通的 z.object 会静默忽略多余参数。

—

工具返回针对 outputSchema 的 structuredContent

类型化数据而非嵌入散文中的 JSON —— 减少转写错误,这正是 M1 所衡量的。

9. 仍待处理事项

  • 附录 A 对账 (P1) —— 阻止投产,启动时强制检查。

  • KLIP 侧 K1–K4 —— MCP_READONLY 角色、svc-mcp 账户、安全组规则。

  • 离机备份和经过测试的恢复 —— 侧车目前仅写入本地。

  • deploy/nginx/mcp.conf 中的真实企业出口 CIDR,然后启用两行被注释掉的 return 403 行。

  • 负载测试,按 30 并发用户容量 NFR 执行。

  • Hub 客户端注册 —— 网关自身的注册,先在测试 DWS Hub 中: HUB_ISSUER、客户端 ID 和密钥,并在 Hub 侧注册重定向 URI <PUBLIC_URL>/authorize/hub/callback。确认 Hub 的 email 和 groups 声明名称,然后运行 hub:check。阶段 7 需要在生产 Hub 中进行第二次独立注册。

  • 决定是否设置 HUB_REQUIRED_GROUP,以及在 Hub 路径在生产环境中验证后是否将 BREAK_GLASS_ENABLED=false。

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Enables read-only querying of the gong-nl-db Postgres database through natural language via Claude Desktop.
    9
    Academic Free v1.1
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables natural language queries against Snowflake Gold-layer tables through Claude Desktop, allowing users to ask business questions in plain English without SQL knowledge.
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables natural language interaction with Kintone data via Claude, allowing listing apps, field definitions, querying and modifying records.
    -