mcp-stepup-gateway
mcp-stepup-gateway
一个 MCP 网关,要求 按需提供通行密钥(WebAuthn)——“升级认证”——然后才允许远程客户端(Claude.ai,通过 Custom Connector)读取或写入受 enquire-mcp 保护的 Obsidian 仓库。Google 登录和允许列表(如 mcp-oauth-gateway 中那样)决定 谁 可以连接;本项目则逐个工具地决定该人可以在不重新验证身份的情况下做什么,以及什么需要新的通行密钥触摸。
它诞生于一个具体案例:mcp-oauth-gateway/enquire-mcp-gateway 已经解决了“认证谁在连接”(OAuth + allowlist)。缺少的是第二层:即使在允许列表内,也不是每次工具调用都应该同样自由。读取笔记是廉价的;通过可能受到提示注入的 LLM 删除或重写仓库内容则不是。这个网关在不触碰 enquire-mcp 本身的情况下增加了这一区分。
它为什么存在
从仓库的角度看,一个通过 OAuth 认证的远程 MCP 客户端仍然只是“一个拥有完全访问权限的 LLM”。这在两个方面是个问题:
LLM 可能被操纵。 笔记或工具响应中的恶意内容可能试图指示代理删除或覆盖内容——提示注入并非假设。
“一次认证”不应该意味着“永远授权”。 一个长期存在的 OAuth 会话不应该在没有全新的人类在场证明的情况下,无限期地给予同一个 LLM 不受限制的写入权限。
这里的解决方案是一种 按工具划分风险级别 的模型,带有一个 短期(15 分钟)的能力句柄 用于授权读取,以及一个 每次调用都需要通行密钥确认 来授权任何写入或删除——该确认根据服务器实际收到的参数渲染,而不是根据 LLM 控制的文本。
Related MCP server: Obsidian MCP Wrapper
架构
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) │
└───────────────────────────────────────────────────────────┘gateway 从不存储任何凭据——它只与 auth-service(内部路由,通过 GATEWAY_KEY 认证)通信,询问“这个句柄是否授权此工具?”或“这个确认是否恰好批准了这些参数?”。人类用户绝不会在聊天中输入或粘贴任何内容:整个通行密钥仪式都发生在浏览器中,在一个由 auth-service 提供的 URL 上。
风险级别
级别 | 要求 | 示例 |
L0 | 无——始终放行 |
|
L1 | 有效的会话句柄(绝对 TTL 15 分钟,空闲 5 分钟) |
|
L2 | 每次调用均需通行密钥确认,并通过 |
|
policies/policy.yaml 将后端的每个工具映射到一个级别。默认拒绝: 任何未明确映射的工具都会落入最严格的级别(default_level: 2)——如果 enquire-mcp 在更新中获得了新工具(后端运行 npx -y,因此每次启动都可能改变版本),那么该工具到达时会处于 受保护 状态,而不是开放状态。有关所用工具名称的来源以及在生产环境之前需要现场验证的内容,请参阅 policies/policy.yaml 中的注释。
设置
需要 Docker 和 Docker Compose。三个服务(gateway、auth-service、backend)会一起启动。
1. 环境变量
cp .env.example .env # Windows: Copy-Item .env.example .env在仓库根目录填写:
Google OAuth(
GOOGLE_CLIENT_ID、GOOGLE_CLIENT_SECRET、PUBLIC_BASE_URL、ALLOWED_EMAILS)——与mcp-oauth-gateway相同;请参阅该项目的 README,了解在 Google Cloud Console 中创建 OAuth Client 的步骤。WebAuthn(
WEBAUTHN_RP_ID、WEBAUTHN_RP_NAME、PUBLIC_ORIGIN、GATEWAY_KEY、DIGEST_KEY)——在设置WEBAUTHN_RP_ID之前,请先阅读下面的警告。使用openssl rand -hex 32生成GATEWAY_KEY和DIGEST_KEY。Backend(
BACKEND_BEARER_TOKEN、OBSIDIAN_VAULT_PATH)——gateway和backend之间共享的令牌,以及要保护的主机上的 Obsidian 仓库路径。
WEBAUTHN_RP_ID是永久性的。 它是嵌入每个已注册通行密钥的 WebAuthn 签名中的域名(不带端口、不带协议)。在首次注册之后更改此值 会使所有通行密钥失效——所有人都需要用新的 bootstrap 重新注册。请在注册第一个通行密钥 之前决定最终域名(与PUBLIC_BASE_URL相同的主机,不带https://),而不是之后。auth-service在未定义此变量的情况下会拒绝启动(src/authsvc/config.py)——这是故意的:在这里使用静默默认值比启动失败更糟糕。
2. 启动堆栈
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 不是可选的——Docker Compose 解析 compose 中的 ${VAR} 时,相对于 compose 文件自身的目录(docker/),而不是仓库根目录。如果不使用此标志运行,OBSIDIAN_VAULT_PATH 会落入一个静默回退(docker/vault,为空),而不是真实的仓库,且没有任何可见错误。请参阅 docker/docker-compose.yml 顶部的 Uso: 注释以了解完整细节(来自 Task 17 审查的发现)。
3. 注册第一个通行密钥(bootstrap)
在 auth-service 的日志中,查找:
[bootstrap] token de registro (10 min): <token>在带有通行密钥的设备(手机或兼容的密码管理器)的浏览器中打开 <PUBLIC_BASE_URL>/register?t=<token>,并完成注册。令牌在 10 分钟后过期;如果超时,请重启 auth-service(docker compose restart auth-service)以生成新的——这也会清除挂起的会话/挑战(默认 SESSION_PURGE_ON_START=true)。
趁 bootstrap 令牌仍然有效,至少注册两个通行密钥(例如手机 + 密码管理器)——这是本项目针对“我丢失了设备”的缓解措施:没有恢复代码(刻意决定;请参阅设计规范中的开放决策部分)。
4. 作为 Custom Connector 连接
在 claude.ai -> Settings -> Connectors -> Add custom connector 中,粘贴 <PUBLIC_BASE_URL>/mcp。将 OAuth Client 字段留空(动态注册)。使用 ALLOWED_EMAILS 中的 Google 帐户登录后,完整的验证流程(unlock、读取、带确认的写入,以及双对话测试)见 tests/integration/test_e2e_manual.md。
已知限制
A8——用户 B 在 15 分钟窗口内打开同一对话会继承句柄。 这是句柄模型真正的漏洞,已被记录并 按设计接受:会话句柄(L1)不绑定到当下正在阅读对话的人的身份,而只绑定到它诞生的对话。如果 Claude 帐户是共享的,而用户 B 在绝对 TTL 15 分钟(或空闲 5 分钟)内打开了用户 A 已解锁的 同一 对话——而不是新对话——那么 B 就继承了 A 获得的读取能力(L1)。这通过短 TTL、空闲超时,以及当客户端稳定提供
Mcp-Session-Id时额外的绑定来缓解——但 并未消除。在任何情况下,写入(L2)对 B 仍然不可及,因为它每次调用都需要新的通行密钥签名。请参阅设计规范的 A8 部分(docs/superpowers/specs/2026-08-16-mcp-stepup-auth-proxy-design.md)以获取完整的威胁分析。这不是一个应该被悄悄修复的 bug——这是一个按对话共享的句柄模型的已知限制,而tests/integration/test_e2e_manual.md中的第 7 步正是为了证明 不同 的情况(新对话)确实被正确阻止。速率限制未连接到任何请求路径。 模块
src/authsvc/ratelimit.py(内存滑动窗口,类Janela)存在且有自己的测试,但auth-service和gateway的任何路由都没有实例化或调用它——它没有被“接入”。实际上,这意味着设计规范第 20 节(安全测试)和第 14 节(防范提示注入,第 4 项)中描述的“句柄暴力破解”和“系统性仓库扫描”缓解措施 在生产环境中尚不存在,尽管基础代码已就绪。这是一个真正的缺口,本项目中的任何其他控制措施都未覆盖——policies/policy.yaml中有一个带示例值的rate_limits部分(level_1: { calls: 60, window_s: 300 }),但当前gateway_main.py或src/stepup/middleware.py中的任何内容都不会读取这些值来实际限制调用。在将此网关暴露给真实流量(而不仅仅是单个可信用户)之前,将ratelimit.Janela连接到 L1 路径(理想情况下也连接到auth-service上的 challenge/confirmation 尝试)应被视为优先事项,而不是锦上添花。其余结构性限制(没有进程监督、
BACKEND_BEARER_TOKEN秘密在不同调用者之间无作用域共享、公开暴露需要自己的隧道)与mcp-oauth-gateway相同,本项目继承了其 OAuth/允许列表层——详情请参阅该项目的 README。
测试
# Windows
.venv\Scripts\pytest.exe -v
# Linux/macOS
.venv/bin/pytest -v覆盖范围:授权策略(src/stepup/policy.py)、step-up 中间件(级别、AUTH_REQUIRED/CONFIRMATION_REQUIRED)、auth-service(WebAuthn、会话、challenges、确认、审计日志、HMAC 摘要),以及 docker-compose.yml 的配置解析(包括缺少 --env-file .env 的两种错误模式)。
针对真实 MCP 客户端和物理通行密钥的端到端流程 不 在此套件中——请参阅 tests/integration/test_e2e_manual.md。
This server cannot be deployed
Maintenance
Related MCP Connectors
- gatewayOAuthai.sealgate
MCP gateway with runtime security policy, tool-call-level control, and audit of agent actions.
- JustOnceOAuthai.justonce
Persistent memory for AI assistants — one shared, OAuth-secured vault for every MCP client.
Zero-setup MCP gateway securely connecting AI to your tools with authentication and workflows
Remote MCP for Copilot CLI switch gate MCP, structured receipts, audit logs, and reviewer-ready evid
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA deny-by-default MCP server for Obsidian vaults where operators declare exact capabilities (list, read, create, etc.) scoped by path globs; everything not permitted is impossible by construction as disallowed tools are never registered.MIT
- FlicenseDqualityDmaintenanceEnables Claude Desktop to securely search and retrieve knowledge from an Obsidian vault through a stateless MCP interface, with progressive disclosure and gated write capabilities.13-
- AlicenseAqualityCmaintenanceSecure MCP server that bridges AI clients like Claude Desktop to Obsidian vaults, enabling read/write operations with OWASP Top 10 security controls and audit logging.948 npmMIT
- FlicenseNot gradedqualityBmaintenanceRemote MCP server for Obsidian vault access, giving Claude read/search/archive access to markdown notes via OAuth 2.1 + PKCE auth.2-