Skip to main content
Glama
joaorura

mcp-stepup-gateway

by joaorura

mcp-stepup-gateway

Um gateway MCP que exige passkey (WebAuthn) sob demanda -- "step-up auth" -- antes de permitir que um cliente remoto (Claude.ai, via Custom Connector) leia ou escreva num vault Obsidian protegido pelo enquire-mcp. Login Google e allowlist (como no mcp-oauth-gateway) decidem quem pode conectar; este projeto decide, tool a tool, o que essa pessoa pode fazer sem reprovar identidade de novo, e o que exige um toque fresco na passkey.

Nasceu de um caso concreto: o mcp-oauth-gateway/enquire-mcp-gateway ja resolvem "autenticar quem conecta" (OAuth + allowlist). O que faltava era uma segunda camada: mesmo dentro da allowlist, nem toda tool call deveria ser igualmente livre. Ler uma nota e barato; apagar ou reescrever conteudo do vault via uma LLM que pode estar sob prompt injection não é. Este gateway adiciona essa distincao sem tocar no enquire-mcp em si.

Por que isso existe

Um cliente MCP remoto autenticado por OAuth ainda e, do ponto de vista do vault, "uma LLM com acesso total". Isso e um problema em dois eixos:

  1. A LLM pode ser manipulada. Conteudo malicioso numa nota, ou numa resposta de ferramenta, pode tentar instruir o agente a apagar ou sobrescrever coisas -- prompt injection nao e hipotetico.

  2. "Autenticado uma vez" nao deveria significar "autorizado para sempre". Uma sessao OAuth de longa duracao nao deveria dar a mesma LLM permissao irrestrita de escrita indefinidamente, sem nenhuma prova fresca de presenca humana.

A solucao aqui e um modelo de niveis de risco por tool, com um handle de capacidade de vida curta (15 min) que autoriza leitura, e uma confirmacao com passkey por chamada que autoriza qualquer escrita ou delecao -- renderizada a partir dos argumentos reais que o servidor recebeu, nunca de texto que a LLM controla.

Arquitetura

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

O gateway nunca guarda credencial nenhuma -- so fala com o auth-service (rotas internas, autenticadas por GATEWAY_KEY) para perguntar "este handle autoriza esta tool?" ou "esta confirmacao aprovou exatamente estes argumentos?". O humano nunca digita nem cola nada no chat: toda a ceremonia de passkey acontece no navegador, numa URL que o auth-service serve.

Niveis de risco

Nivel

O que exige

Exemplo

L0

Nada -- sempre liberado

vault_auth_unlock, vault_auth_check, vault_auth_status

L1

Handle de sessao valido (TTL absoluto 15 min, idle 5 min)

obsidian_search, obsidian_read_note, obsidian_list_notes

L2

Confirmacao com passkey por chamada, ligada aos argumentos exatos via args_digest (HMAC-SHA256)

obsidian_create_note, obsidian_append_to_note, obsidian_archive_note

policies/policy.yaml mapeia cada tool do backend para um nivel. Deny-by-default: qualquer tool nao mapeada explicitamente cai no nivel mais restritivo (default_level: 2) -- se o enquire-mcp ganhar uma tool nova numa atualizacao (o backend roda npx -y, entao pode mudar de versao a qualquer subida), ela chega protegida, nao aberta. Ver os comentarios no proprio policies/policy.yaml para a proveniencia dos nomes de tool usados e o que ainda precisa ser verificado ao vivo antes de producao.

Setup

Requer Docker e Docker Compose. Os tres servicos (gateway, auth-service, backend) sobem juntos.

1. Variaveis de ambiente

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

Preencha, na raiz do repo:

  • Google OAuth (GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, PUBLIC_BASE_URL, ALLOWED_EMAILS) -- mesmo padrao do mcp-oauth-gateway; veja o README daquele projeto para o passo a passo de criar o OAuth Client no Google Cloud Console.

  • WebAuthn (WEBAUTHN_RP_ID, WEBAUTHN_RP_NAME, PUBLIC_ORIGIN, GATEWAY_KEY, DIGEST_KEY) -- ver a advertencia abaixo antes de definir WEBAUTHN_RP_ID. Gere GATEWAY_KEY e DIGEST_KEY com openssl rand -hex 32.

  • Backend (BACKEND_BEARER_TOKEN, OBSIDIAN_VAULT_PATH) -- o token compartilhado entre gateway e backend, e o caminho no host do vault Obsidian a proteger.

WEBAUTHN_RP_ID e PERMANENTE. E o dominio (sem porta, sem protocolo) que fica embutido na propria assinatura WebAuthn de cada passkey registrada. Mudar esse valor depois do primeiro registro invalida TODAS as passkeys -- todo mundo precisa registrar de novo, com um novo bootstrap. Decida o dominio definitivo (o mesmo host que PUBLIC_BASE_URL, sem https://) antes de registrar a primeira passkey, nao depois. O auth-service se recusa a subir sem esta variavel definida (src/authsvc/config.py) -- de proposito: um default silencioso aqui seria pior que falhar no boot.

2. Subir o 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 nao e opcional -- o Docker Compose resolve ${VAR} do compose relativo ao diretorio do proprio arquivo (docker/), nao da raiz do repo. Rodar sem essa flag faz OBSIDIAN_VAULT_PATH cair num fallback silencioso (docker/vault, vazio) em vez do vault real, sem nenhum erro visivel. Ver o comentario Uso: no topo de docker/docker-compose.yml para o detalhe completo (achado do review da Task 17).

3. Registrar a primeira passkey (bootstrap)

Nos logs do auth-service, procure:

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

Abra <PUBLIC_BASE_URL>/register?t=<token> no navegador de um dispositivo com passkey (celular, ou um gerenciador de senhas compativel) e complete o registro. O token expira em 10 minutos; se perder o prazo, reinicie o auth-service (docker compose restart auth-service) para gerar outro -- isso tambem zera sessoes/challenges pendentes (SESSION_PURGE_ON_START=true por padrao).

Registre pelo menos duas passkeys (celular + gerenciador de senhas, por exemplo) enquanto o token de bootstrap ainda vale -- e a mitigacao deste projeto para "perdi o dispositivo": nao ha codigo de recuperacao (decisao deliberada; ver a spec de design, secao de decisoes abertas).

4. Conectar como Custom Connector

Em claude.ai -> Settings -> Connectors -> Add custom connector, cole <PUBLIC_BASE_URL>/mcp. Deixe os campos de OAuth Client vazios (registro dinamico). Depois de logar com uma conta Google presente em ALLOWED_EMAILS, o roteiro completo de verificacao (unlock, leitura, escrita com confirmacao, e o teste de duas conversas) esta em tests/integration/test_e2e_manual.md.

Limitações conhecidas

  • A8 -- Pessoa B abrindo a mesma conversa dentro da janela de 15 minutos herda o handle. Este e o furo real, ja documentado e aceito por design, do modelo de handle: o handle de sessao (L1) nao esta ligado a identidade de quem esta lendo a conversa naquele momento, so a conversa onde ele nasceu. Se a conta Claude e compartilhada e a Pessoa B abre a mesma conversa que a Pessoa A destravou -- nao uma conversa nova -- dentro dos 15 minutos de TTL absoluto (ou 5 min de idle), B herda a capacidade de leitura (L1) que A obteve. Mitigado por TTL curto, idle timeout, e binding adicional ao Mcp-Session-Id quando o cliente o fornece de forma estavel -- mas nao eliminado. Escrita (L2) permanece inatingivel para B em qualquer caso, porque exige uma assinatura de passkey fresca por chamada. Ver a secao A8 da spec de design (docs/superpowers/specs/2026-08-16-mcp-stepup-auth-proxy-design.md) para a analise completa de ameaca. Isto nao e um bug a ser corrigido silenciosamente -- e uma limitacao conhecida do modelo de handle compartilhado por conversa, e o Passo 7 do roteiro em tests/integration/test_e2e_manual.md existe justamente para provar que o caso distinto (conversa nova) esta corretamente bloqueado.

  • Rate limiting nao esta conectado a nenhum caminho de requisicao. O modulo src/authsvc/ratelimit.py (janela deslizante em memoria, classe Janela) existe e tem testes proprios, mas nenhuma rota do auth-service nem do gateway o instancia ou chama -- ele nao esta "wireado". Na pratica, isso significa que a mitigacao de "brute force de handle" e "varredura sistematica do vault" descrita na secao 20 (Testes de seguranca) e na secao 14 (Protecao contra prompt injection, item 4) da spec de design ainda nao existe em producao, apesar do codigo base estar pronto. Isto e uma lacuna real, nao coberta por nenhum outro controle deste projeto -- policies/policy.yaml tem uma secao rate_limits com valores de exemplo (level_1: { calls: 60, window_s: 300 }), mas nada no gateway_main.py ou no src/stepup/middleware.py atual le esses valores para de fato limitar chamadas. Antes de expor este gateway a um uso com volume real (nao so um unico usuario confiavel), conectar ratelimit.Janela ao caminho de L1 (e, idealmente, tambem a tentativas de challenge/confirmacao no auth-service) deveria ser tratado como prioridade, nao como polimento.

  • Demais limitacoes estruturais (sem supervisao de processo, segredo BACKEND_BEARER_TOKEN compartilhado sem escopo por chamador, exposicao publica exige seu proprio tunel) sao as mesmas do mcp-oauth-gateway, do qual este projeto herda a camada de OAuth/allowlist -- ver o README daquele projeto para os detalhes.

Testes

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

Cobrem: a politica de autorizacao (src/stepup/policy.py), o middleware de step-up (niveis, AUTH_REQUIRED/CONFIRMATION_REQUIRED), o auth-service (WebAuthn, sessoes, challenges, confirmacoes, audit log, digest HMAC), e a resolucao de configuracao do docker-compose.yml (incluindo os dois modos de erro do --env-file .env ausente).

O roteiro ponta a ponta contra um cliente MCP real e uma passkey fisica nao esta nesta 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