Skip to main content
Glama
joaorura

mcp-stepup-gateway

by joaorura

mcp-stepup-gateway

要求に応じてpasskey(WebAuthn)を必要とするMCPゲートウェイ --「step-up auth」-- リモートクライアント(Claude.ai、Custom Connector経由)がenquire-mcpで保護されたObsidian vaultを読み書きする前に。Googleログインとallowlist(mcp-oauth-gatewayと同様)は誰が接続できるかを決定します。このプロジェクトは、ツールごとに、その人が再認証なしで何をできるか、そして何が新しいpasskeyタッチを必要とするかを決定します。

具体的なケースから生まれました。mcp-oauth-gateway/enquire-mcp-gatewayは「誰が接続するかを認証する」(OAuth + allowlist)をすでに解決しています。欠けていたのは第二の層でした。allowlistの中であっても、すべてのツール呼び出しが同等に自由であるべきではありません。ノートを読むことは安価ですが、プロンプトインジェクションの可能性があるLLMを介してvaultのコンテンツを削除・書き換えることはそうではありません。このゲートウェイは、enquire-mcp自体に触れることなく、その区別を追加します。

なぜこれが存在するのか

OAuthで認証されたリモートMCPクライアントは、vaultの観点からすると依然として「完全なアクセス権を持つLLM」です。これは2つの軸で問題です。

  1. LLMは操作される可能性があります。 ノートやツールの応答内の悪意のあるコンテンツが、エージェントに物事を削除または上書きするよう指示しようとする可能性があります。プロンプトインジェクションは仮説ではありません。

  2. 「一度認証された」ことは「永遠に許可された」ことを意味すべきではありません。 長期のOAuthセッションは、人間の存在の新しい証明なしに、同じLLMに無制限の書き込み権限を無期限に与えるべきではありません。

ここでの解決策は、ツールごとのリスクレベルのモデルであり、読み取りを許可する短命(15分)のケイパビリティハンドル、および書き込みや削除を許可する呼び出しごとのpasskey確認を備えています。これは、サーバーが受け取った実際の引数からレンダリングされ、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で認証される内部ルート)と話すだけで、「このハンドルはこのツールを承認しますか?」「この確認はこれらの引数を正確に承認しましたか?」と尋ねます。人間はチャットに何も入力したり貼り付けたりしません。passkeyの全儀式は、auth-serviceが提供するURLでブラウザ内で行われます。

リスクレベル

レベル

要件

例

L0

なし -- 常に許可

vault_auth_unlock, vault_auth_check, vault_auth_status

L1

有効なセッションハンドル(絶対TTL 15分、アイドル5分)

obsidian_search, obsidian_read_note, obsidian_list_notes

L2

呼び出しごとのpasskey確認、args_digest(HMAC-SHA256)を介して正確な引数に紐付け

obsidian_create_note, obsidian_append_to_note, obsidian_archive_note

policies/policy.yamlは、バックエンドの各ツールをレベルにマッピングします。デフォルト拒否: 明示的にマッピングされていないツールは、最も制限の厳しいレベル(default_level: 2)に該当します。enquire-mcpがアップデートで新しいツールを取得した場合(バックエンドはnpx -yで実行されるため、起動のたびにバージョンが変わる可能性があります)、そのツールは「オープン」ではなく保護された状態で届きます。使用されているツール名の由来と、本番前にライブで検証する必要があるものについては、policies/policy.yaml自体のコメントを参照してください。

セットアップ

DockerとDocker Composeが必要です。3つのサービス(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クライアントを作成する手順を参照してください。

  • WebAuthn (WEBAUTHN_RP_ID, WEBAUTHN_RP_NAME, PUBLIC_ORIGIN, GATEWAY_KEY, DIGEST_KEY) -- WEBAUTHN_RP_IDを設定する前に、以下の注意事項を参照してください。GATEWAY_KEYとDIGEST_KEYはopenssl rand -hex 32で生成してください。

  • Backend (BACKEND_BEARER_TOKEN, OBSIDIAN_VAULT_PATH) -- gatewayとbackendの間で共有されるトークン、および保護するObsidian vaultのホスト上のパス。

WEBAUTHN_RP_IDは永続的です。 これは、登録された各passkeyのWebAuthn署名自体に埋め込まれるドメイン(ポートなし、プロトコルなし)です。最初の登録後にこの値を変更すると、すべてのpasskeyが無効になります -- 全員が新しいブートストラップで再登録する必要があります。最初のpasskeyを登録する前に、最終的なドメイン(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ファイル自身のディレクトリ(docker/)を基準に${VAR}を解決し、リポジトリのルートでは解決しません。このフラグなしで実行すると、OBSIDIAN_VAULT_PATHは実際のvaultではなく静かなフォールバック(docker/vault、空)に落ち、目に見えるエラーは発生しません。詳細はdocker/docker-compose.ymlの上部にあるUso:コメントを参照してください(Task 17のレビューで判明)。

3. 最初のpasskeyを登録する(ブートストラップ)

auth-serviceのログで次を探してください:

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

passkey対応デバイス(携帯電話、または互換性のあるパスワードマネージャー)のブラウザで<PUBLIC_BASE_URL>/register?t=<token>を開き、登録を完了してください。トークンは10分で期限切れになります。期限を過ぎた場合は、auth-serviceを再起動(docker compose restart auth-service)して新しいトークンを生成してください -- これにより、保留中のセッション/チャレンジもリセットされます(デフォルトでSESSION_PURGE_ON_START=true)。

ブートストラップトークンがまだ有効なうちに、少なくとも2つのpasskeyを登録してください(例:携帯電話 + パスワードマネージャー)。これは「デバイスを失った」場合に対するこのプロジェクトの緩和策です。回復コードはありません(意図的な決定です。設計仕様の未決定事項のセクションを参照)。

4. Custom Connectorとして接続する

claude.ai -> Settings -> Connectors -> Add custom connector で、<PUBLIC_BASE_URL>/mcpを貼り付けます。OAuth Clientのフィールドは空のままにします(動的登録)。ALLOWED_EMAILSに含まれるGoogleアカウントでログインした後、完全な検証手順(unlock、読み取り、確認付き書き込み、2つの会話のテスト)はtests/integration/test_e2e_manual.mdにあります。

既知の制限事項

  • A8 -- 同じ会話を15分のウィンドウ内で開いた人Bはハンドルを引き継ぎます。 これはハンドルモデルの実際の穴であり、すでに文書化され、設計上受け入れられています。セッションハンドル(L1)は、その時点で会話を読んでいる人のIDに結び付けられておらず、ハンドルが生まれた会話にのみ結び付けられています。Claudeアカウントが共有されており、人Bが人Aがロックを解除した同じ会話を開いた場合(新しい会話ではなく)、絶対TTLの15分(またはアイドル5分)以内であれば、BはAが取得した読み取り能力(L1)を引き継ぎます。短いTTL、アイドルタイムアウト、およびクライアントが安定してMcp-Session-Idを提供する場合の追加のバインドによって軽減されますが、排除されてはいません。書き込み(L2)は、呼び出しごとに新しいpasskey署名が必要なため、どのような場合でもBには到達できません。完全な脅威分析については、設計仕様のセクションA8(docs/superpowers/specs/2026-08-16-mcp-stepup-auth-proxy-design.md)を参照してください。これは静かに修正されるべきバグではありません -- 会話ごとに共有されるハンドルモデルの既知の制限であり、tests/integration/test_e2e_manual.mdの手順のステップ7は、異なるケース(新しい会話)が正しくブロックされていることを証明するために存在しています。

  • レート制限はどのリクエストパスにも接続されていません。 src/authsvc/ratelimit.pyモジュール(インメモリのスライディングウィンドウ、クラスJanela)は存在し、独自のテストがありますが、auth-serviceのどのルートもgatewayもそれをインスタンス化または呼び出していません -- 配線されていません。実際には、設計仕様のセクション20(セキュリティテスト)とセクション14(プロンプトインジェクション対策、項目4)で説明されている「ハンドルのブルートフォース」と「vaultの体系的なスキャン」の緩和策は、基本コードが準備できているにもかかわらず、まだ本番には存在しません。これは実際のギャップであり、このプロジェクトの他のどの管理策でもカバーされていません。policies/policy.yamlにはrate_limitsセクションにサンプル値(level_1: { calls: 60, window_s: 300 })がありますが、現在のgateway_main.pyやsrc/stepup/middleware.pyには、呼び出しを実際に制限するためにこれらの値を読み取るものはありません。このゲートウェイを実際のボリュームのある使用(信頼できる単一ユーザーだけでなく)に公開する前に、ratelimit.JanelaをL1パスに接続し(理想的にはauth-serviceでのチャレンジ/確認の試行にも)、これは後回しではなく優先事項として扱うべきです。

  • その他の構造的な制限(プロセス監視なし、BACKEND_BEARER_TOKENシークレットが呼び出し元ごとにスコープされずに共有、外部公開には独自のトンネルが必要)は、このプロジェクトがOAuth/allowlist層を継承しているmcp-oauth-gatewayと同じです。詳細はそのプロジェクトのREADMEを参照してください。

テスト

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

対象範囲:認可ポリシー(src/stepup/policy.py)、ステップアップミドルウェア(レベル、AUTH_REQUIRED/CONFIRMATION_REQUIRED)、auth-service(WebAuthn、セッション、チャレンジ、確認、監査ログ、HMACダイジェスト)、およびdocker-compose.ymlの設定解決(--env-file .envがない場合の2つのエラーモードを含む)。

実際のMCPクライアントと物理的なpasskeyに対するエンドツーエンドの手順は、このスイートには含まれていません -- tests/integration/test_e2e_manual.mdを参照してください。

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    A 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
  • F
    license
    D
    quality
    D
    maintenance
    Enables 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
    -
  • F
    license
    Not graded
    quality
    B
    maintenance
    Remote MCP server for Obsidian vault access, giving Claude read/search/archive access to markdown notes via OAuth 2.1 + PKCE auth.
    2
    -