Skip to main content
Glama
joaorura

mcp-stepup-gateway

by joaorura

mcp-stepup-gateway

Ein MCP-Gateway, das on-demand eine Passkey (WebAuthn) verlangt – „Step-up-Auth“ –, bevor es einem Remote-Client (Claude.ai, über Custom Connector) erlaubt, ein durch enquire-mcp geschütztes Obsidian-Vault zu lesen oder zu schreiben. Google-Login und Allowlist (wie beim mcp-oauth-gateway) entscheiden, wer sich verbinden darf; dieses Projekt entscheidet, Tool für Tool, was diese Person tun kann, ohne die Identität erneut nachzuweisen, und was ein frisches Antippen der Passkey erfordert.

Es entstand aus einem konkreten Fall: mcp-oauth-gateway/enquire-mcp-gateway lösen bereits „authentifizieren, wer sich verbindet“ (OAuth + Allowlist). Was fehlte, war eine zweite Ebene: Selbst innerhalb der Allowlist sollte nicht jeder Tool-Call gleichermaßen frei sein. Eine Notiz lesen ist günstig; Inhalte des Vaults über eine LLM, die unter Prompt-Injection stehen könnte, zu löschen oder neu zu schreiben, ist es nicht. Dieses Gateway fügt diese Unterscheidung hinzu, ohne enquise-mcp selbst anzufassen.

Warum es das gibt

Ein per OAuth authentifizierter Remote-MCP-Client ist aus Sicht des Vaults immer noch „eine LLM mit vollem Zugriff“. Das ist in zwei Achsen ein Problem:

  1. Die LLM kann manipuliert werden. Bösartiger Inhalt in einer Notiz oder in einer Tool-Antwort kann versuchen, den Agenten anzuweisen, Dinge zu löschen oder zu überschreiben – Prompt-Injection ist nicht hypothetisch.

  2. „Einmal authentifiziert“ sollte nicht „für immer autorisiert“ bedeuten. Eine langlebige OAuth-Sitzung sollte derselben LLM nicht auf unbestimmte Zeit uneingeschränkte Schreibrechte geben, ohne einen frischen Beweis menschlicher Anwesenheit.

Die Lösung hier ist ein Modell von Risikostufen pro Tool, mit einem kurzlebigen Capability-Handle (15 Min.), das Lesen autorisiert, und einer Passkey-Bestätigung pro Aufruf, die jedes Schreiben oder Löschen autorisiert – gerendert aus den tatsächlichen Argumenten, die der Server erhalten hat, niemals aus Text, den die LLM kontrolliert.

Architektur

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

Das gateway speichert nie irgendeine Anmeldedaten – es spricht nur mit dem auth-service (interne Routen, authentifiziert über GATEWAY_KEY), um zu fragen: „Autorisiert dieses Handle dieses Tool?“ oder „Hat diese Bestätigung genau diese Argumente genehmigt?“. Der Mensch tippt oder fügt im Chat nichts ein: Die gesamte Passkey-Zeremonie findet im Browser statt, unter einer URL, die der auth-service ausliefert.

Risikostufen

Stufe

Erfordert

Beispiel

L0

Nichts – immer freigegeben

vault_auth_unlock, vault_auth_check, vault_auth_status

L1

Gültiges Sitzungs-Handle (absolute TTL 15 Min., Idle 5 Min.)

obsidian_search, obsidian_read_note, obsidian_list_notes

L2

Passkey-Bestätigung pro Aufruf, gebunden an die exakten Argumente über args_digest (HMAC-SHA256)

obsidian_create_note, obsidian_append_to_note, obsidian_archive_note

policies/policy.yaml ordnet jedes Tool des Backends einer Stufe zu. Deny-by-default: Jedes nicht explizit zugeordnete Tool fällt in die restriktivste Stufe (default_level: 2) – wenn enquire-mcp bei einem Update ein neues Tool bekommt (das Backend läuft mit npx -y, kann also bei jedem Start die Version wechseln), kommt es geschützt an, nicht offen. Siehe die Kommentare in policies/policy.yaml selbst zur Herkunft der verwendeten Tool-Namen und was vor der Produktion noch live verifiziert werden muss.

Setup

Erfordert Docker und Docker Compose. Die drei Dienste (gateway, auth-service, backend) starten gemeinsam.

1. Umgebungsvariablen

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

Fülle im Repository-Stamm aus:

  • Google OAuth (GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, PUBLIC_BASE_URL, ALLOWED_EMAILS) – gleiches Muster wie beim mcp-oauth-gateway; siehe die README jenes Projekts für die Schritt-für-Schritt-Anleitung zum Erstellen des OAuth-Clients in der Google Cloud Console.

  • WebAuthn (WEBAUTHN_RP_ID, WEBAUTHN_RP_NAME, PUBLIC_ORIGIN, GATEWAY_KEY, DIGEST_KEY) – siehe den Hinweis unten, bevor du WEBAUTHN_RP_ID festlegst. Generiere GATEWAY_KEY und DIGEST_KEY mit openssl rand -hex 32.

  • Backend (BACKEND_BEARER_TOKEN, OBSIDIAN_VAULT_PATH) – das zwischen gateway und backend geteilte Token sowie der Pfad auf dem Host zum zu schützenden Obsidian-Vault.

WEBAUTHN_RP_ID ist PERMANENT. Es ist die Domain (ohne Port, ohne Protokoll), die in die WebAuthn-Signatur jeder registrierten Passkey eingebettet wird. Diesen Wert nach der ersten Registrierung zu ändern, macht ALLE Passkeys ungültig – jeder muss sich erneut registrieren, mit einem neuen Bootstrap. Entscheide die endgültige Domain (derselbe Host wie PUBLIC_BASE_URL, ohne https://), bevor du die erste Passkey registrierst, nicht danach. Der auth-service weigert sich zu starten, wenn diese Variable nicht gesetzt ist (src/authsvc/config.py) – bewusst: Ein stiller Default wäre hier schlimmer als ein Fehlschlag beim Booten.

2. Stack starten

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 ist nicht optional – Docker Compose löst ${VAR} aus dem Compose relativ zum Verzeichnis der Datei selbst (docker/) auf, nicht aus dem Repository-Stamm. Wenn du ohne diese Flagge startest, fällt OBSIDIAN_VAULT_PATH auf einen stillen Fallback (docker/vault, leer) zurück, statt auf das echte Vault, ohne sichtbaren Fehler. Siehe den Kommentar Uso: am Anfang von docker/docker-compose.yml für das vollständige Detail (Fund aus dem Review der Task 17).

3. Erste Passkey registrieren (Bootstrap)

Suche in den Logs des auth-service:

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

Öffne <PUBLIC_BASE_URL>/register?t=<token> im Browser eines Geräts mit Passkey (Handy oder ein kompatibler Passwortmanager) und schließe die Registrierung ab. Das Token läuft in 10 Minuten ab; wenn du die Frist verpasst, starte den auth-service neu (docker compose restart auth-service), um ein neues zu erzeugen – das setzt auch ausstehende Sitzungen/Challenges zurück (SESSION_PURGE_ON_START=true standardmäßig).

Registriere mindestens zwei Passkeys (z. B. Handy + Passwortmanager), solange das Bootstrap-Token noch gültig ist – das ist die Abschwächung dieses Projekts für „Gerät verloren“: Es gibt keinen Wiederherstellungscode (bewusste Entscheidung; siehe die Design-Spec, Abschnitt zu offenen Entscheidungen).

4. Als Custom Connector verbinden

In claude.ai -> Settings -> Connectors -> Add custom connector füge <PUBLIC_BASE_URL>/mcp ein. Lasse die OAuth-Client-Felder leer (dynamische Registrierung). Nach dem Login mit einem Google-Konto, das in ALLOWED_EMAILS enthalten ist, findest du das vollständige Verifikationsskript (Unlock, Lesen, Schreiben mit Bestätigung und den Zwei-Konversationen-Test) in tests/integration/test_e2e_manual.md.

Bekannte Einschränkungen

  • A8 – Person B öffnet dieselbe Konversation innerhalb des 15-Minuten-Fensters und erbt das Handle. Das ist die echte, bereits dokumentierte und designbedingt akzeptierte Lücke des Handle-Modells: Das Sitzungs-Handle (L1) ist nicht an die Identität der Person gebunden, die die Konversation gerade liest, sondern nur an die Konversation, in der es entstanden ist. Wenn das Claude-Konto geteilt wird und Person B die gleiche Konversation öffnet, die Person A entsperrt hat – nicht eine neue Konversation – innerhalb der 15 Minuten absoluter TTL (oder 5 Min. Idle), erbt B die Lesefähigkeit (L1), die A erhalten hat. Abgeschwächt durch kurze TTL, Idle-Timeout und zusätzliches Binden an Mcp-Session-Id, wenn der Client diese stabil liefert – aber nicht eliminiert. Schreiben (L2) bleibt für B in jedem Fall unerreichbar, weil es eine frische Passkey-Signatur pro Aufruf erfordert. Siehe Abschnitt A8 der Design-Spec (docs/superpowers/specs/2026-08-16-mcp-stepup-auth-proxy-design.md) für die vollständige Bedrohungsanalyse. Das ist kein still zu behebender Bug – es ist eine bekannte Einschränkung des pro Konversation geteilten Handle-Modells, und Schritt 7 des Skripts in tests/integration/test_e2e_manual.md existiert genau, um zu beweisen, dass der getrennte Fall (neue Konversation) korrekt blockiert ist.

  • Rate Limiting ist mit keinem Anforderungspfad verbunden. Das Modul src/authsvc/ratelimit.py (gleitendes Fenster im Speicher, Klasse Janela) existiert und hat eigene Tests, aber keine Route des auth-service und auch nicht des gateway instanziiert oder ruft es auf – es ist nicht „verdrahtet“. In der Praxis bedeutet das, dass die in Abschnitt 20 (Sicherheitstests) und Abschnitt 14 (Schutz gegen Prompt-Injection, Punkt 4) der Design-Spec beschriebene Abschwächung von „Handle-Brute-Force“ und „systematischem Durchsuchen des Vaults“ in Produktion noch nicht existiert, obwohl der Basiscode fertig ist. Das ist eine echte Lücke, die von keiner anderen Kontrolle dieses Projekts abgedeckt wird – policies/policy.yaml hat einen Abschnitt rate_limits mit Beispielwerten (level_1: { calls: 60, window_s: 300 }), aber nichts im aktuellen gateway_main.py oder src/stepup/middleware.py liest diese Werte, um Aufrufe tatsächlich zu begrenzen. Bevor dieses Gateway einer Nutzung mit echtem Volumen ausgesetzt wird (nicht nur einem einzelnen vertrauenswürdigen Benutzer), sollte das Verbinden von ratelimit.Janela mit dem L1-Pfad (und idealerweise auch mit Challenge-/Bestätigungsversuchen im auth-service) als Priorität behandelt werden, nicht als Politur.

  • Weitere strukturelle Einschränkungen (keine Prozessaufsicht, gemeinsames BACKEND_BEARER_TOKEN-Geheimnis ohne Bereichsbegrenzung je Aufrufer, öffentliche Bereitstellung erfordert einen eigenen Tunnel) sind dieselben wie beim mcp-oauth-gateway, von dem dieses Projekt die OAuth-/Allowlist-Schicht erbt – siehe die README jenes Projekts für die Details.

Tests

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

Abgedeckt werden: die Autorisierungsrichtlinie (src/stepup/policy.py), die Step-up-Middleware (Stufen, AUTH_REQUIRED/CONFIRMATION_REQUIRED), der auth-service (WebAuthn, Sitzungen, Challenges, Bestätigungen, Audit-Log, HMAC-Digest) und die Auflösung der Konfiguration von docker-compose.yml (einschließlich der beiden Fehlermodi der fehlenden --env-file .env).

Das End-to-End-Skript gegen einen echten MCP-Client und eine physische Passkey ist nicht Teil dieser Suite – siehe 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