docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
8 changed files with 89737 additions and 54 deletions
119
i18n/ja/pages/client/caching.md
Normal file
119
i18n/ja/pages/client/caching.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
---
|
||||
translation:
|
||||
sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800]
|
||||
tool: 1
|
||||
---
|
||||
# キャッシュヒント {#caching-hints}
|
||||
|
||||
2026-07-28 プロトコルでは、サーバーが `tools/list`、`prompts/list`、`resources/list`、`resources/templates/list`、`resources/read`、`server/discover` に対して返す結果はすべて、2 つのフィールドを持ちます。`ttlMs` はクライアントがその結果を新鮮なものとして扱ってよいミリ秒数、`cacheScope` はキャッシュした結果をユーザー間で共有してよいか(`"public"`)、それとも 1 つの認可コンテキストに属するか(`"private"`)を表します。
|
||||
|
||||
サーバーは何もキャッシュしません。これらのフィールドは「宣言」です。つまり「このツール一覧は全員にとって同じで、1 分間は変わりません」という意思表示です。それを受けてクライアント(または手前にあるゲートウェイ)はラウンドトリップを省略できます。ヒントに従うかどうかはクライアントの判断で、ヒントを出すのがサーバーの仕事です。そしてその仕事は SDK が肩代わりします。
|
||||
|
||||
デフォルトでは、どの結果も `ttlMs: 0, cacheScope: "private"` を返します。すぐに古くなり、決して共有されないという意味です。これは常に安全で、常に仕様に準拠しています。一覧が本当に安定していて、すべての呼び出し側に対して同一なら、構築時にそう伝えてください。
|
||||
|
||||
```python title="server.py" hl_lines="5-8"
|
||||
--8<-- "docs_src/caching/tutorial001.py"
|
||||
```
|
||||
|
||||
* このマップのキーは**メソッド名**で、キャッシュ可能な 6 つのメソッドだけが有効なキーです。パラメーターの型は `Mapping[CacheableMethod, CacheHint]` なので、エディターがキーを補完し、実行前にタイプミスを指摘します。型チェッカーをすり抜けたものは構築時に例外を送出します。
|
||||
* 記載しなかったメソッドはデフォルトのままです。このマップは上書きの集合であって、一覧表ではありません。
|
||||
* `CacheHint(ttl_ms=5_000)` は `scope` を設定していないので、`"private"` のままです。呼び出し側ごとに 5 秒間新鮮、という意味です。スコープと TTL は独立した判断です。
|
||||
* `"server/discover"` も有効なキーです。ディスカバリーの結果も一覧と同じくキャッシュ可能だからです。
|
||||
|
||||
!!! warning
|
||||
`cacheScope: "public"` は、キャッシュしたレスポンスを「誰にでも」返してよいという意味です。共有ゲートウェイは、リクエストが認証されていたとしても、あるユーザーの結果を平気で別のユーザーに渡します。結果を `"public"` とするのは、すべての呼び出し側に対して同一である場合だけにしてください。また `cacheScope` をアクセス制御として使わないでください。これはラベルであって、鍵ではありません。
|
||||
|
||||
## ハンドラーごとの上書き {#per-handler-override}
|
||||
|
||||
低レベルの `Server` では、ハンドラーが結果を手作業で組み立てます。`ttl_ms` と `cache_scope` は結果モデルの単なるフィールドです。これらを明示的に設定したハンドラーは、フィールド単位で常にコンストラクターのマップより優先されます。
|
||||
|
||||
```python title="server.py" hl_lines="10 16"
|
||||
--8<-- "docs_src/caching/tutorial002.py"
|
||||
```
|
||||
|
||||
ハンドラーは `ttl_ms=1_000` と指定し、スコープについては何も指定していません。実際に送受信される内容は `ttlMs: 1000`(マップの `60_000` ではなくハンドラーの値)と `cacheScope: "public"`(ハンドラーが未設定なのでマップの値)です。明示的な値は設定値に勝ち、設定値はデフォルトに勝ちます。これはフィールドごとに成り立つので、ハンドラーは片方のフィールドを固定し、もう片方をサーバー全体のポリシーに任せられます。
|
||||
|
||||
これはコンストラクターが知り得ない動的な事情への逃げ道にもなります。`resources/read` をユーザーごとにフィルターするハンドラーは、それ以外は公開であるサーバーでも、特定の URI については `cache_scope="private"` を返せます。
|
||||
|
||||
ページ分割された一覧について 1 つ注意点があります。プロトコルは、1 つの一覧の**すべてのページで同じ `cacheScope`** を要求します。コンストラクターのマップはページではなくメソッドをキーにしているため、この条件を構造上満たします。しかしスコープを自分で上書きするハンドラーは、その一貫性に自ら責任を持ちます。カーソルがあるときだけではなく「すべての」ページで上書きしてください。そうしないと 1 ページ目と 2 ページ目で食い違います。
|
||||
|
||||
## クライアントから見えるもの {#what-the-client-sees}
|
||||
|
||||
2026-07-28 のセッションでは、`Client` がヒントに自動で従います。組み込みのレスポンスキャッシュがあり、デフォルトで有効です。`ttlMs` を持って届いた結果は保存され、その TTL 内に同一の呼び出しがあれば、ラウンドトリップなしでキャッシュから返されます。ヒントを「持たない」結果はキャッシュされません。ヒントのない結果には `CacheConfig.default_ttl_ms` が適用され、そのデフォルトは `0`(すぐに古くなる)です。そのため、何も宣言しないサーバーには、これまでとまったく同じ呼び出しごとのトラフィックが届きます。
|
||||
|
||||
```python title="client.py" hl_lines="33 35 38"
|
||||
--8<-- "docs_src/caching/tutorial003.py"
|
||||
```
|
||||
|
||||
呼び出し 4 回、取得 3 回です。2 回目の呼び出しは新鮮なエントリーを見つけ、サーバーに到達しませんでした。(注入した)クロックを TTL の先へ進めたことで、3 回目は再び取得しました。4 回目は `cache_mode="refresh"` を指定しています。このキーワード引数はキャッシュ対象の 5 つのメソッド(`list_tools`、`list_prompts`、`list_resources`、`list_resource_templates`、`read_resource`)にあります。
|
||||
|
||||
* `"use"`(デフォルト)は、新鮮なエントリーがあればそれを返し、なければ取得して保存します。
|
||||
* `"refresh"` はキャッシュから返しません。取得して結果を保存し、キャッシュにあったものを置き換えます。
|
||||
* `"bypass"` はキャッシュに一切触れずにラウンドトリップします。読み込みも書き込みもしません。
|
||||
|
||||
`"use"` より上位にルールが 1 つあります。**`meta` を持つ呼び出しは必ずサーバーに到達します。**`meta` を設定したリクエスト(進捗トークンやトレーシング用フィールドなど)は実際のリクエスト送信を前提にしているため、`cache_mode="use"` では `"refresh"` として扱われます。キャッシュの読み込みは省略され、取得した結果は引き続きキャッシュのエントリーを置き換えます。`"bypass"` と明示的な `"refresh"` はいつもどおりに動作します。
|
||||
|
||||
キャッシュを完全に無効にするには、`Client(server, cache=None)` で構築してください。すべての呼び出しが再びラウンドトリップになり、`cache_mode` は受け付けられるものの何もしません。
|
||||
|
||||
スコープも自動的に尊重されます。`"private"` のエントリーはキャッシュの「パーティション」(後述)をキーにし、`"public"` のエントリーはより広い共有を選べます。そして、名指しされたエントリーについては**通知が TTL に勝ちます**。`list_changed` 通知は対応するキャッシュ済みの一覧を破棄し、`resources/updated` はその URI と完全に一致するキーで保存されたキャッシュ済みの読み込み結果を、どれだけ新鮮でも破棄します。2026-07-28 の接続では、これらの通知は `client.listen(...)` で開く `subscriptions/listen` ストリームに届き、破棄はウォッチャーがイベントを見る前に完了します。詳しくは **[サブスクリプション](subscriptions.md)** を参照してください。
|
||||
|
||||
`resources/updated` について 1 つ注意点があります。破棄は URI の完全一致のみです。ストアの契約には列挙やスキャンの操作がありません(TypeScript のリファレンス実装と同じです)。そのため「サブ」リソースの URI を持つ通知は、その親のキャッシュ済み読み込み結果を破棄しません。サーバーがサブリソースをこの方法で通知する場合は、`cache_mode="refresh"` で親を再取得してください。
|
||||
|
||||
### 設定方法:`CacheConfig` {#configuring-it-cacheconfig}
|
||||
|
||||
```python
|
||||
from mcp.client import CacheConfig
|
||||
|
||||
client = Client("https://api.example.com/mcp", cache=CacheConfig(default_ttl_ms=5_000))
|
||||
```
|
||||
|
||||
* `store`:エントリーの保存先です。デフォルトはクライアントごとの新しいインメモリストアです。クライアントやプロセスをまたいでキャッシュを共有するには、独自の `ResponseCacheStore` 実装(たとえば Redis ベース)を渡してください。契約の型(`ResponseCacheStore`、`CacheKey`、`CacheEntry`、そしてデフォルトの `InMemoryResponseCacheStore`)は `mcp.client` からインポートできます。1 回の検索で、ストアの `get` が最大 2 回(private 側、次に public 側)順に発行されることがあるので、リモートストアのレイテンシー要件はそれに合わせて見積もってください。カスタムストアには明示的な `partition` が**必須**です。
|
||||
* `partition`:認可コンテキストのラベルです。共有ストア内で、あるプリンシパルの `"private"` エントリーが別のプリンシパルに返されないようにします。
|
||||
* `target_id`:明示的なサーバーの識別子です。カスタムトランスポートやインプロセスサーバー用です(後述)。
|
||||
* `default_ttl_ms`:`ttlMs` ヒントを持たない結果に適用する TTL です。デフォルトの `0` では、ヒントのない結果はキャッシュされません。
|
||||
* `share_public`:サーバーが `"public"` と主張したエントリーをパーティションをまたいで返します(後述)。デフォルトでは無効です。
|
||||
* `clock`:エポック秒単位の時刻ソースです。上の例のように注入すれば、有効期限のテストでスリープする必要がありません。
|
||||
|
||||
!!! warning "パーティション = 検証済みプリンシパル"
|
||||
`partition` は、検証済みトークンの subject など、**検証済みの資格情報**から導出してください。リクエストで渡されたデータから導出してはいけませんし、サーバーの URL からも導出してはいけません(サーバーの識別子は別のキー軸です)。SDK はライブラリであり、独自の認証を持ちません。信頼の起点は `CacheConfig` を構築する主体、つまりテナントではなくデプロイメントです。マルチテナントのゲートウェイは、認証済みプリンシパルごとに 1 つの `CacheConfig` を作ります。
|
||||
|
||||
パーティションは `Client` の寿命の間、固定でもあります。接続の認可コンテキストがセッション途中で変わった場合(たとえば別のプリンシパルとして再認証した場合)、キャッシュは追従しません。新しいプリンシパルには新しい `Client` を構築してください。
|
||||
|
||||
キャッシュキーには**サーバーの識別子**も含まれます。接続先として指定した URL 文字列から `user:pass@` のユーザー情報を取り除いたもので、それ以外はバイト単位で完全一致です。大文字小文字の畳み込みも、クエリの並べ替えも、末尾スラッシュの整理もしません。正規化が足りない場合は共有の機会を失うだけですが、正規化しすぎると 2 つのテナント(`?tenant=a` と `?tenant=b`)を統合しかねません。そのため、見かけ上異なる URL は単純にエントリーを共有しません。URL がない場合(インプロセスサーバーや `Transport` インスタンス)、クライアントには代わりにインスタンスごとのランダムな識別子が与えられます。サーバーに名前を付けるには `CacheConfig.target_id` を設定してください(カスタムストアでは必須で、構築時にそう指摘されます)。識別子はキー素材に入る前に sha256 でハッシュされるので、クエリ文字列に秘密情報を含む URL がストアのキーに現れることはありません。ハッシュ前の形を自分でログに出すこともしないでください。
|
||||
|
||||
!!! warning "`share_public` はサーバーをフリート全体で信頼する"
|
||||
デフォルトでは `"public"` のエントリーであっても、自身のパーティション内にとどまります。`share_public=True` にすると、サーバーが `cacheScope: "public"` と付けたエントリーが、そのストアを使う**すべての**パーティションに返されます。サーバーの分類を、全パーティションを代表して信頼することになります。サーバーが(バグであれ悪意であれ)テナント固有のデータに `"public"` を付けると、あるテナントのレスポンスが他のテナントに漏れます。このフラグは意図的にコンストラクターレベル専用です。呼び出しごとの `cache_mode` はキャッシュを狭められますが、呼び出しごとの指定で共有を広げることはできません。
|
||||
|
||||
### キャッシュが決してしないこと {#what-the-cache-never-does}
|
||||
|
||||
* **セッション層の呼び出しはキャッシュを経由しません。**`client.session.list_tools()` などは常にラウンドトリップします。キャッシュは `Client` のメソッド上にあります。
|
||||
* **`server/discover` は対象外です。**ディスカバリーの結果は接続時に一度だけ届き、`ttlMs` を持っていてもレスポンスキャッシュには入りません。再接続時のプローブを省略するために自分で永続化する場合([`prior_discover`](../protocol-versions.md#reconnecting-with-prior_discover))、その鮮度は自分で管理します。`DiscoverResult` はまさにその目的で、解析済みの `ttl_ms` と `cache_scope` を持っています。
|
||||
* **継続ページは決してキャッシュされません。**カーソルなしの呼び出しだけが対象です。期限切れのカーソルで拒否された継続ページは、キャッシュ済みの一覧を「破棄」します。一覧がその下で変わったからです。
|
||||
* **マルチラウンドトリップ(multi-round-trip)の読み込みは決してキャッシュされません。**`input_responses` / `request_state` を与えた `read_resource`、または入力ラウンドを経て解決されるものは、決してキャッシュに入りません(仕様上の MUST です)。
|
||||
* **通知による破棄には通知が必要です。**破棄の確実さはトランスポートの配信に依存します。現在、モダンなインプロセスの経路(デフォルトの `mode="auto"` での `Client(server)`)は単独の通知を配信しません。
|
||||
* **破棄は結果整合であり、即時ではありません。**通信経由の通知は生成されたタスクから配送されるため、通知の到着と競合した呼び出しには、破棄前のエントリーがもう一度返されることがあります。その時間幅は配送のレイテンシーで抑えられ、破棄自体は必ず行われます。
|
||||
* **stale-if-error はありません。**再取得が失敗したからといって期限切れのエントリーが返されることはありません。エラーはそのまま伝播します。
|
||||
* **早期の再取得はありません。**保存されたエントリーは TTL が切れるまで返され、その後の最初の呼び出しがラウンドトリップの費用を負担します。バックグラウンドでの更新はありません。
|
||||
* **集約はありません。**同時に行われた同一の呼び出し 2 つは、取得 2 回です。
|
||||
* **24 時間を超える TTL はありません。**サーバーが送ったものでも設定したものでも、それより大きな `ttlMs` は保存時に切り詰められます(`mcp.client.caching.MAX_TTL_MS`)。どれだけ寛大なヒントが付いていても、エントリーが返され続ける期間には上限があります。
|
||||
* **共有ストア**では、クライアント同士が競合します。各クライアントは、取得中に破棄が先行した場合は自身の書き込みを捨てますが、「同居する」クライアントは、自分が見ていない破棄によって削除されたエントリーを書き戻すことがあります。そしてこの競合の管理自体にも上限があり、追跡するキーが 4096 個を超えると最も古いキーのガードから外されます。どちらの時間幅も許容されており、上記の TTL 上限によって閉じられます。
|
||||
* **プロトコルの世代をまたいで返すことはありません。**エントリーはネゴシエートされたプロトコルバージョンにスコープされます。共有の永続ストア上で、あるセッションが別のネゴシエート済みバージョンで書かれたエントリーを返すことはありません(SDK は古いセッション向けに 2026 のフィールドを取り除くので、同じ一覧でも世代によって実際に異なります)。破棄も同様に現在の世代のエントリーだけに作用し、別の世代のエントリーは TTL によって自然に期限切れになります。
|
||||
|
||||
### ヒントを自分で読む {#reading-the-hints-yourself}
|
||||
|
||||
ヒントは、キャッシュ可能なすべての結果の単なるフィールドでもあります(解析済みの `result.ttl_ms` と `result.cache_scope`)。組み込みキャッシュの上に(またはその代わりに)独自の管理を重ねたい場合に使えます。
|
||||
|
||||
**古いサーバー**(2026 より前のプロトコル)に対しては、これらのフィールドは通信上に存在せず、モデルは保守的なデフォルトを示します。`ttl_ms == 0` と `cache_scope == "private"`、つまり古く、共有されない状態です。何も宣言しなかったサーバーに対する正しい前提です。キャッシュはレガシーセッションも同じように扱います。そこではヒントは一切参照されず(通信上にどんなキーが現れても)、`default_ttl_ms` だけが適用されます。そのデフォルトの `0` は何もキャッシュしないので、2026 より前の接続はキャッシュが存在する前とまったく同じように振る舞います。「サーバーが 0 と言った」と「サーバーが何も言わなかった」を区別する必要がある場合は、`"ttl_ms" in result.model_fields_set` を確認してください。フィールドが実際に届いたときだけ設定されます。
|
||||
|
||||
## 古いクライアント {#older-clients}
|
||||
|
||||
2026 より前のプロトコルバージョンのクライアントには、どちらのフィールドも見えません。SDK がそれらの接続ではシリアライズ時に取り除きます。ヒントは一度設定するだけで、バージョン固有に書くものは何もありません。
|
||||
|
||||
## まとめ {#recap}
|
||||
|
||||
* 6 つのメソッドが `ttlMs` / `cacheScope` を持ちます。SDK のデフォルトは `0` / `"private"` で、古く、共有されず、常に安全です。
|
||||
* 構築時の `cache_hints={method: CacheHint(...)}`(`MCPServer` と `Server` の両方)で、メソッドごとにサーバー全体の値を設定します。
|
||||
* 結果にフィールドを設定したハンドラーは、フィールド単位でマップを上書きします。
|
||||
* `"public"` は、結果がすべての呼び出し側に対して同一であるという約束です。アクセス制御ではありません。
|
||||
* `Client` はヒントに自動で従います。レスポンスキャッシュはデフォルトで有効で、再取得の代わりに新鮮なエントリーを返し、ヒントを提供しないサーバー(またはセッション)については何もキャッシュしません。
|
||||
* 呼び出しごとに、`cache_mode="refresh"` は再取得し、`"bypass"` はキャッシュを飛ばします。構築時の `cache=None` でキャッシュを完全に無効にできます。
|
||||
142
i18n/ja/pages/client/callbacks.md
Normal file
142
i18n/ja/pages/client/callbacks.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
---
|
||||
translation:
|
||||
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
|
||||
tool: 1
|
||||
---
|
||||
# クライアントのコールバック {#client-callbacks}
|
||||
|
||||
MCP のリクエストは、ほぼすべてが一方向です。クライアントからサーバーへ送られます。
|
||||
|
||||
サーバーのほうから**クライアント**に何かを頼むこともできます。ユーザーに質問する、ユーザーのモデルでサンプリングする、ユーザーのワークスペースフォルダーを一覧する、といったことです。こうしたリクエストには、`Client(...)` に**コールバック**を渡して応答します。
|
||||
|
||||
## 問い合わせをするサーバー {#a-server-that-asks}
|
||||
|
||||
次のサーバーのツールは、単独では処理を終えられません。
|
||||
|
||||
```python title="server.py" hl_lines="16"
|
||||
--8<-- "docs_src/client_callbacks/tutorial001.py"
|
||||
```
|
||||
|
||||
* `ctx.elicit(...)` は `elicitation/create` リクエストを**クライアントに**送り、待機します。
|
||||
* 誰か(フォームに入力する人か、こちらのコード)が `name` を渡すまで、ツールは戻りません。
|
||||
|
||||
これはサーバー側の話で、**[エリシテーション(elicitation)](../handlers/elicitation.md)** のページが扱います。このページは通信路の反対側の話です。
|
||||
|
||||
## エリシテーションのコールバック {#the-elicitation-callback}
|
||||
|
||||
```python title="client.py" hl_lines="6-10 16-17"
|
||||
--8<-- "docs_src/client_callbacks/tutorial002.py"
|
||||
```
|
||||
|
||||
* エリシテーションのコールバックは `async (context, params) -> ElicitResult` です。
|
||||
* `params.message` が質問です。`params.requested_schema` は、サーバーが求める答えの JSON Schema です。実際のクライアントはこれをもとにフォームを描画しますが、ここでは自動で埋めています。
|
||||
* 戻り値は `ElicitResult(action="accept", content={...})`、`action="decline"`、`action="cancel"` のいずれかです。それ以外の選択肢は `ErrorData(...)` だけで、これはリクエストを拒否し、呼び出し全体を失敗させます。
|
||||
* `context` は `ClientRequestContext` です。使用中の `session`、サーバーの `request_id`、サーバーが付けた `meta` を持ちます。
|
||||
|
||||
!!! tip
|
||||
`params` は 2 つのエリシテーションモードのユニオンです。ここでは `params.mode` は `"form"` です。`"url"` のリクエストはスキーマの代わりに `params.url` を持ちます。1 つのコールバックで両方を扱い、`params.mode` で分岐してください。パターンの全体は **[エリシテーション](../handlers/elicitation.md)** にあります。
|
||||
|
||||
### 試してみる {#try-it}
|
||||
|
||||
`issue_card` を呼び出し、両側の様子を見てみましょう。
|
||||
|
||||
コールバックは、サーバーからの質問をパース済みの状態で受け取ります。
|
||||
|
||||
```python
|
||||
params.mode # 'form'
|
||||
params.message # 'What name should go on the card?'
|
||||
params.requested_schema # {'properties': {'name': {'title': 'Name', 'type': 'string'}},
|
||||
# 'required': ['name'], 'title': 'CardHolder', 'type': 'object'}
|
||||
```
|
||||
|
||||
コールバックが答えると、ツールの中で `ctx.elicit(...)` が再開し、ツールが完了します。
|
||||
|
||||
```python
|
||||
result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.')]
|
||||
```
|
||||
|
||||
こちらから `tools/call` が 1 回、サーバーからの折り返しの `elicitation/create` が 1 回、それに答えるのがこちらの関数です。すべてが 1 回のツール呼び出しの中で完結します。
|
||||
|
||||
!!! info
|
||||
`Client(...)` の呼び出しにある `mode="legacy"` は、実際に働いています。デフォルトでは `Client(...)` は新しいプロトコルの経路をネゴシエートしますが、その経路にはサーバーからクライアントへのリクエストのためのバックチャネル(back-channel)がありません。コールバックが動く前に `ctx.elicit` が失敗します。これを決めるのはトランスポートではなく、ネゴシエートされたプロトコルです。インメモリでも URL 越しでも同じです。クライアントがこうしたリクエストに答える必要があるときは、必ず `mode="legacy"` を指定してください。このページの裏にあるテストはすべてそうしています。詳しくは **[プロトコルバージョン](../protocol-versions.md)** を参照してください。
|
||||
|
||||
2026-07-28 のセッションでもコールバックが使われなくなるわけではなく、呼ばれ方が変わります。ツールが `ElicitRequest` を含む `InputRequiredResult` を返すと、`Client` はそのエントリを同じ `elicitation_callback` に振り分け、呼び出しを再試行してくれます。この流れは **[マルチラウンドトリップ(multi-round-trip)リクエスト](../handlers/multi-round-trip.md)** で説明しています。
|
||||
|
||||
## コールバックはケイパビリティ {#a-callback-is-a-capability}
|
||||
|
||||
クライアントがエリシテーションのリクエストに答えられることを、サーバーに伝えた覚えはないはずです。伝えたのは SDK です。
|
||||
|
||||
クライアントは接続時に自分の `capabilities` を宣言します。サーバー側の宣言と鏡写しの関係です。このオブジェクトを自分で書くことはありません。**コールバックを登録すること自体が宣言です。**
|
||||
|
||||
| 渡すもの | クライアントが宣言するもの |
|
||||
| --- | --- |
|
||||
| `elicitation_callback=` | `"elicitation": {"form": {}, "url": {}}` |
|
||||
| `sampling_callback=` | `"sampling": {}` |
|
||||
| `list_roots_callback=` | `"roots": {"listChanged": true}` |
|
||||
| どれも渡さない | `{}` |
|
||||
|
||||
細かい指定が 1 つだけあります。サンプリングのサブケイパビリティです。サンプラーが `tools` / `tool_choice` パラメーターを扱える場合は、`sampling_callback` と一緒に `sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability())` を渡してください。サーバーは `sampling.tools` が宣言されているのを確認してからでないと、これらを送れません。
|
||||
|
||||
`logging_callback` と `message_handler` は表にありません。これらは通知を扱うもので、通知にケイパビリティは要りません。
|
||||
|
||||
サーバーは `ctx.session.check_client_capability(...)` で宣言を読み取ります。これを行うツールを追加します。
|
||||
|
||||
```python title="server.py" hl_lines="23-31"
|
||||
--8<-- "docs_src/client_callbacks/tutorial003.py"
|
||||
```
|
||||
|
||||
`elicitation_callback` だけを渡して接続し、呼び出します。
|
||||
|
||||
```python
|
||||
result.structured_content # {'result': ['elicitation']}
|
||||
```
|
||||
|
||||
3 つのコールバックをすべて渡すと結果は `['elicitation', 'sampling', 'roots']`、どれも渡さなければ `[]` です。
|
||||
|
||||
!!! check
|
||||
今度はわざと間違えてみましょう。`elicitation_callback` **なしで**接続し、それでも `issue_card` を呼び出します。
|
||||
|
||||
サーバーの `elicitation/create` リクエストはそれでもクライアントに届きます。そして、扱えると宣言していないので、SDK が代わりにエラーで答えます。そのエラーが呼び出し全体を失敗させます。`call_tool` は `is_error` の結果を返すのではなく、例外を送出します。
|
||||
|
||||
```text
|
||||
MCPError: Elicitation not supported
|
||||
```
|
||||
|
||||
これはツールのエラーではなくプロトコルエラー(`-32600`、*invalid request*)です。モデルが読んで再試行できるものは何もありません。`client_features` を用意する価値があるのはこのためです。行儀のよいサーバーは、頼む前に確認します。
|
||||
|
||||
## 非推奨の 2 つ {#the-deprecated-pair}
|
||||
|
||||
`sampling_callback` は `sampling/createMessage` に答えます。サーバーがクライアント側のモデルに何かを補完させるリクエストです。`list_roots_callback` は `roots/list` に答えます。サーバーが、作業してよいディレクトリを尋ねるリクエストです。
|
||||
|
||||
どちらも動作します。どちらも上のルールに従います。そしてどちらも、**2026-07-28 の仕様で削除される** RPC に応えるものです。新しいサーバーはリクエストの途中でクライアントを呼び返すことはせず、リクエストをツール結果の一部として返してきます(**[マルチラウンドトリップリクエスト](../handlers/multi-round-trip.md)**)。コールバック自体が使われなくなるわけではありません。`InputRequiredResult` が `CreateMessageRequest` や `ListRootsRequest` を含んでいると、`Client` の自動ループが、ここで登録したのと同じ `sampling_callback` または `list_roots_callback` にそれを振り分けます。一覧は **[非推奨の機能](../deprecated.md)** にあります。
|
||||
|
||||
まだ移行していないサーバーとやり取りするには、引き続きこれらのコールバックが必要です。シグネチャは次のとおりです。
|
||||
|
||||
```python title="client.py"
|
||||
--8<-- "docs_src/client_callbacks/tutorial004.py"
|
||||
```
|
||||
|
||||
* サンプリングのコールバックは `CreateMessageRequestParams` の全体(`messages`、`model_preferences`、`max_tokens`)を受け取り、`CreateMessageResult` を返します。モデルを動かすのはこちら側で、やり方は自由です。SDK はリクエストを運ぶだけです。
|
||||
* ルート(roots)のコールバックはパラメーターを一切取らず、`ListRootsResult` を返します。
|
||||
* どちらも、拒否するときは代わりに `ErrorData(...)` を返せます。
|
||||
|
||||
`elicitation_callback` とまったく同じように `Client(...)` に渡します。
|
||||
|
||||
## 通知のコールバック {#the-notification-callbacks}
|
||||
|
||||
あと 2 つあります。どちらも何も宣言しません。
|
||||
|
||||
`logging_callback` は、サーバーが送る `notifications/message` を `LoggingMessageNotificationParams`(`level`、`logger`、`data`)として受け取ります。プロトコルのロギング自体が 2026-07-28 の仕様で非推奨になっています(代わりにどうするかは **[ロギング](../handlers/logging.md)** にあります)。そのため、このコールバックはまだ通知を出すサーバーのために存在します。2026 年世代の接続では、コールバックだけでは何も届きません。2026 年のサーバーは、オプトインしたリクエストにしかログメッセージを送らないからです。`Client(...)` に `log_level="info"`(または別のレベル)を渡すと、すべてのリクエストにそのオプトインが付き、そのレベル以上を受け取れます。2026 年より前のサーバーはこれを無視し、従来どおり `logging/setLevel` の挙動を保ちます。
|
||||
|
||||
`message_handler` は何でも受け取る窓口です。セッションが表に出すサーバー通知はすべて(それぞれ専用のコールバックに加えて)ここに届きます。ストリームを使うトランスポートでは、トランスポートレベルの `Exception` もすべて届きます。届かないものが 2 つあります。`notifications/cancelled` は表に出されず SDK が適用します。動作中の `listen()` ストリームに対する購読の確認応答は、そのストリームが消費します。パラメーターには `IncomingMessage`(`ServerNotification | Exception`、`mcp.client` からエクスポート)で注釈を付けてください。覚えておく価値のあるパターンは `if isinstance(message, Exception): raise message` の 1 つです。これで、接続が壊れたときに黙って消えるのではなく、はっきり失敗します。
|
||||
|
||||
## まとめ {#recap}
|
||||
|
||||
* サーバーはクライアントにリクエストを送れます。`Client(...)` に渡したコールバックで応答します。
|
||||
* 現行のものはエリシテーションのコールバックです。`async (context, params) -> ElicitResult` で、フォームモードと URL モードの両方を 1 つの関数で扱います。
|
||||
* **コールバックの登録がケイパビリティの宣言です。** 登録がなければ、SDK が代わりにサーバーのリクエストを拒否し、呼び出し全体が `MCPError` で失敗します。
|
||||
* サーバーは、頼む前に `ctx.session.check_client_capability(...)` で確認します。
|
||||
* `sampling_callback` と `list_roots_callback` も同じように動きますが、非推奨の機能のためのものです。新しいサーバーは代わりにマルチラウンドトリップリクエストを使います。
|
||||
* `logging_callback` と `message_handler` は通知を受け取ります。何も宣言しません。
|
||||
|
||||
`Client(...)` の第 1 引数はトランスポートのオブジェクトです。すべての種類は **[クライアントのトランスポート](transports.md)** で扱っています。
|
||||
129
i18n/ja/pages/client/identity-assertion.md
Normal file
129
i18n/ja/pages/client/identity-assertion.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0]
|
||||
tool: 1
|
||||
---
|
||||
# アイデンティティアサーション {#identity-assertion}
|
||||
|
||||
通常の OAuth プロバイダー(**[OAuth クライアント](oauth-clients.md)**)は、まず MCP サーバーに「どの認可サーバーを信頼しているか」を尋ねるところから始まります。返ってきた答えが指す先へどこまでも従い、そのうえで人がサインインするか、事前共有したシークレットがその代わりを務めます。
|
||||
|
||||
企業は、そのどちらもサーバーごとに決めたくはありません。企業はすでに ID プロバイダー(Okta、Microsoft Entra ID、自社製のもの)を運用しています。ユーザーは今朝すでにそこへサインイン済みです。そしてそこは、セキュリティチームが「誰が何に到達してよいか」を一か所で決めたい場所でもあります。**Enterprise-Managed Authorization** 拡張である [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) は、その判断をそこへ移します。IdP は有効期間の短い JWT、すなわち **Identity Assertion JWT Authorization Grant**(**ID-JAG**)に署名します。これは「このユーザーが、このクライアントを通じて、この MCP サーバーに到達してよい」という表明です。クライアントはそれを通常のアクセストークンと交換します。ブラウザーも、同意画面も、動的登録もありません。
|
||||
|
||||
このページでは、その交換の両端を扱います。MCP サーバー自体は何も変わりません。**[認可](../run/authorization.md)** で説明したリソースサーバーのまま、届いたトークンを何であれ検査します。
|
||||
|
||||
## 2 つのトークンリクエスト {#two-token-requests}
|
||||
|
||||
ここには 2 つの別々の権限主体が関わっています。両者を区別して呼び分けられれば、このページの大半は理解できたも同然です。**エンタープライズ IdP** は組織の ID プロバイダーです。従業員が誰であるかを知っており、ポリシーが置かれる場所であり、ID-JAG を発行します。SDK がこれと通信することはありません。**MCP 認可サーバー** は **[認可](../run/authorization.md)** のときと同じ当事者です。MCP サーバーのメタデータに名前が載っている発行者(issuer)であり、その MCP サーバーが受け入れるトークンを発行する存在です。通常の OAuth フローでは、この 2 つの役割はたいてい 1 つの箱に収まっています。ここでは 2 つに分かれており、このグラント全体は、後者が前者を信頼すると同意することにほかなりません。
|
||||
|
||||
クライアントは、それぞれに 1 回ずつトークンリクエストを送ります。
|
||||
|
||||
1. **エンタープライズ IdP へ。** クライアントはユーザーのサインイン(OpenID Connect の ID トークン)を ID-JAG と交換します。これは [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) のトークン交換であり、完全に IdP 側の API であって、**SDK はこのリクエストを行いません**。行うのは呼び出し側で、1 つの非同期コールバックの中で実装します。ポリシーの判断が下されるのもここです。IdP が拒否すれば ID-JAG は発行されず、提示するものは何もありません。
|
||||
2. **MCP 認可サーバーへ。** クライアントは [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) の `jwt-bearer` グラント(`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`、ID-JAG を `assertion` として)で ID-JAG を提示し、アクセストークンを受け取ります。**SDK が行うのはこちらのリクエストです**。そしてこれを受け入れることが、このページが認可サーバーに追加する唯一の事柄です。
|
||||
|
||||
以降はすべて 2 番目のリクエストの話です。それを送るクライアントと、それに応答する認可サーバーを扱います。
|
||||
|
||||
## クライアント {#the-client}
|
||||
|
||||
**`IdentityAssertionOAuthProvider`** は `mcp.client.auth.extensions.identity_assertion` にあります。**[OAuth クライアント](oauth-clients.md)** のどのプロバイダーとも同じく `httpx2.Auth` です。インスタンスを作り、`auth=` に載せ、その `httpx2.AsyncClient` をトランスポートに渡します。
|
||||
|
||||
```python title="client.py" hl_lines="49-50 53-61"
|
||||
--8<-- "docs_src/identity_assertion/tutorial001.py"
|
||||
```
|
||||
|
||||
下から順に読んでいきます。
|
||||
|
||||
* `main()` は標準的な OAuth クライアントの `main()`(**[OAuth クライアント](oauth-clients.md)**)そのもので、1 行も変わっていません。そこが肝心です。プロバイダーさえできてしまえば、下流のどこも、どのグラントがトークンを生んだのかを知りません。
|
||||
* プロバイダーが受け取るのは、ほかのプロバイダーには発見できないものです。誰かが認可サーバーに**事前登録**した `client_id` と `client_secret`、その認可サーバーの `issuer`、そして要求に応じて新しい ID-JAG を返す非同期コールバック `assertion_provider` です。
|
||||
* `storage` は同じ `TokenStorage` プロトコルです。呼ばれるのは 2 つのトークンメソッドだけです。ここには動的登録がないので、覚えておくべき `client_info` もありません。
|
||||
|
||||
### アサーションプロバイダー {#the-assertion-provider}
|
||||
|
||||
自分で書くコードは `fetch_id_jag(audience, resource)` だけです。トークン交換のたびに 1 回 await され、構築時に呼ばれることはありません。しかも認可サーバーのメタデータを取得して検証した「後」でしか呼ばれないため、issuer の設定ミスでアサーションが漏れることはありません。2 つの引数は、ID-JAG の発行時に含めなければならないクレームのうちの 2 つです。`audience` は認可サーバーの issuer(ID-JAG の `aud`)、`resource` は MCP サーバーの正規識別子(ID-JAG の `resource`)です。3 つ目はすでに手元にあります。ID-JAG の `client_id` クレームは、プロバイダーに渡した `client_id` を指していなければならず、そうでなければ認可サーバーは交換を拒否します。
|
||||
|
||||
その上にある `idp_issue_id_jag` は**自分で書くコードではありません**。これは ID プロバイダーの代役で、ファイルが単体で完結し、ID-JAG が運ぶクレームをすべて読めるように、同一プロセス内でアサーションに署名しています。実際の `fetch_id_jag` は、代わりに前節の 1 番目のトークンリクエストを行います。すなわち IdP に対する [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) のトークン交換で、[SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) がプロファイル化している Identity Assertion JWT Authorization Grant ドラフトで定義されています。サインイン済みユーザーの ID トークンが `subject_token` として入り、`requested_token_type` は ID-JAG 自身の URN(`urn:ietf:params:oauth:token-type:id-jag`)です。`audience` と `resource` はそのまま渡され、レスポンスが ID-JAG を運んできます。IdP のドキュメントで探すべきは、これらの名前を使ったこの交換です。
|
||||
|
||||
!!! tip
|
||||
交換のたびに新しい ID-JAG が要求されますが、それこそが狙いです。ID-JAG は使い切りで数分しか生きないグラントであり、このページの認可サーバーは同じものを 2 度受け入れることを拒否します。キャッシュしないでください。再利用されるのは、それで手に入れたアクセストークンのほうです。
|
||||
|
||||
### issuer は設定値 {#the-issuer-is-configuration}
|
||||
|
||||
ここに逆転があります。`OAuthClientProvider` は、どの認可サーバーを使うかをリソースサーバーに尋ね、返ってきた答えが指す先へどこまでも従います。このプロバイダーはそれを拒みます。`issuer` は必須で、[RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) のメタデータはその issuer 自身の well-known パスから取得されます。トークンエンドポイントはその issuer のオリジン上になければならず、リソースサーバーには何も尋ねません。
|
||||
|
||||
拡張仕様がこれを要求しているわけではありません。意図的に、より厳しくした選択です。このクライアントは盗む価値のあるものを 2 つ持っています。事前登録されたシークレットと、audience に束縛されたアサーションです。侵害された MCP サーバーに攻撃者の認可サーバーへ誘導されるのを許すクライアントなら、その両方をそこへ POST してしまうでしょう。構築時に issuer を固定すれば、そのやり取り自体がなくなります。
|
||||
|
||||
!!! warning
|
||||
設定した `issuer` は、メタデータ文書の `issuer` フィールドと RFC 8414 §3.3 の単純な文字列比較で照合されます。1 文字ずつ、末尾のスラッシュも含め、正規化なしです。推測しないでください。認可サーバーから `/.well-known/oauth-authorization-server` を取得し、返ってきた `issuer` の値をコピーしてください。このページの認可サーバーでは、それはスラッシュ付きの `https://auth.example.com/` です。issuer が pydantic の URL オブジェクトから組み立てられているためです。一致しない場合、クレデンシャルやアサーションが 1 つも送られる前に、`OAuthFlowError: Authorization server metadata issuer
|
||||
mismatch` でフローが止まります。
|
||||
|
||||
### コンフィデンシャルクライアント {#a-confidential-client}
|
||||
|
||||
`client_secret` は必須で、ないとコンストラクターが `ValueError` を送出します。[SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) の下敷きになっている IETF プロファイルはこのグラントをコンフィデンシャルクライアント専用としており、SEP-990 はクライアントの認証を要求しています。この SDK は、共有シークレットを必須とすることでその両方を強制しています。`token_endpoint_auth_method` で、シークレットをどこに載せて送るかを選びます。`client_secret_post`(デフォルト、フォーム本体の中)か `client_secret_basic`(HTTP Basic ヘッダー)です。プロファイルは `private_key_jwt` も許可していますが、このプロバイダーはサポートしていません。
|
||||
|
||||
!!! tip
|
||||
`client_secret` は環境変数かシークレットマネージャーから読み込んでください。ソース管理には決して入れないでください。
|
||||
|
||||
### プロバイダーがしてくれること {#what-the-provider-does-for-you}
|
||||
|
||||
最初のリクエストは認証なしで送られ、サーバーの `401` がフローを開始します。
|
||||
|
||||
1. **ディスカバリー。** 設定した issuer の [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) well-known パスから認可サーバーのメタデータを取得し、文書の `issuer` が一致することと、トークンエンドポイントが issuer のオリジン上にあることを確認します。
|
||||
2. **アサーション。** `assertion_provider` を await します。
|
||||
3. **交換。** `jwt-bearer` グラントをトークンエンドポイントに POST し、`OAuthToken` を保存し、元のリクエストを `Authorization: Bearer ...` 付きで再送します。
|
||||
|
||||
`WWW-Authenticate` に `insufficient_scope` が示された `403` では、指定した `scope` とチャレンジされたスコープの和集合で手順 2 と 3 をもう一度実行します。(`scope` はあくまで要求にすぎません。このページの認可サーバーは ID-JAG に書かれたものを付与し、それ以外は付与しません。)ここにはリフレッシュトークンはどこにもありません。アクセストークンが期限切れになると、次の `401` で新しい ID-JAG が発行されて再び交換が行われます。IdP が握っているレバーはまさに「そこ」です。失敗は **[OAuth クライアント](oauth-clients.md)** のほかの部分と同じ 2 つの例外です。ディスカバリーと検証には `OAuthFlowError`、トークンエンドポイントが拒否したときはそのサブクラスの `OAuthTokenError` です。
|
||||
|
||||
## 認可サーバー {#the-authorization-server}
|
||||
|
||||
たいていの場合、ここで終わりです。MCP 認可サーバーは誰か別の人の製品であり、ID-JAG の受け入れはその製品側で有効にする設定です。[SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) のうち SDK が担う半分は、上のクライアントです。
|
||||
|
||||
SDK が認可サーバー「そのもの」になることもできます。`create_auth_routes` は認可サーバーのルートを、どんな Starlette アプリでもマウントできるリストとして返します。リポジトリの `examples/servers/simple-auth/` はそうやって認可サーバーを動かしています。SEP-990 は、そのインターフェースにフラグを 1 つとメソッドを 1 つ追加します。
|
||||
|
||||
```python title="auth_server.py" hl_lines="48-50 105-107"
|
||||
--8<-- "docs_src/identity_assertion/tutorial002.py"
|
||||
```
|
||||
|
||||
* `identity_assertion_enabled=True` がすべての門番です。オフ(これがデフォルト)のときは、フックを実装していても `/token` はこのグラントに `unsupported_grant_type` で応答し、メタデータにも載りません。オンにすると、メタデータに `jwt-bearer` グラントタイプが加わり、`authorization_grant_profiles_supported` に `urn:ietf:params:oauth:grant-profile:id-jag` が列挙されます。これは拡張仕様がサポートを告知するために使うフィールドです。(この SDK のクライアントはそれを読みません。1 つの issuer 向けにプロビジョニングされており、単に要求するだけです。)
|
||||
* **`exchange_identity_assertion`** がフックです。これが実行される前に、SDK はクライアントを認証し、パブリッククライアントを拒否し、登録内容にこのグラントが含まれていないクライアントを拒否しています。受け取るのは `IdentityAssertionParams`(生の `assertion`、要求された `scopes` と `resource`)で、返すのは素の `OAuthToken` です。
|
||||
* 動的クライアント登録はこのグラントを無条件に拒否するので、ここでの `get_client` は手作業でプロビジョニングしたクライアントを返します。ID-JAG クライアントが自分で自分を登録して存在するようになることはできません。
|
||||
* クラスの半分は拒否です。`OAuthAuthorizationServerProvider` は認可サーバー「全体」なので、認可コードフローも求められます。ユーザーのサインインも行うサーバーならそれらを本当に実装しますが、このサーバーには入口がちょうど 1 つしかありません。
|
||||
|
||||
!!! warning
|
||||
SDK がアサーションをデコードすることは決してありません。どの IdP を信頼し、その IdP がどの鍵を公開しているかを知っているのはデプロイメントだけなので、`exchange_identity_assertion` の中身はすべてが安全性を支える要です。[RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) §3 に従い、IdP が公開している鍵(JWKS。ここでの共有シークレットはデモ用です)で署名を検証し、`iss` と `exp` も検証してください。JWT ヘッダーの `typ` が `oauth-id-jag+jwt` であることを要求してください。これは、別の JWT がグラントとして再利用されるのを防ぐプロファイルの防護策です。`aud` が自分自身の issuer であることを要求してください。ID-JAG の `client_id` クレームがハンドラーの認証したクライアントと一致すること、`resource` クレームが実際に提供しているリソースを指していることを要求してください。`jti` をアサーションの `exp` まで追跡し、一度しか受け入れないようにしてください。そして付与するスコープ、とりわけ発行するトークンの `resource` は、検証済みの ID-JAG から取り、リクエストからは決して取らないでください。`params.resource` はクライアントが入力したものにすぎません。処理ルールの全体は [Enterprise-Managed Authorization 仕様](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization) にあります。
|
||||
|
||||
不正なアサーションは `TokenError("invalid_grant", ...)` で拒否してください。このフローのもう 1 つのエラーコードは `invalid_target` です。提供していないリソースを指す ID-JAG はこれで拒否され、それによってこのサーバーが他人のリソース向けのトークンを発行するのを防ぎます。そして付与するスコープは ID-JAG の `scope` クレームから取ります(これを持たないアサーションも拒否されます)。実際のサーバーでは、代わりにユーザーのグループをマッピングするかもしれません。
|
||||
|
||||
返される `OAuthToken` が持っていないものにも注目してください。リフレッシュトークンです。IdP は、次の ID-JAG を発行するかどうかを決めることで、このユーザーがいつまでアクセスを保てるかを決めます。ここでリフレッシュトークンを発行してしまうと、その決定権をこっそり手放すことになります。
|
||||
|
||||
!!! info
|
||||
今も `auth_server_provider=` で認可サーバーを組み込んでいるサーバーは、`AuthSettings(identity_assertion_enabled=True)` を通じて同じコードに到達します。新しいサーバーがそこから始めるべきでない理由は **[認可](../run/authorization.md)** で説明しています。
|
||||
|
||||
!!! check
|
||||
このページの 2 つのファイルをつなぎ合わせると、グラント全体は 1 回の `POST /token` です。
|
||||
|
||||
```text
|
||||
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
|
||||
assertion=eyJhbGciOiJIUzI1NiIsInR5cCI6Im9hdXRoLWlkLWphZytqd3QifQ...
|
||||
client_id=finance-agent
|
||||
resource=http://localhost:8001/mcp
|
||||
scope=notes:read
|
||||
client_secret=finance-agent-secret
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
{"access_token": "mcp_...", "token_type": "Bearer", "expires_in": 300, "scope": "notes:read"}
|
||||
```
|
||||
|
||||
`/authorize` も、`/register` も、protected resource metadata の取得もありません。通信路に流れるリクエストは、`401` を引き出したもの、well-known の取得、この交換、そしてベアラートークンを付けた通常の MCP トラフィックだけです。そして、バリデーターが ID-JAG から読み取った `sub` は、ツールの中で `get_access_token().subject` が報告する値とまったく同じです。
|
||||
|
||||
### 試してみる {#try-it}
|
||||
|
||||
SDK リポジトリの `examples/stories/identity_assertion/` は、このページを実際に動かしたものです。同じ `exchange_identity_assertion` バリデーター、そのトークンで保護された MCP サーバー、代役の IdP、そしてクライアントが、1 つの自己検証プログラムにまとまっています。`uv run python -m stories.identity_assertion.client --http` で交換全体を実行し、IdP が名指ししたユーザーがツールから見えるユーザーであることを assert します。
|
||||
|
||||
## まとめ {#recap}
|
||||
|
||||
* [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) により、クライアントがどの MCP サーバーに到達してよいかを、エンドユーザーではなく企業の ID プロバイダーが決められます。IdP はその決定を **ID-JAG** に署名して封じ込めます。
|
||||
* ID-JAG の取得は「自分の IdP」に対する [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) のトークン交換であり、SDK は行いません。それを MCP 認可サーバーに提示するのが [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) の `jwt-bearer` グラントで、SDK はその両側を担います。
|
||||
* `IdentityAssertionOAuthProvider` もまた `httpx2.Auth` の 1 つです。事前登録済みのコンフィデンシャルクライアント、固定した `issuer`、そして 1 つの `assertion_provider(audience, resource)` コールバックから成ります。ブラウザーも、登録も、リフレッシュトークンもありません。
|
||||
* 認可サーバーがリソースサーバーから発見されることはありません。`issuer` には、そのメタデータ文書が返す文字列と完全に同じものを設定してください。比較は 1 文字ずつです。
|
||||
* サーバー側は `identity_assertion_enabled=True` と `exchange_identity_assertion` です。SDK はクライアントを認証し、グラントの可否を判定します。ID-JAG の検証は完全に自分の責任で、発行されるトークンはリクエストのものではなく ID-JAG の `resource` に束縛されます。
|
||||
|
||||
このページが一度も触れなかった当事者が 1 つあります。MCP サーバーです。たった今発行したトークンで MCP サーバーが何をするかは、**[認可](../run/authorization.md)** ですでに行っていたことです。
|
||||
207
i18n/ja/pages/client/index.md
Normal file
207
i18n/ja/pages/client/index.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ebef1e7a0df854f4, a4c687d3d627d516, 8e79141fc2985342, b345dd05b9c3c7ab, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
|
||||
tool: 1
|
||||
---
|
||||
# Client {#the-client}
|
||||
|
||||
**`Client`** は、Python プログラムが MCP サーバーと対話するための手段です。
|
||||
|
||||
1 つのオブジェクトに 1 つのライフサイクルがあります。組み立てて、`async with` に入り、メソッドを呼び出します。プロトコルの動詞(ツールの一覧取得、ツールの呼び出し、リソースの読み取り、プロンプトのレンダリング)はどれも、このオブジェクトの `async` メソッドで、型付きの結果を返します。
|
||||
|
||||
## 最初のクライアント {#your-first-client}
|
||||
|
||||
```python title="client.py" hl_lines="14-18"
|
||||
--8<-- "docs_src/client/tutorial001.py"
|
||||
```
|
||||
|
||||
冒頭のサーバーは、接続先を用意するためだけにあります。クライアントはハイライトされた 5 行です。
|
||||
|
||||
* `Client(mcp)` には**サーバーオブジェクトそのもの**を渡しています。これがインメモリのトランスポートです。サブプロセスもポートも HTTP もありません。このページのすべての例、そして作成するすべてのテストが、この方法で接続します。
|
||||
* `async with` が**ライフサイクル**です。入ると接続してネゴシエーションを行い、出ると切断します。`connect()` / `close()` のペアはなく、ブロックが終わった後の `Client` は再利用できません。
|
||||
* ブロックの中では、接続に関する情報がすでに通常のプロパティとして揃っています。
|
||||
|
||||
### `Client` に渡せるもの {#what-you-can-pass-to-client}
|
||||
|
||||
`Client` は位置引数を 1 つ取り、その型からトランスポートを決定します。
|
||||
|
||||
* `MCPServer`(または低レベルの `Server`)のインスタンス:**プロセス内**で接続します。
|
||||
* URL 文字列(`Client("http://localhost:8000/mcp")`):Streamable HTTP。本番向けの経路です。
|
||||
* **トランスポート**:`async with ... as (read, write)` できるものなら何でも。たとえばサブプロセスをラップする `stdio_client(...)` です。
|
||||
|
||||
このページの残りの内容は、3 つのどれでも同じです。ヘッダー、サブプロセス、タイムアウト、そして `Transport` プロトコルについては、専用のページ **[クライアントのトランスポート](transports.md)** があります。
|
||||
|
||||
### 接続済みクライアントが持つもの {#whats-on-a-connected-client}
|
||||
|
||||
読み取り専用のプロパティが 4 つあり、ブロックに入った瞬間に値が入ります。
|
||||
|
||||
* `client.server_info`:サーバーの識別情報。報告しない 2026 年世代のサーバーでは `None` です(python-sdk のサーバーはデフォルトで報告します)。ここでは `server_info.name` が `"Bookshop"` で、`server_info.version` はサーバーが報告する値です。
|
||||
* `client.server_capabilities`:サーバーができること(`tools`、`resources`、`prompts`、`completions`、...)。サーバーが持たないケイパビリティは `None` です。
|
||||
* `client.protocol_version`:両者が合意したプロトコルバージョン。ここでは `"2026-07-28"` です。
|
||||
* `client.instructions`:サーバーの `instructions=` 文字列。設定されていなければ `None` です。
|
||||
|
||||
プロトコルバージョンを選んだ覚えはないはずです。デフォルトでは `Client` がサーバーを調べ、古いサーバーに対しては従来のハンドシェイクにフォールバックします。そのため、1 つのクライアントがどの世代のサーバーに対しても動作します。これを制御する必要がある場合、詳しくは **[プロトコルバージョン](../protocol-versions.md)** を参照してください。
|
||||
|
||||
!!! tip
|
||||
`client.session` は下層の `ClientSession` で、低レベルへの抜け道です。このページの内容では必要ありません。
|
||||
|
||||
## ツールの一覧取得 {#listing-tools}
|
||||
|
||||
```python title="client.py" hl_lines="15-20"
|
||||
--8<-- "docs_src/client/tutorial002.py"
|
||||
```
|
||||
|
||||
`list_tools()` は `ListToolsResult` を返し、ツールは `.tools` に入っています。それぞれが、ホストがモデルに渡す完全な定義です。
|
||||
|
||||
```python
|
||||
tool.name # 'search_books'
|
||||
tool.title # 'Search the catalog'
|
||||
tool.description # 'Search the catalog by title or author.'
|
||||
```
|
||||
|
||||
そして `tool.input_schema` は、サーバーが関数の型ヒントから導き出した JSON Schema です。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"title": "Query", "type": "string"},
|
||||
"limit": {"default": 10, "title": "Limit", "type": "integer"}
|
||||
},
|
||||
"required": ["query"],
|
||||
"title": "search_booksArguments"
|
||||
}
|
||||
```
|
||||
|
||||
このスキーマには、UI が引数フォームを描画するのに必要なものも、モデルが有効な引数を生成するのに必要なものも、すべて含まれています。
|
||||
|
||||
!!! tip
|
||||
`title` は省略可能なので、人間にツールを見せる UI はどちらかを選ぶ必要があります。`title` があればそれを、なければ `name` を使います。`from mcp.shared.metadata_utils import get_display_name` がまさにそれを行い、ツール、リソース、リソーステンプレート、プロンプトに対応しています。
|
||||
|
||||
## ツールの呼び出し {#calling-a-tool}
|
||||
|
||||
`call_tool(name, arguments)` はツールを実行し、`CallToolResult` を返します。
|
||||
|
||||
```python title="client.py" hl_lines="26-33"
|
||||
--8<-- "docs_src/client/tutorial003.py"
|
||||
```
|
||||
|
||||
サーバーの `lookup_book` は Pydantic の `Book` を返します。クライアントから見えるのは次のとおりです。
|
||||
|
||||
```python
|
||||
result.content # [TextContent(type='text', text='{\n "title": "Dune",\n "author": "Frank Herbert",\n "year": 1965\n}')]
|
||||
result.structured_content # {'title': 'Dune', 'author': 'Frank Herbert', 'year': 1965}
|
||||
result.is_error # False
|
||||
```
|
||||
|
||||
戻り値は 1 つ、読むべきものは 3 つです。それぞれ読み手が異なります。
|
||||
|
||||
### `content`:モデルが読むもの {#content-what-the-model-reads}
|
||||
|
||||
`content` は**コンテンツブロック**の `list` で、コンテンツブロックはユニオン型です。`TextContent`、`ImageContent`、`AudioContent`、`ResourceLink`、`EmbeddedResource` のいずれかです。ツールは種類の異なるブロックを複数返せます。
|
||||
|
||||
`main` が `block.text` に触れる前に `isinstance(block, TextContent)` で絞り込んでいるのはそのためです。`isinstance` の外に `.text` がないことに注目してください。`ImageContent` が持つのは `.text` ではなく `.data` なので、型チェッカーが許しません。このユニオンは、ツールが送ってよいものを正直に表しています。コードもそうあるべきです。
|
||||
|
||||
### `structured_content`:アプリケーションが読むもの {#structured_content-what-your-application-reads}
|
||||
|
||||
`structured_content` はツールの戻り値を JSON にしたもので、ツールが宣言した `output_schema` に一致します。文字列の解析も推測も不要です。
|
||||
|
||||
両方があるときは、意図的に同じことを 2 回言っています。`content` はモデル向け、`structured_content` はコード向けです。構造化されたほうがどこから来るのか、どう制御するのかは、**[構造化出力](../servers/structured-output.md)** のページで説明しています。
|
||||
|
||||
### `is_error`:ツールが失敗したかどうか {#is_error-whether-the-tool-failed}
|
||||
|
||||
例外を送出するツールが、クライアント側で例外を送出することは**ありません**。`is_error=True` の付いた通常の結果として返ってきます。
|
||||
|
||||
!!! check
|
||||
`lookup_book` に `"Solaris"`(カタログにない書名)を問い合わせると、関数は `ValueError` を送出します。それでも呼び出しは正常に返ります。
|
||||
|
||||
```python
|
||||
result.is_error # True
|
||||
result.content # [TextContent(type='text', text="Error executing tool lookup_book: No book titled 'Solaris' in the catalog.")]
|
||||
result.structured_content # None
|
||||
```
|
||||
|
||||
例外のメッセージは `content` に入りました。そこなら**モデル**が読んで、やり直せます。これは意図的なものです。ツールのエラーはクラッシュではなく、会話の一部です。`structured_content` を信用する前に、必ず `is_error` を確認してください。
|
||||
|
||||
!!! warning
|
||||
`is_error=True` がカバーするのは、自分で書いた `raise` だけではありません。サーバーに存在すらしないツールを要求しても(`call_tool("does_not_exist", {})`)、何も送出されません。同じ形の結果が返り、`is_error=True` で `content` には `Unknown tool: does_not_exist` が入ります。`Client` のメソッドが `MCPError` を送出するのは、サーバーが結果ではなく JSON-RPC の**エラー**で応答したときだけです。サーバーがどんなときにどちらを返すかは **[エラーの処理](../servers/handling-errors.md)** で扱っています。
|
||||
|
||||
## リソース {#resources}
|
||||
|
||||
リソースの動詞は組になっています。一覧取得が 2 通り、読み取りが 1 通りです。
|
||||
|
||||
```python title="client.py" hl_lines="22-31"
|
||||
--8<-- "docs_src/client/tutorial004.py"
|
||||
```
|
||||
|
||||
* `list_resources()` は**具体的な**リソース、つまり URI が固定のものを返します。ここでは `['catalog://genres']` です。
|
||||
* `list_resource_templates()` は**パラメーター化された**ものを返します。ここでは `['catalog://genres/{genre}']` です。テンプレートは値を埋めるまで読み取れないため、2 つは別々のリストになっています。
|
||||
* `read_resource(uri)` は通常の `str` の URI を受け取り、両方に対して動作します。`"catalog://genres/poetry"` を渡せば、サーバーがテンプレートに照合します。
|
||||
|
||||
`read_resource` は `contents` を返します。これは `TextResourceContents` または `BlobResourceContents` のリストです。考え方はツールのコンテンツと同じで、`isinstance` で絞り込んでから `.text`(または `.blob`)を読みます。
|
||||
|
||||
クライアントは、リソースが変更されたときに通知を受けることもできます。2025 年世代の接続では `subscribe_resource(uri)` / `unsubscribe_resource(uri)` がそれにあたります。ただしこのメソッドのペアは `MCPServer` が実装していないため、2026-07-28 の通信上(これらの動詞はもう存在しません)ではリクエストに `-32601`、*Method not found* が返ります。2026 年の代替は `subscriptions/listen` ストリームで、こちらは `MCPServer` が実際に提供しています(そこでは `server_capabilities.resources.subscribe` が `True` です)。これを `client.listen(...)` で消費する方法は、このセクションの **[サブスクリプション](subscriptions.md)** のページで説明しています。
|
||||
|
||||
## プロンプト {#prompts}
|
||||
|
||||
```python title="client.py" hl_lines="15-20"
|
||||
--8<-- "docs_src/client/tutorial005.py"
|
||||
```
|
||||
|
||||
`list_prompts()` は、サーバーが何を提供していて、各プロンプトが何を必要とするかを教えてくれます。
|
||||
|
||||
```python
|
||||
prompt.name # 'recommend'
|
||||
prompt.title # 'Recommend a book'
|
||||
prompt.arguments # [PromptArgument(name='genre', required=True)]
|
||||
```
|
||||
|
||||
`get_prompt(name, arguments)` でレンダリングします。引数の dict は `str -> str` で、プロンプトの引数は常に文字列です。結果は `messages`、つまり `PromptMessage` のリストで、それぞれが `role` と `content` ブロックを持ちます。
|
||||
|
||||
```python
|
||||
message.role # 'user'
|
||||
message.content # TextContent(type='text', text='Recommend one poetry book from the catalog and say why.')
|
||||
```
|
||||
|
||||
ホストはこれらのメッセージをそのままモデルに渡します。機能はこれだけです。
|
||||
|
||||
## 補完 {#completions}
|
||||
|
||||
補完ハンドラーを持つサーバーは、ユーザーの入力に合わせてプロンプトやリソーステンプレートの引数を自動補完できます。
|
||||
|
||||
```python title="client.py" hl_lines="27-31"
|
||||
--8<-- "docs_src/client/tutorial006.py"
|
||||
```
|
||||
|
||||
* `ref` は、「どの」プロンプトまたはテンプレートを埋めているかを示します。`PromptReference` または `ResourceTemplateReference` です。
|
||||
* `argument` は `{"name": ..., "value": ...}` で、引数と、ユーザーがこれまでに入力した内容です。
|
||||
|
||||
答えは `result.completion.values` に入っています。`"p"` と入力すると、サーバーは `['poetry']` を返します。サーバー側の実装と、ハンドラーがすでに埋まっている「他の」引数を使って候補を絞り込む方法は、**[補完](../servers/completions.md)** のページで説明しています。
|
||||
|
||||
## ページネーション {#pagination}
|
||||
|
||||
`list_*` メソッドはどれも `cursor=` キーワードを取り、結果はどれも `next_cursor` を持ちます。`next_cursor` が `None` なら、すべて取得済みです。
|
||||
|
||||
```python title="client.py" hl_lines="22-30"
|
||||
--8<-- "docs_src/client/tutorial007.py"
|
||||
```
|
||||
|
||||
このループはどのサーバーに対しても正しく動きます。`MCPServer` はすべてを 1 ページで返すので、`next_cursor` は `None` になり、ループは 1 回だけ実行されます。ほとんどのコードがこのループを書かないのはそのためです。実際にページ分割するサーバーと、カーソルが従うルールについては **[ページネーション](../advanced/pagination.md)** を参照してください。
|
||||
|
||||
## テストでの利用 {#in-tests}
|
||||
|
||||
プロセスもポートも使わない `Client(mcp)` は、それだけでサーバーのテストハーネスになります。
|
||||
|
||||
そのために用意されたコンストラクターのフラグが 1 つあります。`Client(mcp, raise_exceptions=True)` です。効果があるのはインメモリ接続のときだけで、その説明と、それを中心にしたパターン全体の組み立ては **[テスト](../get-started/testing.md)** のページにあります。
|
||||
|
||||
## まとめ {#recap}
|
||||
|
||||
* `Client(x)` は、サーバーオブジェクトにはインメモリで、URL 文字列には Streamable HTTP で、それ以外にはトランスポート経由で接続します。
|
||||
* `async with` がライフサイクルのすべてです。その中では `server_capabilities` と `protocol_version` にすでに値が入っており、サーバーが提供していれば `server_info` と `instructions` も同様です。
|
||||
* `list_tools()` で各ツールの `name`、`title`、`description`、`input_schema` が得られます。
|
||||
* `call_tool()` はモデル向けの `content`、コード向けの `structured_content`、そして `is_error` を返します。例外を送出するツールは、例外ではなく結果として返ってきます。
|
||||
* `content` はブロック型のユニオンです。読む前に `isinstance` で絞り込みます。
|
||||
* `list_resources` / `list_resource_templates` / `read_resource`、`list_prompts` / `get_prompt`、そして `complete` で動詞は一通り揃います。
|
||||
* `list_*` はどれも `cursor=` を取ります。`next_cursor` が `None` になるまでループします。
|
||||
|
||||
サーバーのほうからクライアントに要求できることと、それにどう応えるかは、**[クライアントのコールバック](callbacks.md)** で扱います。
|
||||
145
i18n/ja/pages/client/oauth-clients.md
Normal file
145
i18n/ja/pages/client/oauth-clients.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
---
|
||||
translation:
|
||||
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b]
|
||||
tool: 1
|
||||
---
|
||||
# OAuth クライアント {#oauth-clients}
|
||||
|
||||
一部の MCP サーバーは保護されています。トークンなしでリクエストを送ると、`401 Unauthorized` が返ってきます。
|
||||
|
||||
そのトークンを手に入れる手段が **`OAuthClientProvider`** です。これは MCP のオブジェクトではまったくありません。`httpx2.Auth`、つまり「すべてのリクエストに何かを施す」ための httpx2 標準のフックです。これを `httpx2.AsyncClient` に取り付け、そのクライアントを Streamable HTTP トランスポートに渡せば、あとは気にする必要がありません。
|
||||
|
||||
このページはクライアント側の話です。自分のサーバーにトークンを要求させる方法は **[認可](../run/authorization.md)** で扱います。
|
||||
|
||||
## プロバイダー {#the-provider}
|
||||
|
||||
```python title="client.py" hl_lines="44-54"
|
||||
--8<-- "docs_src/oauth_clients/tutorial001.py"
|
||||
```
|
||||
|
||||
渡すものは 4 つです。
|
||||
|
||||
* `server_url`:接続先の MCP エンドポイント。プロバイダーはそれ以外のすべてをここから発見します。
|
||||
* `client_metadata`:認可サーバーの「アプリケーションを登録する」フォームに入力するような内容。
|
||||
* `storage`:実行と実行のあいだにトークンを保管しておく場所。
|
||||
* `redirect_handler` と `callback_handler`:人間が関わる 2 つの場面。
|
||||
|
||||
ファイル内のほかの箇所には OAuth は一切登場しません。`main()` がトークンを目にすることはありません。
|
||||
|
||||
### クライアントメタデータ {#client-metadata}
|
||||
|
||||
`OAuthClientMetadata` は、本物の [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) 登録ドキュメントを Pydantic モデルにしたものです。
|
||||
|
||||
設定するフィールドは 3 つです。残りはデフォルト値が埋めてくれます。`grant_types` は最初から `["authorization_code", "refresh_token"]`、`response_types` は最初から `["code"]` で、これはまさにこのプロバイダーが実行するフローです。
|
||||
|
||||
!!! check
|
||||
Pydantic モデルなので、**ネットワークに 1 バイトも流れる前に**検証されます。
|
||||
`redirect_uris` を省くと、構築の時点でそのフィールド名を指した `ValidationError` で失敗します。
|
||||
|
||||
```text
|
||||
redirect_uris
|
||||
Field required [type=missing, input_value={'client_name': 'Bookshop Agent'}, input_type=dict]
|
||||
```
|
||||
|
||||
ブラウザーは開かず、認可サーバーに中途半端な登録が残ることもありません。
|
||||
|
||||
### トークンストレージ {#token-storage}
|
||||
|
||||
**`TokenStorage`** は 4 つの非同期メソッドを持つ `Protocol` です。何かを継承する必要はありません。メソッドを書けば、どんなクラスでもトークンストアになります。
|
||||
|
||||
* `get_tokens` / `set_tokens` は `OAuthToken`(アクセストークン、リフレッシュトークン、有効期限、スコープ)を保持します。
|
||||
* `get_client_info` / `set_client_info` は、プロバイダーが登録したときに認可サーバーが発行した `OAuthClientInformationFull`(`client_id` を含む)を保持します。
|
||||
|
||||
上のインメモリ版はちゃんと動きます。ただしプロセスが終了するとすべてを忘れるので、次の実行では一連の手順を最初からやり直すことになります。ファイルやプラットフォームのキーリングに永続化すれば、次の実行は何も聞かれずに済みます。
|
||||
|
||||
!!! tip
|
||||
トークンだけでなく `client_info` も保存してください。プロバイダーは、保存済みの `client_info` が見つからない初回に動的登録を行います。これを捨ててしまうと、実行のたびに新しい登録を発行することになります。
|
||||
|
||||
### 2 つのハンドラー {#the-two-handlers}
|
||||
|
||||
認可コードフローで人間が必要になるのはちょうど一度だけです。誰かがサインインして「許可」をクリックしなければなりません。
|
||||
|
||||
* **`redirect_handler`** は、完全に組み立て済みの認可 URL を引数に await されます。`client_id`、`redirect_uri`、`state`、PKCE チャレンジはすでにその中に入っています。やるべきことはブラウザーをそこへ向かわせることだけです。デスクトップアプリなら `webbrowser.open` を呼び、このファイルでは表示するだけです。
|
||||
* 次に **`callback_handler`** が await されます。ユーザーが `redirect_uri` に戻ってくるまで待ち、そのリダイレクトのクエリパラメーターを `AuthorizationCodeResult` として返します。
|
||||
|
||||
実際のクライアントは、`input()` を呼ぶ代わりにリダイレクト URI 上で小さなローカル HTTP サーバーを動かします。形はまったく同じです。リダイレクトを受け取り、`code`、`state`、`iss` を返します。
|
||||
|
||||
!!! warning
|
||||
`state` と `iss` は届いたとおりそのまま渡してください。プロバイダーは `state` を自分が生成したものと、`iss` を発見した発行者と照合し、一致しなければ拒否します。これらは CSRF とサーバー取り違えに対する防御です。
|
||||
|
||||
### `Client` へ {#into-the-client}
|
||||
|
||||
`main()` を見てください。プロバイダーは **httpx2 クライアント**に載り、httpx2 クライアントは `streamable_http_client(url, http_client=...)` に入り、そのトランスポートが `Client` に入ります。
|
||||
|
||||
`streamable_http_client` には `auth=` キーワードがありません。HTTP レベルのもの(認証、ヘッダー、タイムアウト、プロキシ)はすべて、持ち込む `httpx2.AsyncClient` に設定します。このレイヤー構成については **[クライアントのトランスポート](transports.md)** を参照してください。
|
||||
|
||||
## プロバイダーがやってくれること {#what-the-provider-does-for-you}
|
||||
|
||||
`Client` が初めてリクエストを送ると、サーバーは `401` を返します。そこからプロバイダーが引き継ぎます。
|
||||
|
||||
1. **発見。** `WWW-Authenticate` ヘッダーを読み、サーバーの Protected Resource Metadata を `/.well-known/oauth-protected-resource` から取得します。そこからこのリソースを保護している認可サーバーを知り、「その」サーバーのメタデータを取得します。
|
||||
2. **登録。** ストレージに何もなければ、`OAuthClientMetadata` を使って動的に登録し、結果を保存します。
|
||||
3. **認可。** PKCE のペアと `state` を生成し、認可 URL を組み立て、`redirect_handler` を await します。続いて、コードを受け取るために `callback_handler` を await します。
|
||||
4. **交換。** コードを `OAuthToken` と引き換えて保存し、元のリクエストを `Authorization: Bearer ...` 付きで再送します。
|
||||
|
||||
それ以降は静かになります。トークンはストレージから取り出され、期限切れのアクセストークンはリフレッシュトークンで更新されます。そのどれもうまくいかないときだけ、フローをもう一度実行します。
|
||||
|
||||
これらを自分で書く必要はまったくありませんでした。残るキーワード引数は 2 つ(`client_metadata_url` と `validate_resource_url`)で、このファイルではどちらも不要です。知っておく価値があるのは `client_metadata_url` のほうで、下に専用のセクションがあります。
|
||||
|
||||
### 試してみる {#try-it}
|
||||
|
||||
このドキュメントの例のほとんどは、インメモリの `Client(server)` で確認できます。これは違います。このフローの要点は HTTP の `401` であり、インメモリのクライアントとサーバーのあいだには HTTP がありません。
|
||||
|
||||
リポジトリには実際に動くバージョンが同梱されています。`examples/servers/simple-auth/` はスタンドアロンの認可サーバーと保護された MCP サーバーを動かし、`examples/clients/simple-auth-client/` はこのページのクライアントを小さな CLI に育てたものです。その README に 2 つのコマンドが載っています。サーバーを起動し、それに対してクライアントを実行すれば、4 つのステップが進んでいくのを見られます。
|
||||
|
||||
## Client ID Metadata Documents {#client-id-metadata-documents}
|
||||
|
||||
仕様の 2026-07-28 改訂では、動的クライアント登録が非推奨になり、代わりに **Client ID Metadata Documents**(CIMD)が推奨されます。出会う認可サーバーごとに新しい登録を POST する代わりに、クライアントは自分自身についての JSON ドキュメントを 1 つ、安定した HTTPS URL で公開します。そしてその URL がそのまま `client_id` になります。ドキュメントを取得するのは認可サーバーで、プロバイダーはそれに一切触れません。
|
||||
|
||||
SDK はすでにこれに対応しています。プロバイダーを構築するときに URL を `client_metadata_url=` として渡してください。認可サーバーのメタデータが `client_id_metadata_document_supported: true` を公表していれば、プロバイダーは `/register` リクエストを完全に省きます。URL が `client_id` としてフローに入り、`client_secret` はありません。サーバーがそれを公表していない場合(まだ大半がそうです)、または URL を渡さなかった場合、プロバイダーは**何も言わずに**動的登録にフォールバックし、上の説明どおりにすべてが動きます。保存済みの `client_info` は、依然としてそのどちらよりも優先されます。
|
||||
|
||||
URL は HTTPS で、ルート以外のパスを持っている必要があります。それ以外は、ネットワーク通信が起こる前の構築時点で `ValueError` になります。同梱の `examples/clients/simple-auth-client/` は、これを `MCP_CLIENT_METADATA_URL` 環境変数として受け取ります。
|
||||
|
||||
## マシン間通信 {#machine-to-machine}
|
||||
|
||||
夜間ジョブ、CI のステップ、別のサービス。ブラウザーはなく、「許可」をクリックする人もいません。これが **クライアントクレデンシャル** グラントです。`client_id` と `client_secret` はすでに手元にあり、トークンエンドポイントがフローのすべてです。
|
||||
|
||||
`ClientCredentialsOAuthProvider` は同じ `httpx2.Auth` で、人間がいないだけです。
|
||||
|
||||
```python title="client.py" hl_lines="4 27-33"
|
||||
--8<-- "docs_src/oauth_clients/tutorial002.py"
|
||||
```
|
||||
|
||||
変わった点は次のとおりです。
|
||||
|
||||
* `OAuthClientMetadata` もハンドラーもありません。`client_id` と `client_secret` を渡すと、プロバイダーはそれらを中心に最小限の `client_credentials` 登録を組み立て、動的登録を完全に省きます。
|
||||
* `scope` はスペース区切りの文字列で、OAuth の通信上の形式です。
|
||||
* その先はすべて同じです。同じ `TokenStorage`、同じ `httpx2.AsyncClient(auth=...)`、同じ `streamable_http_client` です。
|
||||
|
||||
デフォルトでは、シークレットはトークンリクエストの HTTP Basic 認証として送られます(`client_secret_basic`)。代わりにフォームボディに入れるには、`token_endpoint_auth_method="client_secret_post"` を渡してください。認可サーバーによっては、2 つのうち片方しか受け付けません。
|
||||
|
||||
!!! tip
|
||||
`client_secret` は環境変数かシークレットマネージャーから読み込んでください。ソース管理からは決して読み込まないでください。
|
||||
|
||||
!!! info
|
||||
`mcp.client.auth.extensions.client_credentials` にはもう 1 つプロバイダーがあります。
|
||||
**`PrivateKeyJWTOAuthProvider`** は、共有シークレットの代わりに JWT で認証するクライアント向けです(`private_key_jwt`、つまり鍵ペアやワークロードアイデンティティの方式)。パターンは同じで、1 つ構築して `auth=` に載せます。同じモジュールには、そのアサーションを組み立てる 2 つのヘルパー、`SignedJWTParameters` と `static_assertion_provider` も含まれています。
|
||||
|
||||
人間がいない状況はもう 1 つあります。クライアントが企業に属していて、どの MCP サーバーに到達してよいかをユーザーではなくその企業のアイデンティティプロバイダーが決める場合です。これは独自の信頼モデルを持つ別のグラントで、専用のページ **[アイデンティティアサーション](identity-assertion.md)** があります。
|
||||
|
||||
## 失敗したとき {#when-it-fails}
|
||||
|
||||
OAuth フローがうまくいかないと、プロバイダーは `mcp.client.auth` の `OAuthFlowError` を送出します。これには 2 つのサブクラスがあります。`OAuthRegistrationError` は、登録の結果として使えるクライアントが得られなかったことを意味します。認可サーバーが登録を拒否したか、登録はされたもののこのフローでは使えないクレデンシャル(たとえば実装していない認証方式)だった場合です。`OAuthTokenError` は、トークンを取得できなかったことを意味します。トークンエンドポイントに拒否されたか、保存済みのクライアントレコードにこのクライアントが適用できない認証方式が含まれていた場合で、後者は送信されずにトークンリクエストの組み立て中に報告されます。`except OAuthFlowError:` 1 つで、発見、登録、認可、交換のすべてをカバーできます。
|
||||
|
||||
すべてがフローエラーというわけではありません。ネットワークが失敗することもあります。それらは通常の `httpx2` の例外で、手を加えられずにそのまま通り抜けます。
|
||||
|
||||
## まとめ {#recap}
|
||||
|
||||
* `OAuthClientProvider` は `httpx2.Auth` です。`httpx2.AsyncClient` に載せ、それを `streamable_http_client(url, http_client=...)` に渡せば、`Client` は OAuth が行われたことを知ることすらありません。
|
||||
* 渡すものは 4 つです。サーバーの URL、`OAuthClientMetadata`、`TokenStorage`、そしてリダイレクト/コールバックのハンドラーのペアです。
|
||||
* `TokenStorage` は `Protocol` です。非同期メソッドが 4 つで、基底クラスはありません。トークンだけでなく `client_info` も永続化してください。
|
||||
* 発見、登録(動的、または **Client ID Metadata Document** 経由)、PKCE、`state` と `iss` のチェック、トークンの更新はプロバイダーの仕事であり、呼び出し側の仕事ではありません。
|
||||
* `ClientCredentialsOAuthProvider` は人間がいない版です。`client_id` と `client_secret` だけで、ハンドラーもブラウザーも要りません。
|
||||
* OAuth の失敗はすべて `OAuthFlowError` です。`OAuthRegistrationError` と `OAuthTokenError` がそのサブクラスです。
|
||||
|
||||
このハンドシェイクのもう半分、つまり「サーバー」にトークンを要求させる方法は **[認可](../run/authorization.md)** で扱います。
|
||||
85
i18n/ja/pages/client/session-groups.md
Normal file
85
i18n/ja/pages/client/session-groups.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
translation:
|
||||
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73]
|
||||
tool: 1
|
||||
---
|
||||
# セッショングループ {#session-groups}
|
||||
|
||||
`Client` は 1 つのサーバーに接続します。実際のアプリケーションでは複数のサーバー(検索サーバー、データベースサーバー、社内 API など)を使いたいことが多く、結局それぞれの接続とツール一覧を個別に管理することになります。
|
||||
|
||||
**`ClientSessionGroup`** は、多数の接続を保持し、それらが公開するものすべてを 1 つのビューにまとめる単一のオブジェクトです。
|
||||
|
||||
## 2 つのサーバー {#two-servers}
|
||||
|
||||
まず、ごく普通のサーバーを 2 つ用意します。互いに何の関係もないので、どちらも自然とツールに `search` という名前を付けています。
|
||||
|
||||
```python title="library_server.py" hl_lines="7"
|
||||
--8<-- "docs_src/session_groups/tutorial001.py"
|
||||
```
|
||||
|
||||
```python title="web_server.py" hl_lines="7"
|
||||
--8<-- "docs_src/session_groups/tutorial002.py"
|
||||
```
|
||||
|
||||
## 1 つのグループ {#one-group}
|
||||
|
||||
`ClientSessionGroup` を作成し、サーバーごとに **`connect_to_server`** を 1 回ずつ呼び出します。
|
||||
|
||||
```python title="client.py" hl_lines="10-12"
|
||||
--8<-- "docs_src/session_groups/tutorial003.py"
|
||||
```
|
||||
|
||||
* `connect_to_server` はサーバーオブジェクトではなく、トランスポートのパラメーターを受け取ります。サブプロセスを起動するなら `StdioServerParameters`(`mcp` から)、すでに URL で待ち受けているサーバーなら `StreamableHttpParameters` または `SseServerParameters`(`mcp.client.session_group` から)です。
|
||||
* `group.tools` は、接続しているすべてのサーバーのツールを集めた `dict[str, Tool]` です。`group.resources` と `group.prompts` も同じ形です。
|
||||
* `group.call_tool(name, arguments)` は名前を引き、それを所有するセッションを見つけて呼び出しを転送します。どのサーバーかを指定する必要はありません。
|
||||
|
||||
!!! check
|
||||
`client.py` を 2 つのサーバーと同じ場所に置いて実行してください。2 回目の `connect_to_server` は拒否されます。
|
||||
|
||||
```text
|
||||
mcp.shared.exceptions.MCPError: {'search'} already exist in group tools.
|
||||
```
|
||||
|
||||
これは `MCPError` で、2 つ目のサーバーの何かが登録される前に送出されます。名前はグループ**全体**で一意でなければならず、自分で管理していない 2 つのサーバーはいずれ衝突します。
|
||||
|
||||
## `component_name_hook` {#component_name_hook}
|
||||
|
||||
これはサーバー側ではなく、グループ側で解決します。`(name, server_info)` を受け取る関数を渡すと、グループは登録するすべての名前に対してその関数を実行します。
|
||||
|
||||
```python title="client.py" hl_lines="7-8 15"
|
||||
--8<-- "docs_src/session_groups/tutorial004.py"
|
||||
```
|
||||
|
||||
もう一度実行してください。`print(sorted(group.tools))` には両方が表示されます。
|
||||
|
||||
```text
|
||||
['Library.search', 'Web.search']
|
||||
```
|
||||
|
||||
* **キー**は自分で決めたものです。`by_server` は `server_info.name`、つまり各 `MCPServer(...)` の構築時に渡された名前からキーを組み立てました。
|
||||
* 中の `Tool` は変更されていません。`group.tools["Web.search"].name` は依然として `"search"` であり、`call_tool` が通信路に載せるのはこの名前です。プレフィックスがプロセスの外に出ることはありません。
|
||||
* ツールだけではありません。ライブラリの `hours` リソースは `Library.hours` として登録されます。
|
||||
|
||||
!!! tip
|
||||
フックは衝突したものだけでなく、**すべて**のサーバーの**すべて**の名前に対して実行されます。衝突時だけプレフィックスを付けるモードはありません。1 つの方式を決めて、全体に適用してください。
|
||||
|
||||
## サーバーの追加と削除 {#adding-and-removing-servers}
|
||||
|
||||
`connect_to_server` は開いた `ClientSession` を返します。後でそのサーバーを外したくなる場合に備えて保持しておいてください。`await group.disconnect_from_server(session)` で、そのサーバーのツール、リソース、プロンプトがグループから削除されます。
|
||||
|
||||
すでに接続済みの `ClientSession` を持っている場合(`Client.session` がそうです)、新しいトランスポートを開く代わりに `await group.connect_with_session(server_info, session)` に渡してください。同じように集約されます。グループは、自分で開いていないセッションを閉じることはありません。`server_info` はコンポーネントのプレフィックスに使うサーバー名を指定します。2026 年世代の接続では `client.server_info` が `None` になることがある(識別情報は任意です)ため、その場合は自分で `Implementation(name=..., version=...)` を渡してください。
|
||||
|
||||
## 従来のハンドシェイク {#the-classic-handshake}
|
||||
|
||||
`ClientSessionGroup` は `Client` ではなく `ClientSession` の上に構築されています。`connect_to_server` を呼ぶたびに従来の `initialize` ハンドシェイクが実行されます。**[プロトコルバージョン](../protocol-versions.md)**で説明している `server/discover` プローブを送ることはありません。このハンドシェイクはすべての MCP サーバーが理解するので、互換性が失われることは一切ありません。ただ、もっと良い方法に対応しているサーバーに対しても、グループは古くて遅い経路を取るというだけです。
|
||||
|
||||
## まとめ {#recap}
|
||||
|
||||
* `ClientSessionGroup` は多数のサーバー接続を保持し、それらのツール、リソース、プロンプトをそれぞれ 1 つの `dict` にまとめます。
|
||||
* サーバーごとに `connect_to_server(params)` を呼びます。受け取るのはトランスポートのパラメーターであり、`Client` が受け取るサーバーオブジェクトや URL ではありません。
|
||||
* `group.call_tool(name, arguments)` は、所有するサーバーへのルーティングを代わりに行います。
|
||||
* 名前はグループ全体で一意でなければなりません。`search` ツールを持つ 2 つのサーバーは、そのままでは共存できません。
|
||||
* `component_name_hook=` は登録されるすべての名前を書き換えます。dict のキーは変わりますが、実際に送信される名前は変わりません。
|
||||
* `connect_with_session` はすでに持っているセッションを追加し、`disconnect_from_server` はセッションを削除します。
|
||||
|
||||
グループが使うハンドシェイク(と、`Client` が優先するより高速なハンドシェイク)について詳しくは、**[プロトコルバージョン](../protocol-versions.md)**を参照してください。
|
||||
88
i18n/ja/pages/client/subscriptions.md
Normal file
88
i18n/ja/pages/client/subscriptions.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
translation:
|
||||
sections: [8f9558e57f29eee1, a88c587739e0465c, 46ebfd5b325ed041, 4d10b00b57ce4bd9, 2cdb0edd1f59b3e2]
|
||||
tool: 1
|
||||
---
|
||||
# サブスクリプション {#subscriptions}
|
||||
|
||||
サーバーのカタログは固定ではありません。ツールは実行時に現れ、リソース URI の背後にある内容は変化します。クライアントはそれを `client.listen(...)` を通じて知ります。これは 1 つの `subscriptions/listen` リクエストで、そのレスポンスが「そのまま」ストリームになります。ストリームは開いたままになり、クライアントが求めた変更通知を運びます。
|
||||
|
||||
このページはクライアント側の話です。ストリームを開き、メインの処理の横で監視し、その終わり方を扱います。変更の公開、フィルタリング、このメソッドの提供はサーバー側の話で、「ハンドラーの中で」にある **[サブスクリプション](../handlers/subscriptions.md)** で説明しています。ここでの例は、そこで作ったスプリントボードサーバーと通信します。
|
||||
|
||||
## ストリームを監視する {#watching-the-stream}
|
||||
|
||||
サブスクリプションは 1 つのコンテキストマネージャーです。中に入るとリクエストが送られ、キーワード引数がサブスクリプションのフィルターになります。そのうえでサーバーの確認応答を待つので、ブロックが始まる時点でストリームはすでに稼働しています。
|
||||
|
||||
```python title="client.py" hl_lines="15 18 28"
|
||||
--8<-- "docs_src/subscriptions/tutorial003.py"
|
||||
```
|
||||
|
||||
反復処理では 4 種類の型付きイベントが得られます。`ToolsListChanged`、`PromptsListChanged`、`ResourcesListChanged`、`ResourceUpdated(uri=...)` です。
|
||||
|
||||
イベントが伝えるのは「何が」変わったかであり、「どう」変わったかではありません。`follow_board` が `read_resource` と `list_tools` を呼ぶのはそのためで、イベントは再取得の合図です。どのリソースが動いたかを決めつけず、`event.uri` を読んでください。フィルターは複数の URI を指定でき、サーバーはそのうちの 1 つのサブリソースに対する変更を報告することもあります。
|
||||
|
||||
消費待ちの重複したイベントは 1 つにまとめられますが、再取得すれば現在の状態は得られます。まとめられるのは同一のイベントだけです。異なる URI に対する 2 つの `ResourceUpdated` は 2 つのイベントです。
|
||||
|
||||
ハンドルにはさらに 2 つのプロパティがあります。
|
||||
|
||||
* `sub.honored` はサーバーが確認応答したフィルターです。渡したフィールドを持つ `SubscriptionFilter` で、属性として読めます(`sub.honored.prompts_list_changed`)。`MCPServer` は求めた種類をすべて受け入れるので、リクエストをそのまま返します。対応する種類が少ないサーバーは確認応答する内容も少なくなり、受け入れられた種類でも一度も発火しないことがあります。サーバーは確認応答する代わりにリクエスト全体を拒否することもあり(サーバー側のページの[誰が監視できるかを決める](../handlers/subscriptions.md#deciding-who-may-watch)を参照)、その場合はリクエストのエラーとして表面化します。
|
||||
* `sub.subscription_id` は listen リクエストの ID で、このストリームのすべてのフレームに刻まれます。複数のサブスクリプションを同時に開くことができ、それぞれが自分の ID で多重分離されます。
|
||||
|
||||
## ブロックせずに監視する {#watching-without-blocking}
|
||||
|
||||
`follow_board` はサーバーがストリームを閉じるまで動き続けます。それは永遠に来ないかもしれないので、単独で動かすとプログラムを占有してしまいます。実際のクライアントが欲しいのは、メインの処理の「横で」動くウォッチャーです。エージェントがツールを呼ぶ一方で、ウォッチャーがキャッシュや UI を最新に保ちます。
|
||||
|
||||
まずサブスクリプションを開き、それからウォッチャーを起動して本来の作業に取りかかってください。
|
||||
|
||||
=== "asyncio"
|
||||
|
||||
```python title="app.py" hl_lines="18 20"
|
||||
--8<-- "docs_src/subscriptions/tutorial004_asyncio.py"
|
||||
```
|
||||
|
||||
=== "trio"
|
||||
|
||||
```python title="app.py" hl_lines="18 21"
|
||||
--8<-- "docs_src/subscriptions/tutorial004_trio.py"
|
||||
```
|
||||
|
||||
=== "anyio"
|
||||
|
||||
```python title="app.py" hl_lines="18 21"
|
||||
--8<-- "docs_src/subscriptions/tutorial004_anyio.py"
|
||||
```
|
||||
|
||||
!!! note
|
||||
`app.py` は最初の例から `BOARD` と `read_board` をインポートします。このリポジトリではその例を `tutorial003.py` として保存しています。レンダリングされたファイルを `client.py` と `app.py` として並べて保存する場合は、代わりに `from client import BOARD, read_board` と書いてください。さらに下の `watch.py` の例も同じように `read_board` をインポートします。
|
||||
|
||||
重要なのは順序です。何も再送されないので、ストリームができる前に公開されたイベントは取りこぼします。`client.listen(...)` に入ると確認応答を待つので、その瞬間以降のすべての変更はウォッチャーに届きます。ブロックの中で取るスナップショットが変更を取りこぼすことはありません。
|
||||
|
||||
リクエストは開いたストリームの横で自由に実行できます。ウォッチャーのタスクからでも他のタスクからでも、同じクライアント上で構いません。未消費の「重複した」イベントはまとめられるので、メインの処理が忙しいときは再取得が 3 回ではなく 1 回で済むこともあります。異なるイベントはまとめられません。多くの URI を指定したフィルターでは、URI ごとに保留中のイベントが 1 つずつキューに入ります。
|
||||
|
||||
監視をやめるにはブロックを抜けます。`unsubscribe` の呼び出しはありません。ブロックを所有するタスクをキャンセルすればそうなり、SDK はトランスポートが期待する方法で listen リクエストをキャンセルします。Streamable HTTP では、そのリクエストのストリームを閉じます。アプリの寿命のあいだ動き続けるウォッチャーは自分からは返らないので、シャットダウン時にそのタスク、またはタスクグループのスコープをキャンセルしてください。
|
||||
|
||||
## ストリームの終わり {#streams-end}
|
||||
|
||||
ストリームの終わり方は 2 通りあり、どちらも通常の制御フローです。サーバーが正常に閉じると `async for` が終わり、突然切れると `SubscriptionLost` が送出されます。
|
||||
|
||||
この違いは診断上のもので、次に何をすべきかの違いではありません。ストリームはなくなり、何も再送されず、まだ関心のあるウォッチャーは改めて listen して再取得します。
|
||||
|
||||
```python title="watch.py" hl_lines="16 20"
|
||||
--8<-- "docs_src/subscriptions/tutorial005.py"
|
||||
```
|
||||
|
||||
サーバーは自分の都合でストリームを正常に閉じます。バックログが大きくなりすぎた購読者を切り離す場合もそうです。そのため、きれいに終わったことは監視をやめる合図ではありません。改めて listen する前にバックオフしてください。
|
||||
|
||||
`SubscriptionLost` にはローカルな原因も 1 つあります。クライアントが保持する未消費イベントは最大 1024 件で、そこまで遅れた消費側は際限なく膨らむ代わりにサブスクリプションを失います。`async for` の本体は短く保ち、時間のかかる作業は別の場所で行ってください。
|
||||
|
||||
`keep_following` が捕捉するのは `SubscriptionLost` だけです。`listen()` に入るときには、`MCPError`(接続に失敗した、またはサーバーがこのメソッドを提供していない)、`TimeoutError`(確認応答が届かなかった)、`ListenNotSupportedError`(2026 年より前の接続)が送出されることもあります。ウォッチャーがそのうちどれを再試行すべきかを決めてください。最後のものは決して回復しません。
|
||||
|
||||
## まとめ {#recap}
|
||||
|
||||
* `async with client.listen(...)` に入ります。入ると確認応答を待つので、その後に公開されたものを取りこぼすことはありません。
|
||||
* `async for event in sub` で反復します。イベントは再取得の合図であって、ペイロードではありません。
|
||||
* サブスクリプションを開いてからウォッチャーをタスクとして実行すれば、その横でツール呼び出しが流れ続けます。
|
||||
* きれいに終わればループが止まり、切れれば `SubscriptionLost` が送出されます。どちらの場合も、改めて listen して再取得します。その前にまずバックオフしてください。
|
||||
* ブロックを抜けることがサブスクリプションの解除です。
|
||||
|
||||
これらのイベントの公開、フィルターの絞り込み、1 プロセスを超えたスケーリングはサーバー側の話です。詳しくは **[サブスクリプション](../handlers/subscriptions.md)** を参照してください。同じイベントはクライアント側のキャッシュを正確に保つのにも役立ちます。次のページは **[キャッシュ](caching.md)** です。
|
||||
114
i18n/ja/pages/client/transports.md
Normal file
114
i18n/ja/pages/client/transports.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
---
|
||||
translation:
|
||||
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, ff7401df479af877, 3d0832f39b0d7059, d4bf7e4479637768, 05e20c0a798860e7]
|
||||
tool: 1
|
||||
---
|
||||
# クライアントのトランスポート {#client-transports}
|
||||
|
||||
どの `Client` も、**トランスポート**を介してサーバーと対話します。トランスポートとは、実際にメッセージを運ぶもののことです。
|
||||
|
||||
トランスポートを別途設定することはありません。`Client` は位置引数を 1 つだけ受け取り、その型からトランスポートを判断します。
|
||||
|
||||
それぞれの「サーバー」側(`mcp.run()` が何をするのか、何をデプロイするのか)については、**[サーバーの実行](../run/index.md)** を参照してください。
|
||||
|
||||
## インメモリ {#in-memory}
|
||||
|
||||
サーバーオブジェクトそのものを渡します。
|
||||
|
||||
```python title="client.py" hl_lines="14"
|
||||
--8<-- "docs_src/client_transports/tutorial001.py"
|
||||
```
|
||||
|
||||
サブプロセスも、ポートも、通信路を流れるバイト列もありません。クライアントとサーバーは同じプロセス内の 2 つのオブジェクトですが、呼び出しは本物のプロトコル層を通ります。`search_books` は、HTTP 越しの場合とまったく同じように一覧に載り、検証され、呼び出されます。
|
||||
|
||||
そのため、これは同時に 2 つの役割を果たします。
|
||||
|
||||
* **テストハーネス。** このドキュメントの例はすべてこの方法で実行されており、**[テスト](../get-started/testing.md)** のページはこのパターンを中心に組み立てられています。
|
||||
* **組み込み用の API。** サーバーを自分で構築するアプリケーションなら、そのツールを呼び出すのにネットワーク越しの経路は必要ありません。
|
||||
|
||||
## Streamable HTTP {#streamable-http}
|
||||
|
||||
URL の文字列を渡すと **Streamable HTTP** になります。デプロイ時に使うトランスポートです。
|
||||
|
||||
```python title="client.py" hl_lines="5"
|
||||
--8<-- "docs_src/client_transports/tutorial002.py"
|
||||
```
|
||||
|
||||
本番用のクライアントはこれですべてです。`Client` は URL を `streamable_http_client(...)` で包み、MCP に必要な設定を施した `httpx2.AsyncClient` の上に載せてくれます。具体的には `follow_redirects=True`、connect/write/pool のタイムアウトが 30 秒、そしてサーバーがレスポンスストリームを開いたままにすることがあるため read のタイムアウトが 300 秒です。
|
||||
|
||||
!!! check
|
||||
構築しただけの `Client` は接続されて**いません**。構築時に行われるのはトランスポートの選択だけで、実際に開くのは `async with` です。入る前に接続に手を伸ばすと、SDK がそのことを教えてくれます。
|
||||
|
||||
```text
|
||||
RuntimeError: Client must be used within an async context manager
|
||||
```
|
||||
|
||||
`Client("http://...")` と書いた時点では、何も解決も取得も起動もされていません。この行にコストはかかりません。
|
||||
|
||||
### 自前の `httpx2.AsyncClient` を使う {#bring-your-own-httpx2asyncclient}
|
||||
|
||||
`Authorization` ヘッダー、Cookie、プロキシ、mTLS、あるいは別のタイムアウトが必要になったら、`httpx2.AsyncClient` を自分で組み立てて `streamable_http_client` に渡します。
|
||||
|
||||
```python title="client.py" hl_lines="8-14"
|
||||
--8<-- "docs_src/client_transports/tutorial003.py"
|
||||
```
|
||||
|
||||
注目すべき点が 2 つあります。
|
||||
|
||||
* `httpx2.AsyncClient` の所有者は**自分**なので、入るのも出るのも自分で行います。SDK は自身が作成していないクライアントを決して閉じません。
|
||||
* `streamable_http_client(url, http_client=...)` はトランスポートを返し、`Client(transport)` はそれを他のものと同じように受け取ります。
|
||||
|
||||
TLS について 1 点。`httpx2` は、同梱の CA リストではなく、オペレーティングシステムのトラストストアに対して証明書を検証します([`truststore`](https://pypi.org/project/truststore/) を使用)。利用できるシステム CA ストアがない環境(一部の最小構成コンテナなど)では、標準の環境変数 `SSL_CERT_FILE`/`SSL_CERT_DIR` を設定するか、`httpx2.AsyncClient` に明示的に `verify=ssl_context` を渡してください(背景は [`httpx` と `httpx-sse` の `httpx2` への置き換え](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)を参照)。
|
||||
|
||||
!!! warning
|
||||
`streamable_http_client` は以前、`headers=` と `timeout=` を直接受け取っていました。今はもう受け取りません。パラメーターは `url`、`http_client`、`terminate_on_close` だけです。習慣で `headers=` を渡すと、次のようになります。
|
||||
|
||||
```text
|
||||
TypeError: streamable_http_client() got an unexpected keyword argument 'headers'
|
||||
```
|
||||
|
||||
HTTP に関わるものはすべて、渡す 1 つの `httpx2.AsyncClient` に集約されています。
|
||||
|
||||
!!! info
|
||||
`httpx2` はおなじみの `httpx` の API をそのまま保っているので、`httpx` を知っていれば、認証、プロキシ、イベントフック、リトライ、接続数の制限のやり方はすでに知っていることになります。SDK はその上に何も足さず、何も引きません。OAuth が差し込まれるのもここです。`httpx2.AsyncClient(auth=OAuthClientProvider(...))` のように書きます。そのフロー全体については **[OAuth クライアント](oauth-clients.md)** を参照してください。
|
||||
|
||||
## stdio {#stdio}
|
||||
|
||||
**stdio** サーバーはサブプロセスです。クライアントがそれを起動し、stdin に JSON-RPC を書き込み、stdout から JSON-RPC を読み取ります。デスクトップのホストが手元のマシンでサーバーを動かす方法がこれです。ホストとは、このコードに UI を加えたもの「そのもの」です。**[本物のホストに接続する](../get-started/real-host.md)** は、同じ関係をホストの側から設定ファイルとして見たものです。
|
||||
|
||||
`StdioServerParameters` でプロセスを記述し、`stdio_client` でトランスポートに変換して、「それ」を `Client` に渡します。
|
||||
|
||||
```python title="client.py" hl_lines="4-8 12"
|
||||
--8<-- "docs_src/client_transports/tutorial004.py"
|
||||
```
|
||||
|
||||
`Client` はパラメーターオブジェクトをそのままでは受け取りません。`StdioServerParameters` は設定であり、`stdio_client(server)` はそこからプロセスを起動する方法を知っているトランスポートです。必ず包んでください。
|
||||
|
||||
`async with` ブロックを抜けると、サブプロセスも終了されます。stdin を閉じ、待機し、居残っていれば強制終了します。自分で後始末をすることはありません。
|
||||
|
||||
!!! warning
|
||||
子プロセスは環境変数を継承**しません**。最小限の許可リスト(POSIX では `HOME`、`LOGNAME`、`PATH`、`SHELL`、`TERM`、`USER`)だけを受け取るので、自分が書いたとは限らないプロセスに機密情報が漏れることはありません。
|
||||
|
||||
API キーを必要とするサーバーは、そこでキーを見つけられません。`env=` で明示的に渡してください。それらの変数は許可リストの上にマージされます。上の例で `BOOKSHOP_API_KEY` がしているのがまさにそれです。
|
||||
|
||||
## SSE {#sse}
|
||||
|
||||
`mcp.client.sse` の `sse_client(url)` は、Streamable HTTP に取って代わられた HTTP トランスポートです。まだこれを話すサーバーと対話するには、同じように `Client(sse_client("http://localhost:8000/sse"))` と包みます。そして、新しいものをこの上に作らないでください。
|
||||
|
||||
## `Transport` プロトコル {#the-transport-protocol}
|
||||
|
||||
`Client` から見れば、上記はすべて同じものです。
|
||||
|
||||
**トランスポート**とは、`(read, write)` というメッセージストリームのペアを yield する非同期コンテキストマネージャーのことです。正式には `mcp.client` の `Transport` プロトコルです。`Client` は引数を型で解決します。サーバーオブジェクトならインプロセスで接続し、`str` なら `streamable_http_client(url)` になり、それ以外は直接トランスポートとして入ります。この最後の規則があるからこそ、`stdio_client(...)`、`streamable_http_client(...)`、`sse_client(...)` はすべて同じ場所に収まり、自分で独自のものを書くこともできます。
|
||||
|
||||
## まとめ {#recap}
|
||||
|
||||
* `Client(mcp)`(サーバーオブジェクト)はインメモリで接続します。テストと組み込みに使ってください。
|
||||
* `Client("http://.../mcp")`(URL)は、本番用のトランスポートである Streamable HTTP で接続します。
|
||||
* ヘッダー、認証、プロキシ、タイムアウトは、`streamable_http_client(url, http_client=...)` に渡す `httpx2.AsyncClient` に設定します。`headers=` キーワードはありません。
|
||||
* stdio は `Client(stdio_client(StdioServerParameters(...)))` であり、パラメーターオブジェクト単体では決してありません。
|
||||
* サブプロセスが受け取るのは自分の環境ではなく、許可リストに基づく環境です。`env=` でそこに追加します。
|
||||
* トランスポートとは、`async with x as (read, write)` と書けるものすべてです。`Client` は、サーバーオブジェクトでも URL でもないものをそのままこのプロトコルに渡します。
|
||||
* `Client` の構築でトランスポートが選ばれ、`async with` でそれが開かれます。
|
||||
|
||||
トランスポートが開いたら、両者はプロトコルバージョンについて合意する必要があります。普段は意識することはありません。意識することになったら、**[プロトコルバージョン](../protocol-versions.md)** のページを参照してください。
|
||||
Loading…
Reference in a new issue