mcp-stepup-gateway
Provides secure access to an Obsidian vault with risk-based access levels, allowing read operations with a short-lived session handle and requiring passkey confirmation for write and delete operations.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-stepup-gatewaycreate a note about the client meeting"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-stepup-gateway
An MCP gateway that requires passkey (WebAuthn) on demand -- "step-up auth" -- before allowing a remote client (Claude.ai, via Custom Connector) to read or write to an Obsidian vault protected by enquire-mcp. Google login and allowlist (as in mcp-oauth-gateway) decide who can connect; this project decides, tool by tool, what that person can do without proving identity again, and what requires a fresh passkey touch.
It was born from a concrete case: mcp-oauth-gateway/enquire-mcp-gateway already solve "authenticate who connects" (OAuth + allowlist). What was missing was a second layer: even within the allowlist, not every tool call should be equally free. Reading a note is cheap; deleting or rewriting vault content via an LLM that may be under prompt injection is not. This gateway adds that distinction without touching enquire-mcp itself.
Why this exists
A remote MCP client authenticated by OAuth is still, from the vault's point of view, "an LLM with full access." This is a problem on two axes:
The LLM can be manipulated. Malicious content in a note, or in a tool response, can try to instruct the agent to delete or overwrite things -- prompt injection is not hypothetical.
"Authenticated once" should not mean "authorized forever." A long-lived OAuth session should not give the same LLM unrestricted write permission indefinitely, without any fresh proof of human presence.
The solution here is a per-tool risk level model, with a short-lived capability handle (15 min) that authorizes reading, and a per-call passkey confirmation that authorizes any write or deletion -- rendered from the actual arguments the server received, never from text the LLM controls.
Architecture
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) │
└───────────────────────────────────────────────────────────┘The gateway never stores any credentials -- it only talks to the auth-service (internal routes, authenticated by GATEWAY_KEY) to ask "does this handle authorize this tool?" or "did this confirmation approve exactly these arguments?". The human never types or pastes anything in the chat: the entire passkey ceremony happens in the browser, at a URL served by the auth-service.
Risk levels
Level | What it requires | Example |
L0 | Nothing -- always allowed |
|
L1 | Valid session handle (absolute TTL 15 min, idle 5 min) |
|
L2 | Passkey confirmation per call, bound to the exact arguments via |
|
policies/policy.yaml maps each backend tool to a level. Deny-by-default: any tool not explicitly mapped falls into the most restrictive level (default_level: 2) -- if enquire-mcp gains a new tool in an update (the backend runs npx -y, so it can change version at any start), it arrives protected, not open. See the comments in policies/policy.yaml itself for the provenance of the tool names used and what still needs to be verified live before production.
Setup
Requires Docker and Docker Compose. The three services (gateway, auth-service, backend) come up together.
1. Environment variables
cp .env.example .env # Windows: Copy-Item .env.example .envFill in, at the repository root:
Google OAuth (
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,PUBLIC_BASE_URL,ALLOWED_EMAILS) -- same pattern asmcp-oauth-gateway; see that project's README for the step-by-step on creating the OAuth Client in the Google Cloud Console.WebAuthn (
WEBAUTHN_RP_ID,WEBAUTHN_RP_NAME,PUBLIC_ORIGIN,GATEWAY_KEY,DIGEST_KEY) -- see the warning below before settingWEBAUTHN_RP_ID. GenerateGATEWAY_KEYandDIGEST_KEYwithopenssl rand -hex 32.Backend (
BACKEND_BEARER_TOKEN,OBSIDIAN_VAULT_PATH) -- the token shared betweengatewayandbackend, and the host path of the Obsidian vault to protect.
WEBAUTHN_RP_IDis PERMANENT. It is the domain (no port, no protocol) that is embedded in the WebAuthn signature of every registered passkey itself. Changing this value after the first registration invalidates ALL passkeys -- everyone needs to register again, with a new bootstrap. Decide the final domain (the same host asPUBLIC_BASE_URL, withouthttps://) before registering the first passkey, not after. Theauth-servicerefuses to start without this variable set (src/authsvc/config.py) -- deliberately: a silent default here would be worse than failing at boot.
2. Bring up the 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 is not optional -- Docker Compose resolves ${VAR} in the compose file relative to the directory of the file itself (docker/), not the repository root. Running without that flag makes OBSIDIAN_VAULT_PATH fall into a silent fallback (docker/vault, empty) instead of the real vault, with no visible error. See the Uso: comment at the top of docker/docker-compose.yml for the full detail (found in the Task 17 review).
3. Register the first passkey (bootstrap)
In the auth-service logs, look for:
[bootstrap] token de registro (10 min): <token>Open <PUBLIC_BASE_URL>/register?t=<token> in the browser of a device with a passkey (phone, or a compatible password manager) and complete the registration. The token expires in 10 minutes; if you miss the deadline, restart the auth-service (docker compose restart auth-service) to generate another one -- this also clears pending sessions/challenges (SESSION_PURGE_ON_START=true by default).
Register at least two passkeys (phone + password manager, for example) while the bootstrap token is still valid -- this is this project's mitigation for "I lost the device": there is no recovery code (deliberate decision; see the design spec, open decisions section).
4. Connect as a Custom Connector
In claude.ai -> Settings -> Connectors -> Add custom connector, paste <PUBLIC_BASE_URL>/mcp. Leave the OAuth Client fields empty (dynamic registration). After logging in with a Google account present in ALLOWED_EMAILS, the full verification script (unlock, read, write with confirmation, and the two-conversation test) is in tests/integration/test_e2e_manual.md.
Known limitations
A8 -- Person B opening the same conversation within the 15-minute window inherits the handle. This is the real gap, already documented and accepted by design, of the handle model: the session handle (L1) is not tied to the identity of who is reading the conversation at that moment, only to the conversation where it was born. If the Claude account is shared and Person B opens the same conversation that Person A unlocked -- not a new conversation -- within the 15-minute absolute TTL (or 5 min idle), B inherits the read capability (L1) that A obtained. Mitigated by short TTL, idle timeout, and additional binding to
Mcp-Session-Idwhen the client provides it stably -- but not eliminated. Write (L2) remains unreachable for B in any case, because it requires a fresh passkey signature per call. See section A8 of the design spec (docs/superpowers/specs/2026-08-16-mcp-stepup-auth-proxy-design.md) for the full threat analysis. This is not a bug to be silently fixed -- it is a known limitation of the per-conversation shared handle model, and Step 7 of the script intests/integration/test_e2e_manual.mdexists precisely to prove that the distinct case (new conversation) is correctly blocked.Rate limiting is not wired into any request path. The module
src/authsvc/ratelimit.py(in-memory sliding window, classJanela) exists and has its own tests, but no route inauth-serviceorgatewayinstantiates or calls it -- it is not "wired". In practice, this means the mitigation for "handle brute force" and "systematic vault scanning" described in section 20 (Security tests) and section 14 (Prompt injection protection, item 4) of the design spec still does not exist in production, even though the base code is ready. This is a real gap, not covered by any other control in this project --policies/policy.yamlhas arate_limitssection with example values (level_1: { calls: 60, window_s: 300 }), but nothing ingateway_main.pyor the currentsrc/stepup/middleware.pyreads those values to actually limit calls. Before exposing this gateway to real-volume usage (not just a single trusted user), wiringratelimit.Janelainto the L1 path (and, ideally, also into challenge/confirmation attempts in theauth-service) should be treated as a priority, not as polish.Other structural limitations (no process supervision,
BACKEND_BEARER_TOKENshared as a secret without per-caller scoping, public exposure requires your own tunnel) are the same asmcp-oauth-gateway, from which this project inherits the OAuth/allowlist layer -- see that project's README for details.
Tests
# Windows
.venv\Scripts\pytest.exe -v
# Linux/macOS
.venv/bin/pytest -vThey cover: the authorization policy (src/stepup/policy.py), the step-up middleware (levels, AUTH_REQUIRED/CONFIRMATION_REQUIRED), the auth-service (WebAuthn, sessions, challenges, confirmations, audit log, HMAC digest), and the docker-compose.yml configuration resolution (including the two error modes of the missing --env-file .env).
The end-to-end script against a real MCP client and a physical passkey is not in this suite -- see 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