チュートリアル

MCP Server に OAuth 2.1 認証を付けるには?Authorization Server から Access Token までの Remote MCP API 実装

Stateless MCP が直すのはセッションであり、誰がツールを呼べるかではない。Remote MCP を公網に出したなら、認証は Authorization Server の発見から Access Token まで通す。JSON-RPC に共有鍵を埋めてはいけない。

MCP 2026-07-28 は HTTP 上の認可を任意と書く。資源を守るなら OAuth 2.1 のサブセットで実装する。Cursor、Claude Desktop、OpenAI Responses のリモート MCP、自前 Agent Host は、まず token なしで Remote MCP を叩く。Server は 401 を返し、WWW-Authenticate で Protected Resource Metadata を示す。クライアントは Authorization Server を発見し、登録し、認可コード + PKCE に resource を付け、Access Token を得て、毎ホップの HTTP に Authorization: Bearer を載せる。本稿はその Remote MCP API 経路の実装である。役割、metadata の形、audience の検証、scope 不足時の step-up。原文は MCP Authorization。既刊の MCP とは、Stateless MCP、inputSchema と Tool Calling は繰り返さない。「誰が呼んでよいか」だけを足す。STDIO でこのブラウザ遷移を回してはならない。資格情報は環境変数から取る。

三者の役割:MCP Server は Token を発行しない

最初の誤りは、MCP Server を token の発行者だと思うことだ。仕様では、保護された MCP Server は OAuth 2.1 のResource Server である。Access Token を受け取り、検証し、audience に縛って消費する。MCP Client は OAuth 2.1 クライアントであり、資源所有者の代わりに token を取り、それを付けてツールを呼ぶ。Authorization Server(AS)がログイン、同意、発行を担う。AS は Resource Server と同機でも、既存 IdP(Okta、Keycloak、Auth0、自前 OIDC)でもよい。仕様は AS の作り方を定めない。MCP Server が位置をどう示すかだけを定める。Authorization Server Discovery を見よ。

認可は MCP 実装にとって任意である。HTTP で資源を守るならこの仕様に従うべきだ。STDIO は従ってはならない。tools/call の params に共有 API キーを入れたり、JSON-RPC の _meta に token を入れたりするのは、2026-07-28 の Remote MCP 認証ではない。認証は HTTP 輸送層にあり、メソッド封筒の中にはない。

他稿との層:MCP は発見と呼び出しのプロトコル。ステートレス化が外したのは Session であり認可ではない。inputSchema は arguments の形を縛る。Schema が通っても呼び出しが許可されたことにはならない。OAuth が答えるのは「この Bearer は本資源向けか、scope は足りるか」だ。Agent ループ自体は AI Agent とは。

役割 OAuth 2.1 上の位置 出荷するもの
MCP Client / HostOAuth Client発見、PKCE、token 保管、毎ホップ Bearer
MCP ServerResource Server401 + PRM、token 検証、audience 束縛
Authorization Server発行者 / IdPログイン、同意、認可コード、Access Token

Authorization Server の発見:401 と RFC 9728

実装は失敗した呼び出しから始まる。Client は https://mcp.example.com/mcp に tools/list か任意の JSON-RPC を、Authorization なしで送る。Server は HTTP 401 を返し、WWW-Authenticate に Bearer と resource_metadata を付ける。scope も付け、最小権限を示す。クライアントはこのヘッダを解析できなければならない。resource_metadata があればそれを使い、なければ RFC 9728 の well-known を探る。

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp",
  resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
  scope="invoices:read"

MCP Server は OAuth 2.0 Protected Resource Metadata(RFC 9728)を実装しなければならない。文書の authorization_servers には少なくとも一つの AS issuer を書く。resource(本 Server の正規 URI)、scopes_supported、bearer_methods_supported も書いてよい。クライアントは続けて AS metadata を取る。パスなし issuer は /.well-known/oauth-authorization-server、次に OpenID の openid-configuration。tenant パスがある場合は仕様の優先順で挿入する。返ってきた文書の issuer は、URL を組み立てた issuer と文字列として完全一致しなければならない。違えば攻撃とみなし、文書を捨てる。

{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": [
    "https://auth.example.com"
  ],
  "bearer_methods_supported": ["header"],
  "scopes_supported": [
    "invoices:read",
    "invoices:write"
  ],
  "resource_documentation": "https://mcp.example.com/docs"
}

上が 401 と、最小の Protected Resource Metadata である。resource に fragment を付けてはならず、scheme を省略してもならない。末尾スラッシュは全サイトで揃える。意味がなければ付けない方がよい。AS を複数書いたら、それぞれ独立の発行者である。クライアントは登録状態を分けて持つ。

クライアント登録:CIMD、事前登録、DCR

AS を見つけたら、認可の前に client_id が要る。三つの仕組みを仕様の優先順で:① Client ID Metadata Documents(CIMD)——client_id 自体が HTTPS URL で、AS がその JSON を取り redirect_uris を検証する;② コンソールでの事前登録(機密または公開);③ Dynamic Client Registration(RFC 7591)の POST /register。DCR は非推奨で、CIMD を知らない AS 向けの互換として残る。新規は CIMD か事前登録を選べ。

authorization_servers が複数なら、それぞれ独立の AS である。client_id、秘密、token は AS ごとに分ける。A の資格情報を B の token 端点に送ってはならない。mix-up / confused deputy のよくある入口だ。

公開クライアント(デスクトップ Host、ブラウザ拡張)は OAuth 2.1 の PKCE が必須である。機密クライアントも付けるべきだ。「client_secret を MCP Server の環境変数に置き、AS へ登録を中継する」を発明してはならない。MCP Server は Resource Server であり、Client ではない。

認可コード + PKCE + resource:Access Token を取る

登録のあとが認可コード流である。ブラウザを開く前に Client は必ず:PKCE の code_verifier / code_challenge を作り;認可要求と token 要求の両方に resource(MCP Server の正規 URI、RFC 8707)を付け;scope を選ぶ(401 の scope を優先、なければ scopes_supported);検証済み AS の issuer を code_verifier と同じ要求記録に残す。同意後、コールバックの iss は RFC 9207 に従い記録値と単純な文字列比較をする。大文字小文字の畳み込みや末尾スラッシュ削除はしない。

Token 要求は code、code_verifier、resource を運ぶ。成功応答は JSON で、access_token、token_type は Bearer、expires_in、あれば refresh_token と scope。refresh token があると思い込むな。MCP Server の scopes_supported と 401 の scope に offline_access を入れてはならない。それは Client が AS に求めるものであり、資源要件ではない。AS metadata の要点は次の通り。authorization_response_iss_parameter_supported が true なら、iss のないコールバックは拒否する。

{
  "issuer": "https://auth.example.com",
  "authorization_endpoint": "https://auth.example.com/oauth/authorize",
  "token_endpoint": "https://auth.example.com/oauth/token",
  "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
  "registration_endpoint": "https://auth.example.com/oauth/register",
  "code_challenge_methods_supported": ["S256"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "response_types_supported": ["code"],
  "token_endpoint_auth_methods_supported": [
    "none",
    "client_secret_basic"
  ],
  "authorization_response_iss_parameter_supported": true,
  "scopes_supported": [
    "invoices:read",
    "invoices:write",
    "offline_access"
  ]
}

Access Token は Client にとって不透明なことが多い。障害切り分けでは JWT であることが多い。まず iss、aud(または resource)、scope、exp、client_id を見る。aud は本 MCP Server に縛られねばならない。支払い API 向け token で tools/call を走らせてはならない。下の payload は正しい aud を持つ。別 API の URI に変えたら Resource Server は 401 しなければならない。

{
  "iss": "https://auth.example.com",
  "sub": "user_1842",
  "aud": "https://mcp.example.com/mcp",
  "client_id": "https://host.example.com/oauth/client.json",
  "scope": "invoices:read",
  "exp": 1790000000,
  "iat": 1789996400
}

Bearer で Remote MCP API を呼び、audience を検証する

token を得たら、Client → MCP Server の毎 HTTP ホップに Authorization: Bearer を付ける。発見、tools/list、tools/call、resources/read も同じ。Token を query に置いてはならない。JSON-RPC のメソッド名と arguments は従来どおり body にある。認証ヘッダと封筒は二層である。Stateless MCP に「一度ログインすれば以降は検査しない」Session はない。各要求が自分を認証する。Stateless MCP を見よ。

POST /mcp HTTP/1.1
Host: mcp.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
Accept: application/json, text/event-stream

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "searchInvoices",
    "arguments": {
      "startDate": "2026-01-01",
      "endDate": "2026-01-31",
      "status": "paid"
    }
  }
}

Server は OAuth 2.1 §5.2 で token を検証する。署名または introspection、exp、iss、RFC 8707 の audience。失敗はすべて 401。audience のあと、本 AS が発行したかも見る。Server は他者向け token を受け取っても中継してもならない。Client も他資源の token をこの MCP Server に送ってはならない。confused deputy の硬い制約である。

認可が通っても arguments が合法だとは限らない。Bearer が答えるのは「誰が、どの資源に、どの scope で」だけだ。searchInvoices の日付と列挙は、なお parse + Schema + 業務規則が要る。「token が有効」を tools/call の唯一の門にしてはならない。形の契約は MCP と JSON Schema。

Scope、403、JSONVue での結合

token は有効だが scope が足りないときは、Server は 403 を返し、WWW-Authenticate に error="insufficient_scope"、今回の操作に必要な scope、同じ resource_metadata を付ける。今回必要な scope は一つの挑戦に揃える。一つずつ欠かして同意を二度求めない。Client の step-up は「旧 scope ∪ 挑戦 scope」とし、他ツールが既に得た権限を失わない。

HTTP 意味 次にすること
401未認証、または token 無効 / 期限切れ / audience 不正PRM を読み、再認可または token 更新
403 + insufficient_scopetoken は有効、権限不足step-up:scope を合併して再認可
400認可要求が壊れている先に Client の要求を直す。業務を再試行しない
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
  scope="invoices:write",
  resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
  error_description="Write scope required to create a credit note"

結合では少なくとも四つの JSON を扱う。Protected Resource Metadata、AS metadata、token 応答、(JWT なら)payload。加えて tools/call の params.arguments。役割が違うので、一つの「汎用検証」ファイルに混ぜるな。失敗標本を一つずつ残す。authorization_servers のない PRM、issuer が食い違う AS metadata、aud を誤った JWT、欠欄の arguments。

ブラウザで足りる:JSON フォーマットで metadata が parse できるか見る。JWT デコードで aud / scope / exp を見る。JSON Schema 検証で PRM 用の契約を当てる。JSON Diffで更新前後の payload、またはモデル arguments と MCP params を比べる。データは本機を出ない。

続き:MCP とは、Stateless MCP、MCP と JSON Schema、A2A vs MCP、AI Agent とは。

よくある質問 FAQ

ローカル STDIO の MCP にも OAuth 2.1 が要るか?

不要だ。仕様は STDIO でこのブラウザ認可を回すなと書く。資格情報は環境変数から取る。OAuth 2.1 は HTTP の Remote MCP 向けだ。同じツールが STDIO と Streamable HTTP を同時に出してよい。401、PRM、Bearer が要るのは HTTP 面だけだ。

Access Token を JSON-RPC の params や _meta に入れてよいか?

仕様準拠としてはいけない。Access Token は HTTP の Authorization ヘッダに置き、query に出してはならない。封筒の _meta はプロトコル版などであり、ログインではない。body に書けばログ、プロキシ、モデル文脈に乗る。

自分で JWT を出し、Server が共有鍵で験せば MCP OAuth を実装したことになるか?

署名検証だけでは足りない。Client が見つけるのは Authorization Server であり、「Server に焼き付けた鍵」ではない。RFC 9728 の metadata、authorization_servers、認可と token 要求の resource、audience 束縛、401 / 403 の挑戦がなお要る。自前 AS が JWT を出してよい。発見と resource パラメータは省略できない。

API Gateway が既に JWT を験している。もう一度要るか?

ゲートウェイは署名と期限を代行できる。MCP Host はなお AS を発見し、resource を付け、WWW-Authenticate を読まねばならない。audience を縛らず「任意の正当な JWT」だけを験すと、他 API 向け token が入る。MCP を普通の REST として守るだけでは、Host が握手できないことが多い。

まとめと次の一歩

Remote MCP の OAuth 2.1 は「ログインページを足す」ことではない。401 発見、AS metadata、クライアント登録、PKCE、resource、Access Token、毎ホップ Bearer という輸送層の契約である。MCP Server は Resource Server。token を出すのは Authorization Server だ。

出荷順:まず 401 + Protected Resource Metadata を既存 MCP Client が解析できるようにし、次に AS を繋ぎ、最後に tools/call の業務を書く。metadata と JWT 標本は JSONVue に残し、Schema 検証とは分ける。プロトコル入口は MCP とは。ステートレス輸送は Stateless MCP。