Skip to main content
Glama
joaorura

mcp-stepup-gateway

by joaorura

mcp-stepup-gateway

Un gateway MCP que exige passkey (WebAuthn) bajo demanda -- "step-up auth" -- antes de permitir que un cliente remoto (Claude.ai, vía Custom Connector) lea o escriba en un vault de Obsidian protegido por enquire-mcp. El inicio de sesión con Google y la allowlist (como en mcp-oauth-gateway) deciden quién puede conectarse; este proyecto decide, herramienta a herramienta, qué puede hacer esa persona sin volver a autenticar la identidad, y qué requiere un toque fresco en la passkey.

Nació de un caso concreto: mcp-oauth-gateway/enquire-mcp-gateway ya resuelven "autenticar quién se conecta" (OAuth + allowlist). Lo que faltaba era una segunda capa: incluso dentro de la allowlist, no toda llamada a herramienta debería ser igualmente libre. Leer una nota es barato; borrar o reescribir contenido del vault a través de una LLM que puede estar bajo prompt injection no lo es. Este gateway añade esa distinción sin tocar enquire-mcp en sí.

Por qué existe esto

Un cliente MCP remoto autenticado por OAuth sigue siendo, desde el punto de vista del vault, "una LLM con acceso total". Esto es un problema en dos ejes:

  1. La LLM puede ser manipulada. El contenido malicioso en una nota, o en una respuesta de herramienta, puede intentar instruir al agente para que borre o sobrescriba cosas -- la prompt injection no es hipotética.

  2. "Autenticado una vez" no debería significar "autorizado para siempre". Una sesión OAuth de larga duración no debería dar a la misma LLM permiso irrestricto de escritura indefinidamente, sin ninguna prueba fresca de presencia humana.

La solución aquí es un modelo de niveles de riesgo por herramienta, con un handle de capacidad de vida corta (15 min) que autoriza lectura, y una confirmación con passkey por llamada que autoriza cualquier escritura o borrado -- renderizada a partir de los argumentos reales que el servidor recibió, nunca de texto que la LLM controla.

Arquitectura

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)         │
└───────────────────────────────────────────────────────────┘

El gateway nunca guarda ninguna credencial -- solo habla con el auth-service (rutas internas, autenticadas por GATEWAY_KEY) para preguntar "¿este handle autoriza esta herramienta?" o "¿esta confirmación aprobó exactamente estos argumentos?". El humano nunca escribe ni pega nada en el chat: toda la ceremonia de passkey ocurre en el navegador, en una URL que sirve el auth-service.

Niveles de riesgo

Nivel

Qué exige

Ejemplo

L0

Nada -- siempre permitido

vault_auth_unlock, vault_auth_check, vault_auth_status

L1

Handle de sesión válido (TTL absoluto 15 min, idle 5 min)

obsidian_search, obsidian_read_note, obsidian_list_notes

L2

Confirmación con passkey por llamada, ligada a los argumentos exactos vía args_digest (HMAC-SHA256)

obsidian_create_note, obsidian_append_to_note, obsidian_archive_note

policies/policy.yaml mapea cada herramienta del backend a un nivel. Deny-by-default: cualquier herramienta no mapeada explícitamente cae en el nivel más restrictivo (default_level: 2) -- si enquire-mcp gana una herramienta nueva en una actualización (el backend ejecuta npx -y, por lo que puede cambiar de versión a cualquier subida), llega protegida, no abierta. Ver los comentarios en el propio policies/policy.yaml para la procedencia de los nombres de herramienta usados y lo que aún necesita ser verificado en vivo antes de producción.

Configuración

Requiere Docker y Docker Compose. Los tres servicios (gateway, auth-service, backend) se levantan juntos.

1. Variables de entorno

cp .env.example .env    # Windows: Copy-Item .env.example .env

Complete, en la raíz del repo:

  • Google OAuth (GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, PUBLIC_BASE_URL, ALLOWED_EMAILS) -- mismo patrón del mcp-oauth-gateway; consulta el README de ese proyecto para el paso a paso de crear el OAuth Client en Google Cloud Console.

  • WebAuthn (WEBAUTHN_RP_ID, WEBAUTHN_RP_NAME, PUBLIC_ORIGIN, GATEWAY_KEY, DIGEST_KEY) -- ver la advertencia a continuación antes de definir WEBAUTHN_RP_ID. Genera GATEWAY_KEY y DIGEST_KEY con openssl rand -hex 32.

  • Backend (BACKEND_BEARER_TOKEN, OBSIDIAN_VAULT_PATH) -- el token compartido entre gateway y backend, y la ruta en el host del vault de Obsidian a proteger.

WEBAUTHN_RP_ID es PERMANENTE. Es el dominio (sin puerto, sin protocolo) que queda incrustado en la propia firma WebAuthn de cada passkey registrada. Cambiar ese valor después del primer registro invalida TODAS las passkeys -- todo el mundo necesita registrarse de nuevo, con un nuevo bootstrap. Decide el dominio definitivo (el mismo host que PUBLIC_BASE_URL, sin https://) antes de registrar la primera passkey, no después. El auth-service se niega a arrancar sin esta variable definida (src/authsvc/config.py) -- a propósito: un default silencioso aquí sería peor que fallar en el boot.

2. Levantar el stack

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 no es opcional -- Docker Compose resuelve ${VAR} del compose relativo al directorio del propio archivo (docker/), no de la raíz del repo. Ejecutar sin esa bandera hace que OBSIDIAN_VAULT_PATH caiga en un fallback silencioso (docker/vault, vacío) en lugar del vault real, sin ningún error visible. Ver el comentario Uso: en la parte superior de docker/docker-compose.yml para el detalle completo (hallazgo del review de la Task 17).

3. Registrar la primera passkey (bootstrap)

En los logs del auth-service, busca:

[bootstrap] token de registro (10 min): <token>

Abre <PUBLIC_BASE_URL>/register?t=<token> en el navegador de un dispositivo con passkey (celular, o un gestor de contraseñas compatible) y completa el registro. El token expira en 10 minutos; si pierdes el plazo, reinicia el auth-service (docker compose restart auth-service) para generar otro -- esto también borra sesiones/challenges pendientes (SESSION_PURGE_ON_START=true por defecto).

Registra al menos dos passkeys (celular + gestor de contraseñas, por ejemplo) mientras el token de bootstrap aún sea válido -- es la mitigación de este proyecto para "perdí el dispositivo": no hay código de recuperación (decisión deliberada; ver la spec de diseño, sección de decisiones abiertas).

4. Conectar como Custom Connector

En claude.ai -> Settings -> Connectors -> Add custom connector, pega <PUBLIC_BASE_URL>/mcp. Deja los campos de OAuth Client vacíos (registro dinámico). Después de iniciar sesión con una cuenta de Google presente en ALLOWED_EMAILS, el guion completo de verificación (desbloqueo, lectura, escritura con confirmación, y la prueba de dos conversaciones) está en tests/integration/test_e2e_manual.md.

Limitaciones conocidas

  • A8 -- La persona B abriendo la misma conversación dentro de la ventana de 15 minutos hereda el handle. Este es el agujero real, ya documentado y aceptado por diseño, del modelo de handle: el handle de sesión (L1) no está ligado a la identidad de quien está leyendo la conversación en ese momento, solo a la conversación donde nació. Si la cuenta de Claude es compartida y la Persona B abre la misma conversación que la Persona A desbloqueó -- no una conversación nueva -- dentro de los 15 minutos de TTL absoluto (o 5 min de idle), B hereda la capacidad de lectura (L1) que A obtuvo. Mitigado por TTL corto, idle timeout, y vinculación adicional al Mcp-Session-Id cuando el cliente lo proporciona de forma estable -- pero no eliminado. La escritura (L2) permanece inalcanzable para B en cualquier caso, porque exige una firma de passkey fresca por llamada. Ver la sección A8 de la spec de diseño (docs/superpowers/specs/2026-08-16-mcp-stepup-auth-proxy-design.md) para el análisis completo de amenaza. Esto no es un bug a corregir silenciosamente -- es una limitación conocida del modelo de handle compartido por conversación, y el Paso 7 del guion en tests/integration/test_e2e_manual.md existe justamente para probar que el caso distinto (conversación nueva) está correctamente bloqueado.

  • El rate limiting no está conectado a ninguna ruta de solicitud. El módulo src/authsvc/ratelimit.py (ventana deslizante en memoria, clase Janela) existe y tiene pruebas propias, pero ninguna ruta del auth-service ni del gateway lo instancia o llama -- no está "cableado". En la práctica, esto significa que la mitigación de "brute force de handle" y "escaneo sistemático del vault" descrita en la sección 20 (Pruebas de seguridad) y en la sección 14 (Protección contra prompt injection, punto 4) de la spec de diseño aún no existe en producción, aunque el código base esté listo. Esto es una brecha real, no cubierta por ningún otro control de este proyecto -- policies/policy.yaml tiene una sección rate_limits con valores de ejemplo (level_1: { calls: 60, window_s: 300 }), pero nada en gateway_main.py ni en src/stepup/middleware.py actual lee esos valores para realmente limitar llamadas. Antes de exponer este gateway a un uso con volumen real (no solo un único usuario confiable), conectar ratelimit.Janela al camino de L1 (e, idealmente, también a intentos de challenge/confirmación en el auth-service) debería tratarse como prioridad, no como pulido.

  • Las demás limitaciones estructurales (sin supervisión de proceso, secreto BACKEND_BEARER_TOKEN compartido sin alcance por llamador, exposición pública requiere su propio túnel) son las mismas que las de mcp-oauth-gateway, del cual este proyecto hereda la capa de OAuth/allowlist -- ver el README de ese proyecto para los detalles.

Pruebas

# Windows
.venv\Scripts\pytest.exe -v
# Linux/macOS
.venv/bin/pytest -v

Cubren: la política de autorización (src/stepup/policy.py), el middleware de step-up (niveles, AUTH_REQUIRED/CONFIRMATION_REQUIRED), el auth-service (WebAuthn, sesiones, challenges, confirmaciones, audit log, digest HMAC), y la resolución de configuración del docker-compose.yml (incluidos los dos modos de error del --env-file .env ausente).

El recorrido de extremo a extremo contra un cliente MCP real y una passkey física no está en esta suite -- ver tests/integration/test_e2e_manual.md.

-
license - not tested
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

View all MCP Connectors

Latest Blog Posts

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