AIエージェントとシステムをつなぐMCP入門(認証/認可編)
Back to Topはじめに
#本ページは「AIエージェントとシステムをつなぐMCP入門」の続編です。
今回は、認証/認可について説明します。
MCP仕様では認可機能は任意扱いですが、MCPを実運用する場合は必須機能と言えます。
本記事では、以下の内容を扱います。
- Streamable HTTPにおけるMCPの認証・認可の流れ
- PRMと認可サーバーの発見
- Keycloakとトークンイントロスペクションを使ったMCPサーバー側の検証
本記事で掲載しているコードはこちらで公開しています。
連載:AIエージェントとシステムをつなぐMCP入門
今回使用するライブラリなど
#- npm@12.0.2
- node@26.6.0
- @modelcontextprotocol/express@2.0.0
- @modelcontextprotocol/node@2.0.0
- @modelcontextprotocol/server@2.0.0
- typescript@7.0.2
- zod@4.4.3
- Keycloak 26.3.4
- Docker compose
@modelcontextprotocolのv2がリリースされたので、本記事からはv2を使用します。
併せてNodeおよびTypeScriptも最新のものを使用します。
MCPモジュールについて
v1では@modelcontextprotocol/sdkを使用していました。
v2でモジュールが分割されたため、@modelcontextprotocol/server, express, nodeを使用します。
MCP仕様:認証/認可
#トランスポート別の認証アプローチ
#トランスポート(通信方式)により、認証の実装指針が異なります。
StreamableHTTP
認可は任意(OPTIONAL)となっていますが、実装する場合はMCPの認可仕様に準拠しなければなりません。
stdio(標準入出力)
MCP仕様で定義されている認証仕様の対象外です。
認証情報(APIキーなど)はサブプロセス起動時に実行環境から直接取得して利用する形になります。
その他
そのプロトコルで確立されたベストプラクティスに準拠します。
準拠規格
#セキュリティと相互運用性を確保しつつ簡素化を図るため、以下の標準規格のサブセットが採用されています。
- OAuth 2.1 (IETF Draft)
- OAuth 2.0 Bearer Token Usage (RFC 6750)
- OAuth 2.0 Authorization Server Metadata (RFC 8414)
- OAuth 2.0 Dynamic Client Registration Protocol (RFC 7591)
- Resource Indicators for OAuth 2.0 (RFC 8707)
- OAuth 2.0 Protected Resource Metadata (RFC 9728)
- OAuth Client ID Metadata Documents (CIMD)
ロール
#以下のロールが定義されています。
- MCPサーバー(OAuth 2.1リソースサーバー): アクセストークンを受け入れ、保護されたリソースへのリクエストに応答する。
- MCPクライアント(OAuth 2.1クライアント): リソース所有者に代わって保護されたリソースへリクエストを行う。
- 認可サーバー: ユーザーと対話し、アクセストークンを発行する。MCPサーバーと同一でも、独立した形で構成してもよい。
認証フロー
#MCPの公式ドキュメントに掲載されている「Authorization Flow Steps」を一部和訳して転載。
MCP仕様では、クライアントがPRMと認可サーバーメタデータを取得し、PKCEを利用した認可コードフロー(Authorization Code Flow)でトークンを取得します。
今回の検証コードでは主にリソースサーバーとしての動作検証を目的とし、クライアント側のブラウザ認証フローは省略しています。
- Keycloakで事前に取得したアクセストークンを使い、MCPサーバーがBearerトークンを受け取る。
- Keycloakの
Introspection endpointでトークンの有効性、Audience、Scopeを検証する。
sequenceDiagram
autonumber
participant B as User-Agent (Browser)
participant C as MCPクライアント
participant M as MCPサーバー
participant A as 認可サーバー
Note over C: 認可サーバーの発見
C->>M: MCPリクエスト(トークンなし)
M->>C: 401 Unauthorized(WWW-Authenticate header)
Note over C: リソースメタデータの発見
C->>M: PRM(Protected Resource Metadata)リクエスト
M->>C: PRM
C->>A: 認可サーバーメタデータ取得
A-->>C: 認可サーバーメタデータ
Note over C: クライアント登録
alt Client ID Metadata Documents
A->>C: Fetch metadata from client_id URL
C-->>A: JSON metadata document
else Dynamic client registration
C->>A: POST /register
A->>C: Client Credentials
else Pre-registered client
Note over C: Use existing client_id
end
C->>B: Open browser with authorization URL + code_challenge + resource
B->>A: Authorization request with resource parameter
A->>B: Redirect to callback with authorization code + iss
B->>C: Authorization code callback
C->>A: Token request + code_verifier + resource
A->>C: Access token (+ refresh token)
C->>M: MCPへリクエスト(トークンあり)
M-->>C: MCPからレスポンス認可サーバーの発見
#MCPクライアントは、以下の手順で認可サーバーの場所と機能を特定する。
-
リソースメタデータの発見(RFC 9728)
クライアントは
401 Unauthorizedレスポンスを受け取った際、resource_metadata(リソースメタデータ)を取得しなければなりません。優先順位
WWW-Authenticateヘッダーのresource_metadataパラメーターに示されたURL- MCPサーバーエンドポイント配下のWell-Known URI(e.g.
/.well-known/oauth-protected-resource/path/to/mcp) - ドメインルートのWell-Known URI(e.g.
/.well-known/oauth-protected-resource)
-
認可サーバーメタデータの取得(RFC 8414)
クライアントは、リソースメタデータから得られたIssuer(発行者)URLの形式に応じて認可サーバーのエンドポイントを特定します。
優先順位
- パスコンポーネントがある場合(例:/tenant1)
- OAuth 2.0方式(e.g.
/.well-known/oauth-authorization-server/tenant1) - OIDC方式パス挿入(e.g.
/.well-known/openid-configuration/tenant1) - OIDC方式パス付与(e.g.
tenant1/.well-known/openid-configuration)
- OAuth 2.0方式(e.g.
- パスコンポーネントがない場合
- OAuth 2.0方式(e.g.
/.well-known/oauth-authorization-server) - OIDC方式(e.g.
/.well-known/openid-configuration)
- OAuth 2.0方式(e.g.
厳格な検証ルール
- メタデータドキュメント内のissuer値は、Well-Known URLの構築に使用した発行者識別子と完全に一致しなければならない。
- 不一致の場合は、クライアントはそのメタデータを使用不可とする。
- パスコンポーネントがある場合(例:/tenant1)
検証コードでは、「パスコンポーネントあり」の「OAuth 2.0方式」を使用しています。
クライアント登録
#認証フローを開始する前に、クライアントはクライアントIDを取得する必要があります。
登録メカニズムと優先順位
- 事前登録(Pre-registration): クライアントとサーバー間に既存の関係がある場合、静的な認証情報を使用する。
- Client ID Metadata Documents (CIMD): クライアントとサーバー間に事前関係がない場合、HTTPS URLをクライアントIDとして使用する。
- 動的クライアント登録 (Dynamic Client Registration): CIMD未対応サーバーとの後方互換性のために維持されていますが非推奨なので、基本的に選択しない。
- ユーザー入力: 他の手段がない場合、ユーザーに情報を入力させる。
検証コードでは、Keycloakに事前登録した静的な認証情報を使用しています。(Pre-registrationに相当)
検証コードの解説
#検証コード仕様
#下記のような検証コードを使って説明します。
Keycloakに依存する処理は、keycloak.tsに集約する形で構成しています。
| 項目 | 内容 | 備考 |
|---|---|---|
| トランスポート | Streamable HTTP(Stateless) | |
| 認可サーバー | Keycloak | 8081ポートで公開 |
| MCPエンドポイント | POST /mcp |
Bearer認証必須 |
| トークン検証 | トークンイントロスペクション(KeycloakのIntrospection endpoint) |
|
| 認証エラー | 401 Unauthorized + WWW-Authenticate |
トークン無効または期待するAudienceと不一致の場合にスローします |
| 認可エラー | 403 Forbidden + WWW-Authenticate |
トークンは有効でも、スコープが不足している場合にスローします |
| PRM | GET /.well-known/oauth-protected-resource/mcp |
- 今回の検証ではやらないこと
- ブラウザの認可画面
- PKCEパラメーターの生成
- 認可コードを受け取るcallback
- 認可コードからトークンへの交換
- CIMD
- Dynamic Client Registration
Keycloakの設定
#Docker composeで起動する際、同梱しているRealm設定を使って自動生成しています。
これにより、後述する動作検証に必要なRealmはすべて設定された状態でKeycloakが起動されます。
- realm:
mcp-demo - Client Scope:
mcp:tools: aud=http://localhost:3000/mcpmcp:no-scope: aud=http://localhost:3000/mcpmcp:diff-audience: aud=http://localhost:3000/mcp-diff
- Client
- Introspection用:
mcp-server - 正常系トークン取得用:
mcp-demo-client(mcp:toolsを付与) - Scope不足検証用:
mcp-demo-no-scope-client(mcp:no-scopeを付与) - Audienceなし検証用:
mcp-demo-no-audience-client - Audience不一致検証用:
mcp-demo-diff-audience-client(mcp:diff-audienceを付与)
- Introspection用:
- Audience: どのリソースサーバー向けのトークンか
- Scope: リソースサーバーで何を実行できるか
PRMの公開
#MCPクライアントが認可サーバーを発見するためのPRMの公開。公開はmcpAuthMetadataRouterが担います。
クライアントが認証なしでMCPエンドポイントへアクセスした場合、WWW-AuthenticateヘッダーにもPRMのURLが含まれます。クライアントはそのURLからPRMを取得し、認可サーバーの場所を知ることができます。
app.use(
mcpAuthMetadataRouter({
oauthMetadata,
resourceServerUrl: mcpServerUrl,
scopesSupported: [REQUIRED_SCOPE],
resourceName: "MCP Auth Streamable HTTP",
}),
);
PRM例
{
"resource": "http://localhost:3000/mcp",
"authorization_servers": ["http://localhost:8081/realms/mcp-demo"],
"scopes_supported": ["mcp:tools"],
"resource_name": "MCP Auth Streamable HTTP"
}
resource: 保護対象であるMCPサーバーの識別子authorization_servers: 認可サーバーのIssuer URLscopes_supported: MCPサーバーが利用するScopeの候補resource_name: リソースの表示名
Bearer認証ミドルウェア
#MCPエンドポイントの前段でBearer認証しています。
requireBearerAuthは、MCPエンドポイント開始前に実行されるミドルウェアです。
authMiddlewareでエラーが検出された場合は、そこで処理を中断するため、以降の処理は実行されません。
const authMiddleware = requireBearerAuth({
// ※1
verifier: tokenVerifier,
// ※2
requiredScopes: [REQUIRED_SCOPE],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
});
//omit
app.post(MCP_PATH, authMiddleware, async (req, res) => {
const server = createServer(); // authMiddlewareでエラーが発生した場合、ここは実行されません
// omit
- ※1: トークン検証(詳細な検証内容は後述します)
- ※2: スコープ検証
- トークンが指定されたスコープを持っているか検証します。
- トークンが持つスコープと、requiredScopesに設定したスコープを
requireBearerAuthが比較します。
Keycloak Introspectionによるトークン検証
#MCPサーバーが受け取ったアクセストークンは、KeycloakのIntrospection endpointで検証しています。
ここで重要なのは、HTTP 200だけではトークンが有効だと判断できない点です。
export function createTokenVerifier(mcpServerUrl: URL): OAuthTokenVerifier {
return {
verifyAccessToken: async (token) => {
// Keycloakのイントロスペクションでトークン検証 ※1
const params = new URLSearchParams({token, client_id: OAUTH_CLIENT_ID});
if (OAUTH_CLIENT_SECRET) params.set("client_secret", OAUTH_CLIENT_SECRET);
const response = await fetch(KEYCLOAK_INTROSPECTION_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params.toString(),
});
// 200 以外を返した場合 ※2
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new OAuthError(OAuthErrorCode.InvalidToken, `Invalid or expired token: ${text}`);
}
// omit
// トークンが無効または期限切れの場合 ※3
if (!data.active) throw new OAuthError(OAuthErrorCode.InvalidToken, "Inactive token");
- ※1
- token: 検証対象のトークン
OAUTH_CLIENT_ID,OAUTH_CLIENT_SECRET: MCPクライアントの情報ではなく、MCPサーバーがKeycloakにIntrospectionを依頼するための認証情報
- ※2, 3
- 200以外が返された場合は、無効または期限切れとみなして
InvalidTokenを返します。 - 200が返されても
activeがfalseの場合、トークンが無効なため、同様にInvalidTokenを返します。
- 200以外が返された場合は、無効または期限切れとみなして
Audienceの検証
#トークンに設定されているaudは「トークンが、どのリソースサーバー向けに発行されたか」を表します。
トークンが有効でも、別のリソース向けに発行されたトークンを受け入れないようにAudienceを検証しています。
// Audience (aud) クレームの検証
if (OAUTH_STRICT) { // OAUTH_STRICTを有効にしている場合、検証を実行します
// ※1
if (!data.aud) throw new OAuthError(OAuthErrorCode.InvalidToken, "Resource indicator (aud) missing");
const audiences = Array.isArray(data.aud) ? data.aud : [data.aud];
const allowed = audiences.some((audience) =>
checkResourceAllowed({ requestedResource: audience, configuredResource: mcpServerUrl }),
);
// ※2
if (!allowed) throw new OAuthError(OAuthErrorCode.InvalidToken, `Expected audience compatible with ${mcpServerUrl}, got: ${audiences.join(",")}`);
}
- ※1
- audが存在しない場合は、リソースインジケーターが欠落している旨を返します
- ※2
- audが許可されているものと一致しない場合は、期待するAudienceと一致しない旨を返します
検証コードを使った動作検証
#- 基本フロー(認可フローの流れを検証します)
- Authorizationヘッダー未設定でリクエスト(MCP Inspector)
- Authorizationヘッダー未設定でリクエスト(curl)
- PRM(Protected Resource Metadata)リクエスト
- 有効なトークンを使って正常アクセス
- 例外フロー
- Authorizationヘッダーに形式違いの値を設定してリクエスト
- Authorizationヘッダーに無効なトークンを設定してリクエスト
- スコープ不足のクライアントでリクエスト
- Audience不足のクライアントでリクエスト
- 異なるAudienceが設定されているクライアントでリクエスト
1. Authorizationヘッダー未設定でリクエスト(MCP Inspector)
#Authorizationヘッダー未設定の状態で、MCP InspectorからMCPサーバーへ接続します。
UI上のポップアップメッセージ
OAuth Authorization Failed
Policy 'Allowed Client Scopes' rejected request to client-registration service. Details: Not Permitted to use specified clientScope
コンソールに出力されるログ
Error from MCP server: StreamableHTTPError: Streamable HTTP error: Error POSTing to endpoint: {"error":"invalid_token","error_description":"Missing Authorization header"}
at StreamableHTTPClientTransport.send (file:///xxx/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js:364:23)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5) {
code: 401
}
レスポンスヘッダーが確認できなかったので、curlで再検証。
2. Authorizationヘッダー未設定でリクエスト(curlで再検証)
#先ほどと同じくAuthorizationヘッダー未設定の状態で、curlを使ってtools/listを呼び出します。
curl -i -X POST http://localhost:3000/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="Missing Authorization header", scope="mcp:tools", resource_metadata="http://localhost:3000/.well-known/oauth-protected-resource/mcp"
結果
- HTTPステータスとして
401 Unauthorizedが返されました。 errorには、「トークン無効」と示されています。error_descriptionには、「Authorizationヘッダーが見つからない」と示されています。resource_metadataには、PRMエンドポイントのURLが設定されています。
3. PRM(Protected Resource Metadata)の確認
#resource_metadataに設定されていたURLにアクセスし、認証に必要なメタデータを確認します。
curl -s http://localhost:3000/.well-known/oauth-protected-resource/mcp
{
"resource":"http://localhost:3000/mcp",
"authorization_servers":["http://localhost:8081/realms/mcp-demo"],
"scopes_supported":["mcp:tools"],
"resource_name":"MCP Auth Streamable HTTP"
}
MCP Inspectorの場合
結果
PRMが返されました。
- resource: 保護対象リソースを識別するURL
- authorization_servers: 認可サーバーのIssuer URL
- scopes_supported: 利用可能な認可スコープ
4. 有効なトークンを使って正常アクセス
#認証して有効なトークンを取得してから、tools/listを呼び出します。
- トークン取得
Keycloakからアクセストークンを取得します。
curl -s -X POST http://localhost:8081/realms/mcp-demo/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=mcp-demo-client" \
-d "client_secret=mcp-demo-client-secret"
{
"access_token":"<valid_token>",
"expires_in":300,
"refresh_expires_in":0,
"token_type":"Bearer",
"not-before-policy":0,
"scope":"mcp:tools"
}
- MCPサーバーへアクセス
Keycloakから取得したaccess_tokenを使って、MCPへアクセスします。
curl -s -X POST http://localhost:3000/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer <valid_token>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
event: message
data: {"result":{"tools":[{"name":"sum_numbers","title":"sum_numbers","description":"Sum two numbers","inputSchema":{"type":"object","$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"a":{"type":"number","description":"first number"},"b":{"type":"number","description":"second number"}},"required":["a","b"]},"outputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"result":{"type":"number","description":"sum result"}},"required":["result"],"additionalProperties":false}},{"name":"get_server_policy","title":"get_server_policy","description":"Return simple authorization policy for demo","inputSchema":{"type":"object","properties":{}},"outputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"resource":{"type":"string","description":"resource server url"},"requiredScope":{"type":"string","description":"required scope"}},"required":["resource","requiredScope"],"additionalProperties":false}}]},"jsonrpc":"2.0","id":1}
結果
正常にアクセスでき、tools/listの結果が確認できました。
このレスポンスには2026-07-28 RCで対応したJSONスキーマ(2020-12)の$schemaも含まれています。
RCの変更内容については、MCP 2026-07-28 RC解説で説明しています。
MCP Inspectorで接続した場合
5.1. Authorizationヘッダーに形式違いの値を設定してリクエスト
#Authorizationヘッダーに「形式不正のトークン(Bearer から始まらない値)」を設定してリクエストしてきたケースを想定した例外処理の確認。
curl -i -X POST http://localhost:3000/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-H "Authorization: bad_format_token" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="Invalid Authorization header format, expected 'Bearer TOKEN'", scope="mcp:tools", resource_metadata="http://localhost:3000/.well-known/oauth-protected-resource/mcp"
{
"error":"invalid_token",
"error_description":"Invalid Authorization header format, expected 'Bearer TOKEN'"
}
結果
- HTTPステータスとして
401 Unauthorizedが返されました。 error_descriptionには、「無効な形式である」と示されています。
5.2. Authorizationヘッダーに無効なトークンを設定してリクエスト
#Authorizationヘッダーに形式は正しいが「存在しないトークン」を指定してリクエストしてきたケースを想定した例外処理の確認。
curl -i -X POST http://localhost:3000/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer unknown_token" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="Inactive token", scope="mcp:tools", resource_metadata="http://localhost:3000/.well-known/oauth-protected-resource/mcp"
{
"error":"invalid_token",
"error_description":"Inactive token"
}
結果
- HTTPステータスとして
401 Unauthorizedが返されました。 error_descriptionには、「トークンが無効である」と示されています。
5.3. スコープ不足のクライアントでリクエスト
#「権限不足のトークン(必要なクライアントスコープが設定されていないクライアントで認証)」を使ってリクエストしていたケースを想定した例外処理の確認。
- トークン取得
curl -s -X POST http://localhost:8081/realms/mcp-demo/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=mcp-demo-no-scope-client" \
-d "client_secret=mcp-demo-no-scope-client-secret"
{"access_token":"<valid_token>","expires_in":300,"refresh_expires_in":0,"token_type":"Bearer","not-before-policy":0,"scope":"mcp:no-scope"}
- MCPサーバーへアクセス
curl -i -X POST http://localhost:3000/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer <valid_token>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", error_description="Insufficient scope", scope="mcp:tools", resource_metadata="http://localhost:3000/.well-known/oauth-protected-resource/mcp"
{"error":"insufficient_scope","error_description":"Insufficient scope"}
結果
- HTTPステータスとして
403 Forbiddenが返されました。 errorには、「スコープ不足」と示されています。error_descriptionには、「スコープ不足」と示されています。
5.4. Audience設定なしのクライアントでリクエスト
#「期待するAudienceが設定されてないトークン(必要なAudienceが設定されていないクライアントで認証)」を使ってリクエストしてきたケースを想定した例外処理の確認。
- トークン取得
curl -s -X POST http://localhost:8081/realms/mcp-demo/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=mcp-demo-no-audience-client" \
-d "client_secret=mcp-demo-no-audience-client-secret"
{"access_token":"<valid_token>","expires_in":300,"refresh_expires_in":0,"token_type":"Bearer","not-before-policy":0,"scope":""}
- MCPサーバーへアクセス
curl -i -X POST http://localhost:3000/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer <valid_token>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="Resource indicator (aud) missing", scope="mcp:tools", resource_metadata="http://localhost:3000/.well-known/oauth-protected-resource/mcp"
{"error":"invalid_token","error_description":"Resource indicator (aud) missing"}
結果
- HTTPステータスとして
401 Unauthorizedが返されました。 error_descriptionには、「リソースインジケーターが見つからない」旨が示されています。
5.5. 異なるAudienceが設定されているクライアントでリクエスト
#「別リソース向けのトークン(期待されるAudienceとは異なる値を設定したクライアントで認証)」を使ってリクエストしてきたケースを想定した例外処理の確認。
- トークン取得
curl -s -X POST http://localhost:8081/realms/mcp-demo/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=mcp-demo-diff-audience-client" \
-d "client_secret=mcp-demo-diff-audience-client-secret"
{"access_token":"<valid_token>","expires_in":300,"refresh_expires_in":0,"token_type":"Bearer","not-before-policy":0,"scope":"mcp:diff-audience"}
- MCPサーバーへアクセス
curl -i -X POST http://localhost:3000/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer <valid_token>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="Expected audience compatible with http://localhost:3000/mcp, got: http://localhost:3000/mcp-diff", scope="mcp:tools", resource_metadata="http://localhost:3000/.well-known/oauth-protected-resource/mcp"
{"error":"invalid_token","error_description":"Expected audience compatible with http://localhost:3000/mcp, got: http://localhost:3000/mcp-diff"}
結果
- HTTPステータスとして
401 Unauthorizedが返されました。 error_descriptionには、「期待しているAudienceが異なる」旨が示されています。
まとめ
#Streamable HTTPでは、MCP仕様に基づく認証・認可の仕組みを利用できます。
MCPクライアントは、401レスポンスのWWW-AuthenticateヘッダーやPRMから認可サーバーを発見し、アクセストークンを取得します。
MCPサーバーは、受け取ったBearerトークンを検証し、保護対象リソースへのアクセスを許可または拒否します。
今回の検証コードでは、Keycloakとトークンイントロスペクションを使い、MCPサーバー側の認証・認可処理を確認しました。
Authorizationヘッダーの未設定や形式不正、無効なトークン、Audienceの不備を401として拒否し、Scope不足を403として拒否できることも確認しました。
実際にMCPサーバーを公開する際は、利用者やクライアントごとに必要なScopeを定義し、保護するリソースに応じたAudienceを検証することが重要です。


