mcp-stepup-gateway

mcp-stepup-gateway

An MCP gateway that enforces step-up authentication using WebAuthn passkeys for remote clients, requiring session handles for read operations and fresh passkey confirmations for writes to protect an Obsidian vault.

Category
Visit Server

README

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.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured