Skip to main content
Glama
dwsitproject-hub

MCP Gateway

MCP Gateway — Этап 1 (KLIP, только чтение)

Единый защищённый сервис, который позволяет авторизованным сотрудникам Energi-Up запрашивать KLIP на естественном языке через Claude, не открывая приложение. Строго только чтение.

Целевое развёртывание (PRD Q3, теперь закрыто): <gateway-hostname> -> <gateway-public-ip> (ECS-MCP, ap-southeast-5). Обратите внимание: имя хоста — mcp-gw, а не mcp.example.com, как предполагалось в документах v0.9; PRD/TSD следует обновить соответствующим образом.

Документы: PRD v0.9 · TSD v0.9 · Руководство по внедрению · Ревью дизайна · Runbook развёртывания


1. Зафиксированные версии (T-1)

Ревision спецификации MCP и версия SDK зафиксированы здесь. Если собственная документация SDK расходится с проектными документами, решающим является SDK — он определяет точный API.

Компонент

Зафиксировано

Примечания

@modelcontextprotocol/sdk

1.30.0 (точно, без каретки)

Опубликован 27 июля 2026 г. Предоставляет OAuth AS, транспорт Streamable HTTP и requireBearerAuth.

Ревision протокола MCP

2025-11-25 реализован; формат провода совместим с клиентами 2025-06-18

Ревision 2026-07-28 (черновик) убрала сессии на уровне протокола, GET-поток и Last-Event-ID. Этот сервер не сохраняет состояние, поэтому он прямосовместим с этим изменением и отвечает 405 на устаревшие GET/DELETE на /mcp.

Node.js

22 LTS (node:22-slim)

TypeScript

5.9.3, strict + exactOptionalPropertyTypes

express

5.2.1

Повышено с 4.x из TSD: SDK зависит от express@^5.2.1, а монтирование роутера express-5 в приложение express-4 смешивает мажорные версии path-to-regexp.

zod

4.4.3

Повышено с 3.x из TSD: определения типов SDK нацелены на zod 4. Обратите внимание: .default() теперь предоставляет тип выходных данных.

jose

6.2.9

Токены шлюза RS256.

@node-rs/argon2

2.1.0

Замена для argon2: готовые бинарные сборки, поэтому инструментарий node-gyp в node:22-slim не нужен.

axios

1.19.0

Клиент KLIP, за защитой метода.

pg

8.23.0

PostgreSQL

16 (контейнер)

nginx

mainline от nginx.org

Конфигурация в 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.

Мы сознательно не используем ProxyOAuthServerProvider из SDK. Проксирование передало бы Claude токен Hub, что нарушило бы привязку аудитории по RFC 8707, дало бы Claude более широкие области Hub, чем klip:read, и вывело бы выпуск токенов из-под нашего контроля, так что аварийный рубильник 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; не смешивайте их. Собственный code_challenge от Claude защищает участок Claude→шлюз (обрабатывается SDK); отдельный верификатор, который шлюз хранит на своей стороне, защищает участок шлюз→Hub.

Какой клиент Hub? Собственный шлюза — не KLIP

Зарегистрируйте новый OIDC-клиент для MCP Gateway. Не используйте повторно регистрацию KLIP в Hub, даже если KLIP уже зарегистрирован в тестовом DWS Hub.

Hub касается только одной из двух границ доверия:

Граница

Учётные данные

Участвует ли Hub?

Claude → шлюз (какой человек спрашивает)

собственный клиент Hub шлюза + пилотный список допуска

да

шлюз → KLIP (чтение данных)

svc-mcp через /api/auth/login от KLIP

нет — PRD §7 исключает это явно

Повторное использование клиента KLIP конкретным образом ломает первую границу. Шлюз проверяет aud ID-токена по своему собственному HUB_CLIENT_ID; совместное использование идентификатора клиента KLIP означает, что ID-токен, выпущенный при входе в KLIP, был бы принят шлюзом — это форма confused-deputy. Отдельные клиенты — это то, что делает две полагающиеся стороны различимыми. Это также сохраняет независимость списков разрешённых redirect-URI, секретов клиентов, графиков ротации, записей аудита Hub SSO и переключателей отключения — отключение клиента Hub коннектора не должно останавливать вход в KLIP.

Ни одно из этих изменений не требует изменений на стороне KLIP. K1–K4 не затрагиваются.

Два экземпляра Hub, две регистрации

Зарегистрируйте шлюз отдельно в каждом Hub и сопоставьте их с соответствующим KLIP:

Этап

KLIP_ENV

HUB_ISSUER

Клиент

4–6 (сборка, staging UAT)

staging

тестовый DWS Hub

клиент шлюза в тестовом Hub

7+ (продуктовый cutover)

production

продуктовый Hub

отдельный клиент шлюза в продуктовом Hub

Сопоставление проверяется при загрузке, потому что ошибка в одну сторону опасна, а не просто неаккуратна:

  • KLIP_ENV=production + тестовый HUB_ISSUER → шлюз отказывается запускаться. Иначе любой, кто может создать аккаунт в тестовом Hub, получил бы доступ к реальным коммерческим данным.

  • KLIP_ENV=staging + продуктовый HUB_ISSUER → предупреждение и продолжение работы.

hub:check выводит обнаруженное сопоставление, так что cutover можно проверить:

pairing:       KLIP staging  <->  Hub testing

Что требует DWS Hub (это не ванильный OIDC)

Согласно Docs/SSO-TARGET-APP-INTEGRATION.md. Четыре пункта отличаются от значений по умолчанию, которые предполагает библиотека OIDC-клиента, и три привели бы к полному отказу:

DWS Hub

Тип клиента

публичный, PKCE S256 — token_endpoint_auth_methods_supported: ["none"], секрета клиента не существует

Discovery

/api/sso/.well-known/openid-configuration — не путь RFC 8414 от издателя

Тело токена

JSON; форма-кодирование возвращает unsupported_grant_type

Области

только openid profile email — нет claims групп, поэтому HUB_REQUIRED_GROUP непригоден

redirect_uri

обязателен в запросе токена и должен совпадать байт-в-байт

Поэтому HUB_DISCOVERY_URL настраивается явно, HUB_CLIENT_SECRET необязателен, а HUB_TOKEN_BODY по умолчанию равен json с одноразовым откатом на форму при unsupported_grant_type (с записью в лог того, что сработало, чтобы это можно было зафиксировать).

Настройка

  1. Зарегистрируйте шлюз как OIDC-клиент в Hub с redirect 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

    Это выводит redirect URI для регистрации, выполняет discovery и предупреждает, если Hub не объявляет PKCE S256. Шлюз также проверяет discovery при загрузке и сообщает о нём в /healthz как hub_oidc.

  4. Добавьте пилотных пользователей (без паролей — их аутентифицирует 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 — это не способ откатиться к паролю, который никто не задавал.

Установите BREAK_GLASS_ENABLED=false, как только путь через Hub будет подтверждён в production, чтобы полностью исключить пароль как поверхность атаки.

Опциональный групповой контроль — недоступен на DWS Hub

HUB_REQUIRED_GROUP добавляет вторую проверку по claim groups. DWS Hub не выдаёт такой claim (он объявляет только openid profile email), поэтому его установка приведёт к отказу для каждого пользователя. hub:check предупредит об этом. Разрешительный список пилота в таблице users остаётся механизмом контроля авторизации.

Вход, инициированный IdP

Hub может направить пользователя сразу на callback из плитки своей панели управления. Для коннектора так работать не может: callback существует, чтобы завершить запрос авторизации, начатый Claude, поэтому при обращении без такого запроса не остаётся ничего, для чего можно было бы выдать код. Шлюз обнаруживает это и сообщает «начните с 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, кг→MT, порядок округления, отрицательный остаток, временные метки WIB

guard.spec.ts (9)

Исчерпывающая таблица метод/путь для T-6, обходы путей и выходы за пределы origin

envelope.spec.ts (12)

Конверт T-5, усечение next_step, обезвреживание инъекционной полезной нагрузки

truncation.spec.ts (4)

Ограниченная выборка публикует totals_partial, но никогда totals

integration.spec.ts (23)

Все 9 инструментов против mock KLIP; ни один не-GET запрос никогда не достигает KLIP; повторный вход после 401; AUTH_DEGRADED; типизированные ошибки; дисциплина unit-тестов

resource.spec.ts (6)

Привязка audience по RFC 8707: принимается только канонический resource этого сервера

audit.spec.ts (8)

Маскирование S5 — агрессивно в отношении строк, инертно в отношении чисел

hub.spec.ts (20)

Hub OIDC против mock-провайдера: несовпадение issuer в discovery, посторонний ключ подписи, неверный issuer/audience, истёкший токен, отсутствующий и повторно использованный nonce, claim без email

hubGroupGate.spec.ts (5)

Допуск по HUB_REQUIRED_GROUP, включая почти совпадающие имена групп

hubPairing.spec.ts (5)

Production KLIP за тестовым Hub отказывается запускаться; обычные связки — нет

hubTokenAuth.spec.ts (7)

Метод аутентификации token-endpoint, выбираемый из discovery, включая Hub, поддерживающие только POST, и Hub с публичным клиентом

hubDws.spec.ts (16)

DWS Hub смоделирован точно: discovery /api/sso, публичный клиент, тело токен-запроса только в JSON, отсутствие scope groups, плюс запасной вариант кодирования в обе стороны

Mock Hub — это работающий мини-OIDC-провайдер — настоящий discovery-документ, настоящие JWKS, настоящие RS256 ID-токены и проверка PKCE для кода авторизации — со всеми настройками, необходимыми для подделки плохого токена, потому что негативные сценарии и есть суть.

Фикстура mock KLIP намеренно воспроизводит особенности реальной системы: килограммы, помеченные как MT, статусы на разных языках, incoterm за пределами стандартной четвёрки, количества со значением null, контракт с перепоставкой, limit молча усекаемый до 100, и примечание к контракту, содержащее полезную нагрузку для prompt-инъекции.

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

Kill switch (S8) — целевое время: менее 5 минут:

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

Break-glass, если контейнер приложения нездоров:

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, имя query-параметра, максимальный размер страницы, имя поля ответа и значение enum, от которых зависит адаптер. Каждая запись на данный момент не проверена.

Этот барьер исполняемый, а не формальный: при KLIP_ENV=production процесс отказывается запускаться, пока какой-либо маршрут не проверен. Запустите routes:verify на staging, зафиксируйте результаты, установите verified: true для каждого маршрута и enums.verified = true.

Два поля важнее остальных:

  • `

#

Изменение

Почему

B3

Origin отклоняется только при наличии и недействительности

Спецификация требует 403 только для присутствующего и недействительного Origin. Claude обращается к коннектору server-to-server и может не отправлять Origin вовсе; отклонение отсутствующего Origin давало бы 403 на каждый вызов инструмента.

B4

aud = <PUBLIC_URL>/mcp, а не голое имя хоста

Привязка аудитории по RFC 8707. В документ PRM добавлены authorization_servers + resource, в 401-вызов — scope, а также iss по RFC 9207.

B5

Без состояния транспорт; идентичность из токена на каждый запрос

Редакция от 2026-07-28 убрала сессии протокола. T-4 выполняется по построению — не существует сессии, которую можно перехватить.

B6

Аварийный выключатель запускается через docker compose exec

На хосте нет Node.js, поэтому cd /opt/mcp && node cli.js никогда не мог бы сработать.

B7

nginx — базовый уровень защиты от злоупотреблений; лимит на пользователя по ключу sub из OAuth

Весь трафик приходит из общего диапазона исходящих адресов Anthropic, поэтому ограничение по IP помещает весь пилот в один bucket. Добавлено limit_req_status 429 (по умолчанию — 503).

H1

OAuth построен на mcpAuthRouter + OAuthServerProvider из SDK

В SDK уже есть AS, включая отзыв токенов и ограничение частоты по умолчанию. Наши — только хранилище и аутентификация пользователей.

H2

Вход через downstream Hub OIDC, плюс одна локальная учётная запись break-glass

Вынесено из Фазы 2. Hub аутентифицирует; таблица пользователей остаётся пилотным allowlist. ProxyOAuthServerProvider отклонён намеренно — см. §3.

H3

Каркас allowlist в nginx для 160.79.104.0/21 от Anthropic

Anthropic публикует стабильные диапазоны исходящего трафика и рекомендует allowlist; /authorize остаётся открытым для корпоративного egress, так как выполняется в браузере пользователя. Закомментировано до подтверждения реальных исходных адресов на этапе 6.

H4

Усечённые результаты публикуют totals_partial; агрегация в целых кг; несопоставленные enum исключаются; отрицательный outstanding сохраняется

Четыре разных пути к уверенно неверному числу.

H5

Короткий TTL-кэш; страницы 2..N загружаются конкурентно; размер страницы ограничен maxLimit

Десять последовательных round-trip'ов не уложатся в P95 ≤ 5 с.

H6

Добавлен 9-й инструмент klip_reference плюс типизированный UNKNOWN_FILTER_VALUE

Без него опечатка в имени завода возвращает пустой набор, который читается как «ничего не осталось».

H7

environment и source берутся из KLIP_ENV

Захардкоженная строка «KLIP production» заставила бы каждый staging-ответ UAT утверждать, что это production.

H9

audit_events разбит по месяцам range-партициями; добавлен audit:export; учитывается X-Forwarded-For

Удаление партициями — единственное, что допускает триггер append-only; экспорт U5 не имел реализации; IP клиентов иначе записывались бы как 127.0.0.1.

H10

Sidecar для резервного копирования, healthcheck контейнера, детали /healthz ограничены внутренними вызывающими

Описано в TSD, но так и не было реализовано в руководстве, поэтому на момент запуска этого бы не существовало.

—

Неизвестные параметры инструментов отклоняются через z.strictObject

PRD 8.1 требует отклонения; обычный z.object молча игнорирует лишние параметры.

—

Инструменты возвращают structuredContent по схеме outputSchema

Типизированные данные вместо JSON, встроенного в текст, — меньше ошибок транскрибации, а именно это измеряет M1.

9. Открытые вопросы

  • K1–K4 на стороне Hub — роль MCP_READONLY, учётная запись svc-mcp, правило security-group.

  • Внешняя синхронизация бэкапов и протестированное восстановление — sidecar пишет только локально.

  • Реальные корпоративные egress-диапазоны в deploy/nginx/mcp.conf и включение двух закомментированных строк return 403.

  • Нагрузочное тестирование на ёмкости 30 одновременных подключений.

  • Регистрация клиента в тестовом Hub — собственная регистрация коннектора, с URI редиректа <PUBLIC_URL>/authorize/callback, ключами HUB_ISSUER, client_id/client_secret Hub, подтверждёнными email и группами, затем hub:check. Отдельная регистрация для продакшн-Hub — на этапе 7.

  • Решить, задавать ли HUB_REQUIRED_GROUP и выключать ли BREAK_GLASS_ENABLED после подтверждения пути через Hub на проде.

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.
    -