mcp-stepup-gateway
mcp-stepup-gateway
MCP-шлюз, который требует passkey (WebAuthn) по запросу — «step-up auth» — прежде чем разрешить удалённому клиенту (Claude.ai, через Custom Connector) читать или писать в хранилище Obsidian, защищённое enquire-mcp. Вход через Google и allowlist (как в mcp-oauth-gateway) решают, кто может подключаться; этот проект решает, для каждого инструмента, что этот человек может делать без повторного подтверждения личности, а что требует свежего касания passkey.
Он родился из конкретного случая: mcp-oauth-gateway/enquire-mcp-gateway уже решают «аутентифицировать того, кто подключается» (OAuth + allowlist). Не хватало второго уровня: даже внутри allowlist не каждый tool call должен быть одинаково свободным. Прочитать заметку — дёшево; удалить или переписать содержимое хранилища через LLM, которая может находиться под prompt injection, — нет. Этот шлюз добавляет это различие, не трогая сам enquire-mcp.
Зачем это нужно
Удалённый MCP-клиент, аутентифицированный через OAuth, с точки зрения хранилища всё ещё является «LLM с полным доступом». Это проблема в двух аспектах:
LLM можно манипулировать. Вредоносное содержимое в заметке или в ответе инструмента может попытаться указать агенту удалить или перезаписать что-то — prompt injection не гипотетичен.
«Аутентифицированный один раз» не должно означать «авторизованный навсегда». Длительная OAuth-сессия не должна давать одной и той же LLM неограниченное право записи бессрочно, без свежего доказательства присутствия человека.
Решение здесь — модель уровней риска для каждого инструмента с короткоживущим handle способности (15 мин), который авторизует чтение, и подтверждением passkey на каждый вызов, которое авторизует любую запись или удаление — формируемым на основе реальных аргументов, полученных сервером, а не текста, контролируемого LLM.
Архитектура
Cliente MCP remoto (Claude.ai, via Custom Connector)
│ HTTPS (OAuth Google + allowlist -- fora do escopo deste
│ README; ver mcp-oauth-gateway/enquire-mcp-gateway)
▼
┌───────────────────────────────────────────────────────────┐
│ gateway │
│ │
│ StepUpMiddleware -- por tool call: │
│ 1. policy.yaml decide o nivel (0/1/2) da tool │
│ 2. L0 (tools de auth) -- sempre passa │
│ 3. L1 (leitura) -- exige handle de sessao valido │
│ (senao devolve AUTH_REQUIRED + URL de unlock) │
│ 4. L2 (escrita/delete) -- exige confirmacao fresca │
│ por chamada (args_digest HMAC liga a aprovacao aos │
│ argumentos EXATOS; senao devolve CONFIRMATION_REQUIRED) │
│ │
│ Tools injetadas (nivel 0, sempre disponiveis): │
│ vault_auth_unlock / vault_auth_check / vault_auth_status │
└──────────────────────────┬───────────────────────────────────┘
│ Streamable HTTP + bearer
▼
┌───────────────────────────────────────────────────────────┐
│ auth-service │
│ │
│ WebAuthn (passkey) -- registro, challenges de unlock e de │
│ confirmacao, sessoes (SQLite), audit log append-only. │
│ So alcancavel via rotas /internal (X-Gateway-Key) do │
│ gateway, ou pelas telas publicas /unlock, /confirm, │
│ /register (esta ultima so com token de bootstrap). │
└──────────────────────────┬───────────────────────────────────┘
│ nunca fala com o backend
│ diretamente -- so autentica
▼
(o handle/token volta ao Claude via
gateway, que entao repassa a chamada
original ao backend)
│
▼
┌───────────────────────────────────────────────────────────┐
│ backend │
│ enquire-mcp (serve-http, vault Obsidian) │
└───────────────────────────────────────────────────────────┘gateway никогда не хранит никаких учётных данных — он только обращается к auth-service (внутренние маршруты, аутентифицируемые через GATEWAY_KEY) с вопросами «этот handle авторизует этот инструмент?» или «это подтверждение одобрило именно эти аргументы?». Человеку никогда не нужно вводить или вставлять что-либо в чат: вся процедура passkey происходит в браузере, по URL, который обслуживает auth-service.
Уровни риска
Уровень | Что требуется | Пример |
L0 | Ничего — всегда разрешено |
|
L1 | Действительный handle сессии (абсолютный TTL 15 мин, idle 5 мин) |
|
L2 | Подтверждение passkey на каждый вызов, привязанное к точным аргументам через |
|
policies/policy.yaml сопоставляет каждый инструмент бэкенда с уровнем. Deny-by-default: любой инструмент, не сопоставленный явно, попадает на самый ограничительный уровень (default_level: 2) — если enquire-mcp получит новый инструмент в обновлении (бэкенд запускается через npx -y, поэтому версия может измениться при любом подъёме), он появится защищённым, а не открытым. Смотрите комментарии в самом policies/policy.yaml о происхождении используемых имён инструментов и о том, что ещё нужно проверить вживую перед продакшеном.
Настройка
Требуются Docker и Docker Compose. Три сервиса (gateway, auth-service, backend) поднимаются вместе.
1. Переменные окружения
cp .env.example .env # Windows: Copy-Item .env.example .envЗаполните в корне репозитория:
Google OAuth (
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,PUBLIC_BASE_URL,ALLOWED_EMAILS) — тот же шаблон, что и вmcp-oauth-gateway; см. README того проекта с пошаговой инструкцией по созданию OAuth Client в Google Cloud Console.WebAuthn (
WEBAUTHN_RP_ID,WEBAUTHN_RP_NAME,PUBLIC_ORIGIN,GATEWAY_KEY,DIGEST_KEY) — см. предупреждение ниже перед установкойWEBAUTHN_RP_ID. СгенерируйтеGATEWAY_KEYиDIGEST_KEYс помощьюopenssl rand -hex 32.Backend (
BACKEND_BEARER_TOKEN,OBSIDIAN_VAULT_PATH) — общий токен междуgatewayиbackendи путь на хосте к защищаемому хранилищу Obsidian.
WEBAUTHN_RP_ID— ПОСТОЯННАЯ. Это домен (без порта и протокола), который встраивается в саму подпись WebAuthn каждой зарегистрированной passkey. Изменение этого значения после первой регистрации инвалидирует ВСЕ passkey — всем придётся регистрироваться заново, с новым bootstrap. Определите окончательный домен (тот же хост, что иPUBLIC_BASE_URL, безhttps://) до регистрации первой passkey, а не после.auth-serviceотказывается запускаться без этой переменной (src/authsvc/config.py) — намеренно: молчаливый default здесь был бы хуже, чем падение при загрузке.
2. Поднять стек
docker compose --env-file .env -f docker/docker-compose.yml up -d --build
docker compose --env-file .env -f docker/docker-compose.yml logs -f auth-service--env-file .env не опционально — Docker Compose разрешает ${VAR} из compose относительно каталога самого файла (docker/), а не корня репозитория. Запуск без этого флага приводит к тому, что OBSIDIAN_VAULT_PATH попадает в тихий fallback (docker/vault, пустой) вместо реального хранилища, без какой-либо видимой ошибки. См. комментарий Uso: в верхней части docker/docker-compose.yml для полной информации (найдено в ревью Task 17).
3. Регистрация первой passkey (bootstrap)
В логах auth-service найдите:
[bootstrap] token de registro (10 min): <token>Откройте <PUBLIC_BASE_URL>/register?t=<token> в браузере устройства с passkey (телефон или совместимый менеджер паролей) и завершите регистрацию. Токен истекает через 10 минут; если не уложились в срок, перезапустите auth-service (docker compose restart auth-service), чтобы сгенерировать новый — это также обнуляет незавершённые сессии/challenges (SESSION_PURGE_ON_START=true по умолчанию).
Зарегистрируйте как минимум две passkey (например, телефон + менеджер паролей), пока токен bootstrap ещё действителен — это митигация данного проекта для случая «я потерял устройство»: кода восстановления нет (осознанное решение; см. спецификацию дизайна, раздел открытых решений).
4. Подключение через Custom Connector
В claude.ai -> Settings -> Connectors -> Add custom connector вставьте <PUBLIC_BASE_URL>/mcp. Оставьте поля OAuth Client пустыми (динамическая регистрация). После входа с помощью учётной записи Google, присутствующей в ALLOWED_EMAILS, полный сценарий проверки (unlock, чтение, запись с подтверждением и тест с двумя разговорами) находится в tests/integration/test_e2e_manual.md.
Известные ограничения
A8 — Человек B, открывающий тот же разговор в пределах 15-минутного окна, наследует handle. Это реальная дыра, уже задокументированная и принятая по дизайну, в модели handle: handle сессии (L1) не привязан к личности того, кто читает разговор в данный момент, а только к разговору, в котором он родился. Если учётная запись Claude является общей и Человек B открывает тот же разговор, который разблокировал Человек A — не новый разговор — в течение 15 минут абсолютного TTL (или 5 мин idle), B наследует возможность чтения (L1), полученную A. Смягчено коротким TTL, idle timeout и дополнительной привязкой к
Mcp-Session-Id, когда клиент предоставляет его стабильно, — но не устранено. Запись (L2) остаётся недостижимой для B в любом случае, поскольку требует свежей подписи passkey на каждый вызов. См. раздел A8 спецификации дизайна (docs/superpowers/specs/2026-08-16-mcp-stepup-auth-proxy-design.md) для полного анализа угроз. Это не баг, который нужно молча исправить — это известное ограничение модели handle, общего для разговора, и Шаг 7 сценария вtests/integration/test_e2e_manual.mdсуществует именно для того, чтобы доказать, что отдельный случай (новый разговор) корректно заблокирован.Rate limiting не подключён ни к одному пути запроса. Модуль
src/authsvc/ratelimit.py(скользящее окно в памяти, классJanela) существует и имеет собственные тесты, но ни один маршрутauth-serviceилиgatewayне инстанцирует и не вызывает его — он не «подключён». На практике это означает, что митигация «brute force handle» и «систематического сканирования хранилища», описанная в разделе 20 (Тесты безопасности) и в разделе 14 (Защита от prompt injection, пункт 4) спецификации дизайна, всё ещё не существует в продакшене, хотя базовый код готов. Это реальный пробел, не покрытый ни одним другим контролем этого проекта —policies/policy.yamlсодержит секциюrate_limitsс примерными значениями (level_1: { calls: 60, window_s: 300 }), но ничто в текущемgateway_main.pyилиsrc/stepup/middleware.pyне читает эти значения, чтобы фактически ограничивать вызовы. Прежде чем выставлять этот шлюз на использование с реальным объёмом (а не только одним доверенным пользователем), подключениеratelimit.Janelaк пути L1 (и, в идеале, также к попыткам challenge/подтверждения вauth-service) следует рассматривать как приоритет, а не как полировку.Остальные структурные ограничения (без супервизии процессов, общий секрет
BACKEND_BEARER_TOKENбез разграничения по вызывающему, для публичной экспозиции требуется собственный туннель) — те же, что и уmcp-oauth-gateway, от которого этот проект наследует уровень OAuth/allowlist — см. README того проекта для подробностей.
Тесты
# Windows
.venv\Scripts\pytest.exe -v
# Linux/macOS
.venv/bin/pytest -vПокрывают: политику авторизации (src/stepup/policy.py), middleware для step-up (уровни, AUTH_REQUIRED/CONFIRMATION_REQUIRED), auth-service (WebAuthn, сессии, challenges, подтверждения, журнал аудита, HMAC-дайджест) и разрешение конфигурации docker-compose.yml (включая два режима ошибки при отсутствующем --env-file .env).
Сквозной сценарий против реального MCP-клиента и физической passkey не входит в этот набор — см. tests/integration/test_e2e_manual.md.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Remote MCP for Copilot CLI switch gate MCP, structured receipts, audit logs, and reviewer-ready evid
Self-hosted federated MCP gateway: one OAuth 2.1 MCP server in front of N apps, user-level scopes.
MCP server teaching AI agents to implement TideCloak: auth, E2EE, IGA, security analysis
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/joaorura/mcp-stepup-gateway'
If you have feedback or need assistance with the MCP directory API, please join our Discord server