mcp-stepup-gateway
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:
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.
„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 |
|
L1 | Gültiges Sitzungs-Handle (absolute TTL 15 Min., Idle 5 Min.) |
|
L2 | Passkey-Bestätigung pro Aufruf, gebunden an die exakten Argumente über |
|
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 .envFülle im Repository-Stamm aus:
Google OAuth (
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,PUBLIC_BASE_URL,ALLOWED_EMAILS) – gleiches Muster wie beimmcp-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 duWEBAUTHN_RP_IDfestlegst. GeneriereGATEWAY_KEYundDIGEST_KEYmitopenssl rand -hex 32.Backend (
BACKEND_BEARER_TOKEN,OBSIDIAN_VAULT_PATH) – das zwischengatewayundbackendgeteilte Token sowie der Pfad auf dem Host zum zu schützenden Obsidian-Vault.
WEBAUTHN_RP_IDist 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 wiePUBLIC_BASE_URL, ohnehttps://), bevor du die erste Passkey registrierst, nicht danach. Derauth-serviceweigert 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 intests/integration/test_e2e_manual.mdexistiert 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, KlasseJanela) existiert und hat eigene Tests, aber keine Route desauth-serviceund auch nicht desgatewayinstanziiert 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.yamlhat einen Abschnittrate_limitsmit Beispielwerten (level_1: { calls: 60, window_s: 300 }), aber nichts im aktuellengateway_main.pyodersrc/stepup/middleware.pyliest 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 vonratelimit.Janelamit dem L1-Pfad (und idealerweise auch mit Challenge-/Bestätigungsversuchen imauth-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 beimmcp-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 -vAbgedeckt 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.
This server cannot be installed
Maintenance
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
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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