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
访问服务器

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.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选