チュートリアル
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 / Host | OAuth Client | 発見、PKCE、token 保管、毎ホップ Bearer |
| MCP Server | Resource Server | 401 + 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_scope | token は有効、権限不足 | 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。