本文へスキップ
Hironobu Iga

Strands Agents の MCP クライアントが処理する OAuth の範囲

v1.53.0 の MCPClient は、URL とクライアント資格情報だけで、OAuth(client_credentials)のディスカバリからトークンの取得、同一インスタンス内での再利用までを処理します。一方で、要求するスコープはクライアントの指定ではなく、サーバーが広告した集合の全部に決まります。ローカルに認可サーバーごと立てて検証しました。

公開日

はじめに

Strands Agents の Python SDK v1.53.0(2026 年 8 月 21 日)で、MCPClient が OAuth で保護された MCP サーバーへ組み込みで接続できるようになりました。 transport を自作して渡せば以前から接続はできましたが、トークンの取得から付与までを SDK が処理するのはこのバージョンからです。 渡すのは URL とクライアント資格情報だけで、スコープの指定は任意です。

from strands.tools.mcp import MCPClient
from strands.tools.mcp.mcp_types import MCPClientCredentials

client = MCPClient(
    url="http://127.0.0.1:9000/mcp",
    auth=MCPClientCredentials(
        client_id="lab-client",
        client_secret="...",
        scopes=["mcp:tools"],
    ),
)

トークンエンドポイントの URL は書いていません。 それでつながるなら、書かなかった情報はどこから来るのでしょうか。 認可サーバー(以下 AS)と MCP サーバーを両方ローカルに立てて、HTTP のやり取りを確認しました。 検証コードはリポジトリに置いています。 使ったバージョンは strands-agents 1.53.0 と、その依存の MCP SDK 1.29.0 で、以下の挙動はこのバージョンのものです。

この auth が対応するのは client_credentials グラント(ユーザーを介さず、クライアント自身にあらかじめ与えられた権限でアクセストークンを得る、M2M 向けの認可グラント)だけで、トランスポートは streamable HTTP だけです。 それ以外のフローは、auth_provider 引数に任意の httpx.Auth を渡す形で、利用者が実装します。

書かなかった情報の出どころ

接続すると、クライアントは次の順で HTTP リクエストを送っていました。 httpx のログに、401 応答の WWW-Authenticate ヘッダの値だけを 1 行補っています。

POST http://127.0.0.1:9000/mcp                                    "401 Unauthorized"
     WWW-Authenticate: Bearer error="invalid_token",
       resource_metadata="http://127.0.0.1:9000/.well-known/oauth-protected-resource/mcp"
GET  http://127.0.0.1:9000/.well-known/oauth-protected-resource/mcp  "200 OK"
GET  http://127.0.0.1:9001/.well-known/oauth-authorization-server    "200 OK"
POST http://127.0.0.1:9001/token                                   "200 OK"
POST http://127.0.0.1:9000/mcp                                    "200 OK"

401 の WWW-Authenticate にある resource_metadata が指す先が、保護リソースメタデータ(RFC 9728。リソースサーバーが自分の認可情報を公開する文書)です。 その authorization_servers から AS を、AS のメタデータ(RFC 8414)からトークンエンドポイントを解決します1。 この順序は、MCP の認可仕様が定めるディスカバリ手順に沿っています。

AS のログには、トークン要求がこのように残っていました。

POST /token grant_type=client_credentials auth=client_secret_basic
     scope='mcp:tools' resource='http://127.0.0.1:9000/mcp'

今回の構成では、AS への各トークン要求に resource パラメータ(RFC 8707)が付くので、AS はトークンの audience をこの MCP サーバー向けに絞れます。 別サーバーへの使い回しを実際に拒むのは、トークンを受け取った MCP サーバーによる audience の検証です。 MCP 仕様は、その検証をリソースサーバーの必須事項にしています。 クライアント認証は client_secret_basic 固定でした。

要求するスコープはどこで決まるのか

一般的な OAuth では、要求するスコープを決めるのはクライアントです。 AS やリソースサーバーのメタデータにある scopes_supported は選べるスコープの一覧で、クライアントはその中から必要な分だけを要求し、一覧に無いスコープを要求すれば AS がそれを拒否するか、発行するスコープを狭めます。 この前提で読むと、冒頭の例で MCPClientCredentials に渡した scopes=["mcp:tools"] は、その「必要な分」の指定です。

MCP の認可仕様は、この前提を採っていません。 仕様の Scope Selection Strategy は、401 応答の WWW-Authenticate に scope があればそれを、無ければ保護リソースメタデータの scopes_supported に列挙されたスコープをすべて要求する、と定めています。 scopes_supported は選択肢の一覧ではなく、基本機能に必要な最小の集合と位置づけられ、追加のスコープは 403 を受けてから段階的に要求する設計です。

ログでクライアントが要求していた scope='mcp:tools' も、この手順で決まった値です。 検証に使った MCP SDK のサーバー実装(FastMCP)は、設定の required_scopes を保護リソースメタデータの scopes_supported として公開します。 今回の構成では 401 応答にスコープのヒントが無いので、サーバーが要求するスコープが、そのままクライアントの要求になりました。

では、MCPClientCredentials の scopes に必要な分だけを書いたら、どうなるのでしょうか。 docstring には「サーバーがスコープを広告している場合はそちらが優先され、この値は無視される」とあります。 それなら、少なくともサーバーが広告していなければ使われるはずです。

確認すると、どちらの場合も、クライアントの指定はトークン要求に現れませんでした。

  • サーバーが mcp:tools と mcp:admin を広告し、クライアントが scopes=["mcp:tools"] だけを指定した場合:トークン要求は scope='mcp:tools mcp:admin'(広告された全部)
  • サーバーが何も広告していない場合:同じ指定をしても、トークン要求に scope パラメータ自体が付かない

MCP SDK 1.29.0 は、要求スコープを「401 の WWW-Authenticate の値、保護リソースメタデータの scopes_supported、AS メタデータの scopes_supported、どれも無ければ未指定」の順に選び、その結果で内部の要求スコープを上書きします。 仕様との違いは、AS メタデータを候補に含めている点だけです。 クライアントの設定値は仕様にも実装にも候補として入っていないため、MCPClientCredentials の scopes に何を書いても、トークン要求には残りません。 残る 2 つの処理は別の場所にあります。 AS は自身の認可ポリシーに従ったスコープでトークンを発行し、MCP サーバーはトークンのスコープを required_scopes と照合して、不足していれば 403 で拒否します。

この照合は、クライアントが何を要求したかとは独立に行われます。 必要スコープを欠くトークンで呼ぶと、実際に 403 が返りました。 ただし、返ってきた WWW-Authenticate には、仕様が 403 に含めることを推奨する機械可読な scope パラメータがありません。 必要スコープは、人間向けの error_description に書かれるだけです。 403 の WWW-Authenticate が error="insufficient_scope" を含み、scope を含まない場合、MCP SDK 1.29.0 のクライアントは保護リソースメタデータから要求スコープを選び直します。 今回のように初回と同じ scopes_supported が選ばれる構成では同じ要求を繰り返すだけなので、AS の発行範囲が変わらない限り再試行も 403 のままです。

トークンの再利用と、失敗したときの見え方

トークンはインメモリに保存され、MCPClient インスタンスに紐付きます。 1 つのインスタンスでセッションを 2 回開き直し、計 3 回ツールを呼んでも、AS へのトークン要求は 1 回だけでした。 これは有効期限内で確認した結果で、期限が切れれば、実装上は同じインスタンスでも取り直します。 一方、インスタンスを作り直すとインメモリの状態を引き継げません。 呼び出しのたびに MCPClient を生成する書き方だと、期限が残っていても呼び出しごとにトークン要求が発生します。

client_secret を間違えると、受け取る例外は MCPClientInitializationError で、そのメッセージは the client initialization failed: unhandled errors in a TaskGroup (1 sub-exception) です。 原因はここからは読み取れません。 この例外に直接連鎖しているのは TaskGroup の ExceptionGroup で、AS が返した invalid_client は、その中に含まれる OAuthTokenError のメッセージに残っています。 例外のメッセージだけをログに書く作りだと原因が記録されないため、logger.exception のように、例外の連鎖を含むトレースバックごと記録することになります。

おわりに

現行の MCP 仕様に沿った構成で、この auth をそのまま使うための条件は、SDK が読める形で 2 種のメタデータを公開していること、client_credentials と client_secret_basic を受け付けること、広告された集合をそのまま初期スコープとして要求してよいこと、インメモリのトークン保存で足りることです。 加えて、この auth で得るトークンはクライアント自身の権限を表すもので、ユーザーの権限を含みません。 ユーザーごとの認可が必要な接続では、auth_provider に自前の実装を渡すことになります。 それ以外で上の条件に収まる接続なら、書くのは冒頭の数行だけで済みます。

Footnotes

  1. AS を自作して試す場合の注意として、MCP SDK 1.29.0 のクライアントは、RFC 8414 が省略を認めている authorization_endpoint を AS メタデータの必須項目にしています。client_credentials にしか対応しない AS でも、この検査を満たすためのダミーのエンドポイントが必要でした。 ↩