docs: publish translated docs in twelve languages and the tool that maintains them (#3280)

This commit is contained in:
Max 2026-08-14 17:07:34 +01:00 • committed by GitHub
commit 52ad0a8876
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 89737 additions and 54 deletions

View file

@ -0,0 +1,128 @@
---
translation:
sections: [b50152f05c81e786, b302059b22fb7cb4, 85682a1bf561243a, 53fc48838eb6837a, b24190e0842786ec, 85f93e150fc9b240]
tool: 1
---
# Context {#the-context}
ツールの引数はモデルから渡されます。それ以外のすべて(処理中のリクエスト、ツールが属するサーバー、クライアントに話しかける手段)は、1 つのオブジェクトから得られます。それが **`Context`** です。
自分で組み立てる必要も、設定する必要もありません。要求するだけです。
## 要求する {#ask-for-it}
任意のツールに、`Context` で注釈したパラメーターを追加してください。
```python title="server.py" hl_lines="2 8"
--8<-- "docs_src/context/tutorial001.py"
```
* SDK はリクエストごとに新しい `Context` を組み立てて渡します。
* パラメーターの**名前は関係ありません**。`ctx`、`context`、`c` のどれでもよく、SDK は注釈を見て見つけます。
* リソースやプロンプトでも、同じように宣言できます。
* `ctx.request_id` は、関数がいま処理しているリクエストの id です。
!!! info
FastAPI を使ったことがあれば、この仕組みには見覚えがあるはずです。フレームワーク自身の型(あちらでは `Request`、こちらでは `Context`)でパラメーターを宣言すると、フレームワークがそれを供給します。登録するものも設定するものもありません。型注釈がこの仕組みのすべてです。
### モデルからは見えない {#invisible-to-the-model}
ここはしっかり身につけておきたい部分です。`tools/list` が `search_books` について報告する入力スキーマは次のとおりです。
```json
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"}
},
"required": ["query"],
"title": "search_booksArguments"
}
```
プロパティは 1 つです。`ctx` は引数ではありません。スキーマには決して現れず、モデルに知らされることもなく、どのクライアントも値を入れられません。これは作成者と SDK の間の取り決めであり、通信上には現れません。
### 試してみる {#try-it}
MCP Inspector でサーバーを実行してください。
```console
uv run mcp dev server.py
```
`search_books` のフォームには `query` フィールドが 1 つだけあります。`dune` を指定して呼び出してください。
```text
[request 3] Found 3 books matching 'dune'.
```
この数字は、たまたまそのときのリクエストの番号です。もう一度ツールを呼び出すと変わります。リクエストごとに専用の `Context` が作られるからです。
## 何が得られるか {#what-it-gives-you}
注入されるオブジェクトは小さなものです。`request_id` のほかに次のものがあります。
* `await ctx.read_resource(uri)`:ツールの中からサーバー**自身の**リソースを 1 つ読みます。次のセクションで扱います。
* `await ctx.report_progress(progress, total, message)`:長い呼び出しの最中に、進捗を呼び出し側へ逐次送ります。詳しくは **[進捗](progress.md)** を参照してください。
* `await ctx.elicit(message, schema)` と `await ctx.elicit_url(...)`:ツールを一時停止してユーザーに質問します。これが **[エリシテーション(elicitation)](elicitation.md)** です。
* `ctx.session`:このクライアントとの会話のサーバー側です。クライアントに送る通知はここにあり、最後のセクションで使います。
* `ctx.headers`:トランスポートが運んだリクエストヘッダー、stdio では `None` です。カスタムヘッダーは `(ctx.headers or {}).get("x-...")` で読めます。ヘッダーはクライアントが与える入力です。ロケールや機能フラグには使えますが、身元の確認には決して使わないでください。
* `ctx.request_context`:リクエストごとの生のレコードです。実際に手を伸ばすフィールドは `lifespan_context`、つまり起動コードが yield したオブジェクトです(**[ライフスパン](lifespan.md)** を参照)。
ロギングは意図的にこの一覧に入れていません。サーバーは、ほかの Python プログラムと同じく Python の `logging` モジュールでログを記録します。その理由は短いページ **[ロギング](logging.md)** にまとめてあります。
!!! tip
注入が行われるのは登録した関数だけです。ツールが呼び出すヘルパーに専用の `Context` は渡されないので、`ctx` を通常の引数として渡してください。どこか別の場所から取り出せる暗黙の「現在のコンテキスト」はありません。
## 自分のリソースを読む {#read-your-own-resources}
サーバーのリソースはクライアントだけのものではありません。ツールからも読めます。
```python title="server.py" hl_lines="16"
--8<-- "docs_src/context/tutorial002.py"
```
`ctx.read_resource` は `resources/read` を処理するのと同じレジストリを通して URI を解決するので、ツールはクライアントが受け取るのと同じものを得ます。コンテンツブロックごとに 1 つの `ReadResourceContents` を持つイテラブルです。この URI の場合は 1 つです。
```python
contents.content # 'fiction, non-fiction, poetry'
contents.mime_type # 'text/plain'
```
* `content` は `genres()` が返したものそのままです。情報源は 1 つです。クライアントはリソースを閲覧し、ツールはそれを消費し、誰も文字列をコピーしません。
* `describe_catalog` の唯一のパラメーターは `Context` なので、その入力スキーマには**プロパティが 1 つもありません**。モデルは `{}` で呼び出します。
## 一覧が変わったことをクライアントに伝える {#tell-the-client-the-list-changed}
サーバーが提供するものは、インポート時に固定されるわけではありません。実行時にツールを登録し、それをクライアントに伝えます。
```python title="server.py" hl_lines="15-16"
--8<-- "docs_src/context/tutorial003.py"
```
* `mcp.add_tool(recommend_book)` は普通の関数をツールとして登録します。名前、説明、スキーマは `@mcp.tool()` を使った場合とまったく同じように導出されます。
* `await ctx.session.send_tool_list_changed()` は `notifications/tools/list_changed` を送ります。これを受け取ったクライアントは `tools/list` を再度呼び出し、`recommend_book` を目にします。
同種のメソッドには `send_resource_list_changed()`、`send_prompt_list_changed()`、そして特定の 1 つのリソースの変更を知らせる `send_resource_updated(uri)` があります。
2026-07-28 の接続では、クライアントは自分が開いた `subscriptions/listen` ストリーム上でしか変更通知を受け取らないため、上記の `send_*` メソッドはそれらのストリームに届きません。`Context` の公開メソッドは、購読中のすべてのストリームに一度に配信します。`await ctx.notify_tools_changed()`、`await ctx.notify_prompts_changed()`、`await ctx.notify_resources_changed()`、`await ctx.notify_resource_updated(uri)` です。レプリカをまたいだスケールアウトも含め、詳しくは **[サブスクリプション](subscriptions.md)** を参照してください。
!!! check
誰かが `enable_recommendations` を実行するまで、約束しているツールは存在しません。それでも呼び出すと、結果はモデルが読めるエラーです。
```text
Unknown tool: recommend_book
```
`enable_recommendations` を実行すると、まったく同じ呼び出しが成功します。ツールの一覧は本当に動的です。`tools/list` は「いま」登録されているものをそのまま反映します。
## まとめ {#recap}
* パラメーターに `Context` を注釈すると(ツールでも、リソースでも、プロンプトでも)、SDK がそれを注入します。名前は自由です。
* モデルからは見えません。入力スキーマに含まれるのは、常に本物の引数だけです。
* `ctx.request_id` はリクエストを識別し、`ctx.request_context.lifespan_context` は起動時に yield したものです。
* `await ctx.read_resource(uri)` を使うと、ツールからサーバー自身のリソースを読めます。
* `ctx.session` はクライアントへ戻るチャネルです。`send_tool_list_changed()` とその同種のメソッドは、変更した一覧を取得し直すようクライアントに伝えます。
* 進捗の報告とエリシテーションも `Context` が出発点です。それぞれに専用のページがあります。
モデルが目にすることのない、自分の関数で埋めるパラメーターが **[依存関係](dependencies.md)** です。

View file

@ -0,0 +1,137 @@
---
translation:
sections: [b0389403e98d25ad, e2cf58b43b285e86, a363e1a38e1a5971, 6cfac078feb18013, b4535bd61df337e6, e97ed44207f929fd]
tool: 1
---
# 依存関係 {#dependencies}
ツールの引数はモデルから渡されます。しかし、モデルから渡されるべきではない値もあります。記録から調べた価格、人間にしか出せない確認、モデルがでっち上げると間違えかねないあらゆる値です。
**依存関係**とは、自分の関数で埋めるパラメーターです。パラメーターに注釈を付けて関数を指定すると、ツールが実行される前に SDK がその関数を呼び出します。
## 宣言する {#declare-one}
パラメーターの型を `Annotated[...]` で包み、`Resolve(fn)` を追加します。
```python title="server.py" hl_lines="18-19 23"
--8<-- "docs_src/dependencies/tutorial001.py"
```
* `check_stock` は**リゾルバー**です。SDK が `reserve_book` の前に実行する普通の関数で、その戻り値が `stock` 引数になります。
* その `title` パラメーターはツール自身の `title` 引数で、**名前で**照合されます。リゾルバーが受け取るのは、ツール本体が受け取るのとまったく同じ、検証済みの値です。
* ツール本体は、すでに存在する `Stock` から始まります。ツールの中に在庫を調べるコードはなく、「見つからなかったら」という前置きもありません。
!!! info
FastAPI を使ったことがあれば、これは `Depends` です。同じ仕組みで、同じ理由です。関数が必要なものを宣言し、フレームワークがそれを供給し、配線は型注釈の中にあります。
### モデルからは見えない {#invisible-to-the-model}
`tools/list` が `reserve_book` について報告する入力スキーマは次のとおりです。
```json
{
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"}
},
"required": ["title"],
"title": "reserve_bookArguments"
}
```
プロパティは 1 つです。**[Context](context.md)** の `Context` と同じく、解決されるパラメーターは自分と SDK の間の取り決めです。`stock` はスキーマに含まれず、モデルには一切知らされません。それでも `stock` の値を送ってくるクライアントがあっても、その値は無視されます。ツールが受け取れるのはリゾルバーの値だけです。
肝心なのは最後の部分です。モデルが渡せないパラメーターは、モデルが間違えようのないパラメーターです。
### 試してみる {#try-it}
MCP Inspector でサーバーを実行します。
```console
uv run mcp dev server.py
```
`reserve_book` のフォームには `title` フィールドが 1 つあるだけです。`stock` はどこにもありません。`Dune` で呼び出してみてください。
```text
Reserved 'Dune' (6 copies left).
```
ツール本体は何も調べていません。先に `check_stock` が実行され、それが返した `Stock` が引数として届きました。`Neuromancer` を試すと、同じリゾルバーがツールにゼロを渡します。
!!! tip
ツール本体で `check_stock(title)` を呼ぶだけでも済みます。依存関係として宣言するのは、その値がヘルパー呼び出し以上の扱いに値するときです。在庫を必要とするツールはどれも同じパラメーターを宣言し、いくつのツールが宣言していても、SDK はリゾルバーを 1 回の呼び出しにつき最大 1 回しか実行しません。残りは次のセクションで扱います。互いに依存するリゾルバーと、ユーザーに質問するリゾルバーです。
## 依存関係の依存関係 {#dependencies-of-dependencies}
リゾルバーは、同じ注釈を使って自分自身の依存関係を宣言できます。
```python title="server.py" hl_lines="22 29-30"
--8<-- "docs_src/dependencies/tutorial002.py"
```
* `estimate_delivery` は `check_stock` に依存しています。SDK はグラフを順番に実行します。まず在庫、次に見積もり、最後にツールです。
* `stock` も `delivery` も最終的には `check_stock` を必要としますが、実行されるのは**1 回の呼び出しにつき 1 回**です。在庫の検索は 1 回、利用側は 2 つです。
* 登録するものは何もありません。グラフは注釈「そのもの」です。
!!! check
「呼び出しごとに 1 回」を鵜呑みにしないでください。`check_stock` に `print` を入れて、Inspector から `order_book` を呼び出してみましょう。呼び出しごとに 1 行です。利用側は 2 つ、検索は 1 回です。
SDK がグラフを解析するのは、ツールが呼び出されたときではなく、登録されたときです。分類できないパラメーター(`Context` でも `Resolve(...)` でもツール引数の名前でもないもの)とリゾルバーの循環は、どちらも起動時に `InvalidSignature` を送出します。サーバーはクライアントが接続する前に失敗し、問題のパラメーターやリゾルバーの名前がエラーに示されます。
リゾルバーのパラメーターは、ツールのパラメーターとまったく同じように解決されます。別の `Resolve(...)`、名前で照合されるツール自身の引数、または `Context` です。`ctx.headers` もライフスパンのオブジェクトも、すべて使えます。
!!! warning
HTTP トランスポートでは、`Context` に `ctx.headers` が含まれます。ヘッダーはツール引数と同じく**クライアントが供給する入力**です。ロケールや機能フラグには問題ありませんが、身元の確認には決して使わないでください。呼び出し側が誰であるかは、誰でも設定できるヘッダーではなく、認可レイヤー(**[認可](../run/authorization.md)**)から得ます。
!!! tip
「呼び出しごとに 1 回」は文字どおりの意味です。次の `tools/call` では `check_stock` が再び実行されます。リクエストより長く生き続けるべきリソース(データベースプールや HTTP クライアントなど)は **[ライフスパン](lifespan.md)** に置くものです。リゾルバーからは `ctx.request_context.lifespan_context` を通じて参照できます。
## 必要なときだけ尋ねる {#ask-when-you-must}
リゾルバーは答えを知っている必要はありません。`Elicit(message, Model)` を返せば、SDK がユーザーに尋ねます。**[エリシテーション(elicitation)](elicitation.md)** の仕組みを、代わりに実行してくれます。
```python title="server.py" hl_lines="26-32 39"
--8<-- "docs_src/dependencies/tutorial003.py"
```
* 在庫がある場合:`confirm_backorder` は `Backorder` を直接返します。**質問もラウンドトリップもありません。**ユーザーの作業を中断するのは、その答えが意味を持つときだけです。
* 在庫がない場合:SDK がエリシテーションを送信し、答えを `Backorder` に照らして検証し、注入します。リゾルバーはプロトコルに一切触れません。
* ツールは `backorder.confirm` をほかの引数と同じように読み取ります。**いいえ**と答えるのも立派な答えです。エリシテーションは `confirm=False` で受理され、ツールは実行され、注文は行われません。尋ねることは、ツール本体の配管ではなく前提条件になりました。
では、ユーザーがまったく答えない場合、つまり質問を辞退したりキャンセルしたりした場合はどうなるでしょうか。
!!! check
`Neuromancer` で `order_book` を実行し、質問を辞退してみてください。注釈を `Annotated[Backorder, Resolve(...)]` と書いた場合、ツール本体は実行されません。呼び出しは、モデルが読めるエラー結果で失敗します。
```text
Error executing tool order_book: Resolver for parameter 'backorder' could not resolve: elicitation was decline
```
前提条件としてはこれが正しいデフォルトです。答えがなければ注文もありません。辞退をツールで扱いたい結果にしたいとき(取り寄せはやめても、別のタイトルを提案したいなど)は、代わりに `ElicitationResult[Backorder]` で注釈を付けてください。ツールは受理・辞退・キャンセルの結果をまるごと受け取り、それに応じて分岐できます。この形式のほか、尋ねることに関するそれ以外のすべて(スキーマの規則、3 つの答え、会話のクライアント側)は **[エリシテーション](elicitation.md)** で説明しています。
!!! info
フレームワークは、ネゴシエートされたプロトコルバージョンから質問のトランスポートを選びます。上のコードはどちらでも同じです。**2026-07-28** 以降では、質問はマルチラウンドトリップ(multi-round-trip)の `tools/call` の中で運ばれます。サーバーが質問を返し、クライアントの `elicitation_callback` がそれに答え、`Client` が呼び出しを再試行してくれます(**[マルチラウンドトリップリクエスト](multi-round-trip.md)**)。**2025-11-25** 以前では、呼び出しの途中で行われる同期的なエリシテーションリクエストです。各質問は 1 回の呼び出しにつきちょうど 1 回だけ尋ねられます。これは質問についての保証であり、リゾルバーについての保証ではありません。マルチラウンドトリップの形式では、質問の後に呼び出しが再開されるたびに、どのリゾルバーも再び実行される可能性があります。そのため、`return Elicit(...)` より前のコードはそれらのラウンドごとに実行されます。記録された答えは、繰り返される質問をユーザーに再度尋ねることなく満たします。記録された答えが参照されるのは、リゾルバーが尋ねたときだけです。`check_stock` のように尋ね**ずに**答えるリゾルバーは、常に自分で計算した値を供給します。それぞれの答えは対応する質問に照合されるので、エリシテーションを行うリゾルバーは、ツールの引数とそれまでの答えから決定論的に質問を導かなければなりません。呼び出しごとに生成される値(`default_factory` の ID やタイムスタンプ)はラウンドごとに導き直されるため、答えを結び付けたい質問の中に含めてはいけません。そうした変わりやすいデータから組み立てた質問は、記録された答えをすべて古く見せてしまいます。その結果、サーバーはクライアントのラウンド上限が呼び出しを終わらせるまで、ラウンドごとに同じ質問を繰り返します。
## ユーザーではなくクライアントに尋ねる {#ask-the-client-not-the-user}
エリシテーションは、リゾルバーが尋ねられる 3 つの質問のうちの 1 つで、マルチラウンドトリップのフローではこれ以外は許されません。残りの 2 つはユーザーではなく**クライアント**に向けられます。`Sample(...)` を返せばクライアントを通じて LLM の呼び出しを実行し(`sampling/createMessage` リクエスト)、`ListRoots()` を返せばクライアントの現在のルート(roots)を取得します。どちらにも受理・辞退という結果はありません。利用側は結果の型を直接注釈に書きます。`CreateMessageResult`(リクエストが `tools` または `tool_choice` を伴う場合は `CreateMessageResultWithTools`)、または `ListRootsResult` です。
```python title="server.py" hl_lines="10-15 21"
--8<-- "docs_src/dependencies/tutorial004.py"
```
* フレームワークはこれらを `Elicit` とまったく同じように振り分けます。**2026-07-28** ではマルチラウンドトリップの `tools/call` の中で、**2025-11-25** では単独のサーバーからクライアントへのリクエストで運ばれます。宣言されていないケイパビリティは、`-32021` のプロトコルエラーで呼び出しを拒否します(`sampling`、`roots`、フォームモードの `elicitation`。リクエストが `tools` または `tool_choice` を伴う場合は `sampling.tools`)。
* 上の info ボックスが質問について述べていることは、すべてそのまま当てはまります。`Sample` リクエストは、その正確な表現によって記録された結果と照合されます。そのため、ツールの引数とそれまでの答えから決定論的に組み立ててください。そうすれば、クライアントが LLM の呼び出しに支払うのはラウンドごとに 1 回ではなく、ツール呼び出しごとに 1 回になります。記録された結果は呼び出しの残りの間 `request_state` に載って運ばれるため、補完が非常に大きいと、残りのラウンドトリップがすべて重くなります。
* 単独のサンプリングとルートの「機能」は、2026-07-28 で非推奨になります(SEP-2577)。クライアントのモデルを必要とする新しいサーバーは、この運び手を通じて尋ねます。必要としないサーバーは、LLM プロバイダーと直接統合してください。`"none"` 以外の `include_context` の値はそれ自体が非推奨です。使わないでください。
## まとめ {#recap}
* ツールのパラメーターに `Annotated[T, Resolve(fn)]` を付けると、SDK が `fn` を実行し、その戻り値を注入します。
* 解決されるパラメーターはモデルからは見えず、クライアントからも渡せません。モデルがでっち上げてはならない値(価格、身元、権限)はここに置きます。
* リゾルバーのパラメーターも同じ方法で解決されます。`Context`、別の `Resolve(...)`、または名前で照合されるツール引数です。グラフは、利用側がいくつあっても、各リゾルバーをラウンドごとに最大 1 回だけ実行します。各質問はちょうど 1 回だけ尋ねられ、質問の後に呼び出しが再開されると、どのリゾルバーも再び実行される可能性があります。
* 不正なグラフは、呼び出しの途中ではなく登録時に `InvalidSignature` で失敗します。
* ユーザーに尋ねるには `Elicit(message, Model)` を返します。ただし、必要なときだけです。包まない注釈は辞退されると中断し、`ElicitationResult[T]` ならツールが分岐できます。
* クライアントに LLM の補完やルートの一覧を尋ねるには、`Sample(...)` または `ListRoots()` を返します。そのままの結果が注入されます。
サーバーが起動時に一度だけ組み立てる状態と、ハンドラーからそこに到達する方法については、**[ライフスパン](lifespan.md)** のページを参照してください。

View file

@ -0,0 +1,175 @@
---
translation:
sections: [335ca2a0b266f003, d1ad562d3fe87bc0, 0bb1396c86daeba4, d1cb1235bb9ee267, 833179c09d239c83, e5d6dec2d2e655e8]
tool: 1
---
# エリシテーション {#elicitation}
処理の途中で答えがひとつ足りないだけのツールは、失敗する必要はありません。
**エリシテーション(elicitation)**を使えば、質問できます。ツール呼び出しの途中でユーザーに質問が届き、その答えが同じ関数呼び出しの中に戻ってきます。
モードは 2 つあります。
* **フォームモード**:値(確認、日付、数量)が必要な場合です。フィールドを記述すると、クライアントがフォームを描画します。
* **URL モード**:ユーザーに別の場所(OAuth の同意画面、決済ページ)へ行ってもらう必要がある場合です。そこでユーザーが行うことは、何ひとつプロトコルを通りません。
そして、質問の仕方も 2 通りあります。まず選ぶべきなのは**リゾルバー**です。質問をパラメーターに結び付けておけば、SDK が質問します。どんな接続でも、クライアントがどのプロトコルの世代を話していても動きます。直接的な方法である `await ctx.elicit(...)` は、サーバーからクライアントへのリクエストです。この経路は、レガシー接続(仕様バージョン 2025-11-25 以前)のクライアントにしか存在しません。このページでは両方を扱いますが、まずはリゾルバーから始めてください。
## リゾルバーで質問する {#ask-with-a-resolver}
ツール全体の実行を左右する質問(「本当によろしいですか」「一致した 3 つのアカウントのうちどれですか」など)は、ツール本体から取り出して**リゾルバー**に移せます。そうすれば、フレームワークが代わりに質問してくれます。
`Annotated[T, Resolve(fn)]` と注釈したパラメーターには、ツール本体の前に `fn` を実行した結果が入ります。リゾルバーは、値がすでに分かっていればそのまま返し、フレームワークに質問させたいときは `Elicit(...)` を返します。
```python title="server.py" hl_lines="24-30 35-36"
--8<-- "docs_src/elicitation/tutorial004.py"
```
* `confirm_delete` はツール自身の `path` 引数を名前で読み取り、フォルダーの中身を一覧し、**必要なときにだけ質問します**。空のフォルダーなら、クライアントとの往復なしで `Confirm(ok=True)` に解決されます。
* `delete_folder` は `ElicitationResult[Confirm]` と注釈しているので、フレームワークは結果全体を注入し、ツールはすべての場合を `match` で分岐します。承諾して確認、承諾したが削除しない(`ok=False`)、拒否、キャンセルです。
* `confirm` パラメーターはツールの入力スキーマには決して現れません。クライアントが `path` を渡し、リゾルバーが `confirm` を渡します。
ツールが分岐する必要がないなら、代わりにラップしていないモデル(`Annotated[Confirm, Resolve(confirm_delete)]`)で注釈してください。承諾ならツールはモデルを受け取り、拒否やキャンセルなら呼び出しはエラーで中断されます。
リゾルバーは**すべての**接続で動きます。レガシー接続のクライアントには、SDK が質問を直接送ります。**2026-07-28** の接続では、SDK が呼び出しから質問を「返し」、クライアントの次の試行が答えを運んできます。リゾルバーがその違いを知ることはありません。その裏で何が起きているかは **[マルチラウンドトリップリクエスト(multi-round-trip requests)](multi-round-trip.md)** で説明しています。
質問は、リゾルバーにできることのひとつにすぎません。質問せずに計算する依存関係、依存関係の依存関係、モデルが渡せるものと渡せないものといった仕組み全般は、**[依存関係](dependencies.md)** のページで説明しています。
## ツールの中から質問する {#ask-from-inside-the-tool}
ツールは、自分の本体の途中で止まって質問することもできます。
!!! warning
`ctx.elicit()` と `ctx.elicit_url()` はサーバーからクライアントへのリクエストです。この経路は、レガシー接続(仕様バージョン **2025-11-25** 以前)のクライアントにしか存在しません。**2026-07-28** の接続にはサーバー起点のリクエストがないため、これらの呼び出しは失敗します。リゾルバーはどちらでも動きます。詳しくは **[プロトコルバージョン](../protocol-versions.md)** を参照してください。
`await ctx.elicit()` はメッセージと Pydantic モデルを受け取ります。
```python title="server.py" hl_lines="9-11 20-23 25"
--8<-- "docs_src/elicitation/tutorial001.py"
```
* **`Context`** パラメーターがあるからこそ `ctx.elicit` が使えます。どのツールでも受け取れます。このオブジェクトについては専用のページ **[Context](context.md)** があります。
* `AlternativeDate` は、欲しい答えの**スキーマ**です。
* ツールは `async def` です。そうでなければなりません。途中で止まって人を待つからです。
* ほかの日付なら、ツールはすぐに返ります。質問するのは必要なときだけです。
* ユーザーが承諾した日付は、`book_table` 自身をもう一度通ります。答えも、ほかの入力と同じ入力です。代わりの日付も満席なら、やみくもに確定するのではなく、もう一度質問します。
### クライアントが受け取るもの {#what-the-client-receives}
クライアントは、メッセージと一緒に、モデルから生成された JSON Schema を受け取ります。
```json
{
"properties": {
"accept_alternative": {
"description": "Try another date?",
"title": "Accept Alternative",
"type": "boolean"
},
"date": {
"default": "2025-12-26",
"description": "Alternative date (YYYY-MM-DD)",
"title": "Date",
"type": "string"
}
},
"required": ["accept_alternative"],
"title": "AlternativeDate",
"type": "object"
}
```
このスキーマがフォームです。`Field(description=...)` がラベルになり、デフォルト値は入力欄にあらかじめ入って、そのフィールドを省略可能にします。これは、**[ツール](../servers/tools.md)** のページがツールの引数について説明しているのと同じ、Pydantic から JSON Schema への変換の仕組みです。
!!! warning
エリシテーションのスキーマは、ツールの入力スキーマほど表現力がありません。フラットなプリミティブ型のフィールドだけです。`str`、`int`、`float`、`bool`、または文字列の `Literal`(`enum` になります)。モデルの中にモデルを入れると、クライアントに何かを送る前に `ctx.elicit` が例外を送出します。
```text
TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition
```
人の作業を中断して質問しているのです。答えに入れ子が必要なら、それはツールの引数にすべきでした。
### 3 つの答え {#the-three-answers}
`result.action` を見れば、ユーザーが何をしたかが分かります。可能性はちょうど 3 つです。
* `"accept"`:フォームを送信しました。`result.data` は検証済みの `AlternativeDate` インスタンスです。
* `"decline"`:断りました。
* `"cancel"`:選ばずに質問を閉じました。
`result.data` は `"accept"` のときにしか存在しません。だからこそ、この例では先に `result.action` を確認しています。この順序は型チェッカーが強制します。`result.action == "accept"` の後では `result.data` は `AlternativeDate` ですが、その前には `.data` 自体がありません。
断られてもエラーではありません。断られたことが何を意味するか(ここでは、予約しないこと)はツールが決め、モデルには普通に答えます。
!!! tip
答えは、コードに届く前にモデルに照らして検証されます。`bool` に `"maybe"` を送ってくるクライアントがいても、予約が壊れることはありません。呼び出しはスキーマ不一致のエラーで失敗し、`if` は実行されません。
## ユーザーを URL へ誘導する {#send-the-user-to-a-url}
認証情報、カード番号、OAuth の同意など、モデルやクライアントを通してはならないものがあります。そうしたものについては、データを求めるのではなく、ユーザーにどこかへ行ってもらうよう頼みます。
```python title="server.py" hl_lines="10-14 23"
--8<-- "docs_src/elicitation/tutorial002.py"
```
* `ctx.elicit_url()` は、メッセージ、開いてもらう **URL**、そして自分で決める `elicitation_id` を受け取ります。これは、サーバー内でこのエリシテーションを識別できる任意の文字列です。
* 結果にあるのはアクションだけです。`"accept"` はユーザーが URL を開くことに同意したという意味で、向こう側でやるべきことを終えたという意味**ではありません**。
* 決済は、ユーザーのブラウザーと決済プロバイダーの間で、帯域外で行われます。MCP を通って戻ってくる内容は一切ありません。
2 つ目のツールを見てください。帯域外のフローが終わったことをサーバーが知ったとき(webhook やポーリング。ここでは 2 つ目のツールとしてモデル化しています)、`ctx.session.send_elicit_complete(...)` が同じ `elicitation_id` を付けて `notifications/elicitation/complete` を送ります。これによってクライアントは、「waiting for payment...」の表示をやめてよいと分かります。これがなければ、クライアントは推測するしかありません。
## クライアント側 {#the-client-side}
質問するのはサーバーです。クライアントは、`Client(...)` に **`elicitation_callback`** を渡すことで答えます。
```python title="client.py" hl_lines="6-7 18"
--8<-- "docs_src/elicitation/tutorial003.py"
```
* 1 つのコールバックで両方のモードを扱います。`params` は `ElicitRequestFormParams` と `ElicitRequestURLParams` のユニオンで、`isinstance` で分岐します。
* URL の場合は、`params.url` をユーザーに見せ、ユーザーが選んだアクションを返します。`content` は決して返しません。
* フォームの場合、本物のアプリケーションなら `params.requested_schema` を描画し、ユーザーの入力を `content` として返します。このコールバックは決まった答えで常に「はい」と答えます。これはまさに、テストで欲しいコールバックです。
* コールバックを渡すことは、**ケイパビリティの宣言**でもあります。これによってサーバーは、このクライアントに質問できることを知ります。クライアントがサーバーの代わりに答えられるほかのことは、**[クライアントのコールバック](../client/callbacks.md)** にまとまっています。
!!! info
エリシテーションはサーバーからクライアントへのリクエストであり、それは従来のハンドシェイクによるセッションにしか存在しません。このクライアントが `mode="legacy"` を渡しているのはそのためです。**2026-07-28** の接続では、ツールは代わりに呼び出しから質問を「返す」ことで質問します。その流れは **[マルチラウンドトリップリクエスト](multi-round-trip.md)** で説明しています。
### 試してみる {#try-it}
`ctx.elicit` を使うフォームモードの `server.py`(`book_table` のほう)を Streamable HTTP で起動し(1 行で起動するコマンドは **[サーバーの実行](../run/index.md)** にあります)、クライアントの `main()` を実行して、`book_table` にクリスマス当日の予約を頼んでください。
コールバックは、送られてきた質問を表示します。
```text
No tables for 2 on 2025-12-25. Would you like to try another date?
```
コールバックは `{"accept_alternative": True, "date": "2025-12-27"}` と答え、その間ずっと `await ctx.elicit(...)` の中で待っていたツールが予約を完了します。
```text
Booked a table for 2 on 2025-12-27.
```
今度は URL モードの `server.py` に差し替え、同じ `main()` を `pay_deposit` に向けてください。同じコールバックがもう一方の分岐を通り、決済リンクを表示し、ツールは「Complete the payment in your browser.」と返してきます。呼び出しの途中で、双方向に 1 往復です。
!!! check
今度は `Client` から `elicitation_callback=` を取り除き、もう一度クリスマス当日で `book_table` を呼び出してください。呼び出し全体がプロトコルエラーで失敗します。
```text
Elicitation not supported
```
コールバックを登録しなかったクライアントは `elicitation` ケイパビリティを宣言していないので、質問する相手がいません。ツールが受け取ったのは `"decline"` ではなく、例外です。これに備えて設計してください。どのエリシテーションにも、「質問できなかったらどうするか」に対する妥当な答えが必要です。
## まとめ {#recap}
* `Annotated[T, Resolve(fn)]` と注釈したパラメーターはリゾルバーが埋め、リゾルバーは質問が必要なときに `Elicit(...)` を返します。すべての接続で動きます。
* スキーマはフラットな Pydantic モデルです。プリミティブ型のフィールドだけで、戻ってくるときに検証されます。
* `result.action` は `"accept"`、`"decline"`、`"cancel"` のいずれかで、`result.data` は承諾のときにだけ存在します。
* `await ctx.elicit(message, schema=Model)` はツール本体の中から質問し、`await ctx.elicit_url(message, url, elicitation_id)` はモデルを通してはならないもののためにあります(帯域外の部分が終わったことは `ctx.session.send_elicit_complete(elicitation_id)` で伝えます)。どちらもサーバーからクライアントへのリクエストなので、クライアントがレガシー接続である必要があります。
* クライアントは 1 つの `elicitation_callback` で答え、params の型で分岐します。これを登録することがケイパビリティの宣言になります。
* 2026-07-28 の接続では、サーバーは質問をプッシュする代わりに返します。同じコールバックに質問を届けるのは **[マルチラウンドトリップリクエスト](multi-round-trip.md)** です。
その「返す」仕組みの裏側(再試行ループ、`requestState` の保護、自分で駆動する方法)は、すべて **[マルチラウンドトリップリクエスト](multi-round-trip.md)** で説明しています。

View file

@ -0,0 +1,24 @@
---
translation:
sections: [424930166c4bc6f3]
tool: 1
---
# ハンドラーの中で {#inside-your-handler}
ハンドラーの引数はクライアントから渡されます。それ以外にハンドラーが読み取れるものと、実行中にできることは、すべてここにまとめてあります。
読み取れるもの:
* **[Context](context.md)** は、どのハンドラーでも要求できる唯一の追加パラメーターです。処理中のリクエスト、そのヘッダー、セッション、そして進捗通知と変更通知のための操作を提供します。
* **[依存関係](dependencies.md)** は、モデルからは決して見えないパラメーターです。`Resolve` を使って自分の関数で値を埋めます。
* **[ライフスパン](lifespan.md)** では、サーバーが起動時に一度だけ構築する状態と、ハンドラーが `Context` を通じてそれにアクセスする方法を扱います。
実行中にできること:
* **[エリシテーション(elicitation)](elicitation.md)** と、それを運ぶ 2026-07-28 のパターンである **[マルチラウンドトリップ(multi-round-trip)リクエスト](multi-round-trip.md)** を使って、ユーザーに追加の入力を求めます。
* **[サンプリングとルート(roots)](sampling-and-roots.md)** を使って、クライアントに LLM の補完やワークスペースのフォルダーを要求します。非推奨ですが、引き続き提供されています。
* 時間のかかる処理について **[進捗](progress.md)** を報告します。
* **[ロギング](logging.md)** でログを書き出します(サーバーを運用する人に向けて、標準エラーに出力します)。
* **[サブスクリプション](subscriptions.md)** で、購読中のクライアントに変更があったことを伝えます。
まだハンドラーを登録していない場合は、**[ツール](../servers/tools.md)** から始めてください。ここにあるページはどれも、ハンドラーが 1 つあることを前提にしています。

View file

@ -0,0 +1,101 @@
---
translation:
sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53]
tool: 1
---
# ライフスパン {#lifespan}
実際のサーバーの多くは、動いている間ずっと何かを保持しています。データベースのプール、HTTP クライアント、読み込んだモデルなどです。
それを呼び出しのたびに組み立てたくはありませんし、終了時にはきれいに閉じたいはずです。そのためにあるのが**ライフスパン**です。
## 型付きのライフスパン {#a-typed-lifespan}
ライフスパンは、サーバーを受け取って**オブジェクトを 1 つ** `yield` する `@asynccontextmanager` です。yield したものは、サーバーが動いている限りすべてのハンドラーから利用できます。
```python title="server.py" hl_lines="25-31 34 38 40"
--8<-- "docs_src/lifespan/tutorial001.py"
```
下から順に読んでいきます。
* `app_lifespan` は `yield` の**前**で `Database` に接続し、その**後**、`finally` の中で切断します。これが起動と終了の処理です。
* yield するのは `AppContext` です。セットアップしたものを保持するだけの素朴な dataclass です。今日はフィールドが 1 つでも、明日は 10 個になるかもしれません。
* つなぎ込みは `MCPServer("Bookshop", lifespan=app_lifespan)` だけで完了します。
* ツールの中では、yield したオブジェクトは `ctx.request_context.lifespan_context` として取り出せます。
ライフスパンは **1 回だけ**実行されます。サーバーの起動時(最初のリクエストより前)に入り、サーバーの停止時に抜けます。その間のすべてのリクエストが同じ `AppContext` を共有します。
!!! info
FastAPI の `lifespan` を書いたことがあれば、すでに知っている内容です。同じデコレーター、同じ `yield`、同じ `finally` です。
### モデルから見えるもの {#what-the-model-sees}
新しいものは何もありません。`ctx` は **Context** パラメーターなので、SDK が注入し、入力スキーマには決して現れません。
```json
{
"type": "object",
"properties": {
"genre": {"title": "Genre", "type": "string"}
},
"required": ["genre"],
"title": "count_booksArguments"
}
```
モデルが渡せる引数は `genre` だけです。ライフスパンはサーバー側の内部事情です。
`@mcp.resource()` と `@mcp.prompt()` の関数も `ctx` パラメーターを受け取れます。ただし型は裸の `Context` と書きます。理由は次の節で説明します。`ctx` が持っているものはすべて **[Context](context.md)** にまとめてあります。
### 本当に型が付いている {#it-really-is-typed}
もう一度アノテーションを見てください。`ctx: Context[AppContext]` です。
この型パラメーター 1 つがあるからこそ、型チェッカーにとって `ctx.request_context.lifespan_context` は `AppContext` **そのもの**になります。`.db` は自動補完され、`.dbb` はサーバーを動かす前からエラーになります。
代わりに裸の `Context` と書くと、`lifespan_context` の型は `dict[str, Any]` になります。ライフスパンが何を yield したのか、型チェッカーには知りようがないからです。実行時にはオブジェクトはそこにありますが、型による補助は失われます。
!!! warning
`Context[AppContext]` は**ツール専用**の書き方です。`@mcp.resource()` や `@mcp.prompt()` の関数に付けると、そのハンドラーの呼び出しはすべて失敗します。クライアントにはエラーが返り、サーバーのログには理由が記録されます。
```text
Context is not available outside of a request
```
リソースとプロンプトでは、裸の `ctx: Context` と書いてください。ライフスパンが yield したオブジェクトは、実行時には引き続き `ctx.request_context.lifespan_context` にあります。手放すのは型パラメーターであって、オブジェクトではありません。
!!! tip
ライフスパンは必ず存在します。渡さなければ SDK のデフォルトが空の `dict` を yield するので、`ctx.request_context.lifespan_context` は `{}` であり、`None` になることはありません。裸の `Context` で型が `dict[str, Any]` になるのも、このデフォルトがあるためです。
## 実際に動かして確かめる {#watch-it-happen}
「起動処理は最初のリクエストより前に走る」というのは、言われたまま信じるべき類の話ではありません。
サーバーをライフサイクルだけに絞り込みましょう。`Database` に `connected` フラグを持たせ、`connect()` と `disconnect()` でそれを切り替え、その状態を報告するツールを追加します。
```python title="server.py" hl_lines="11 14 17 25 44"
--8<-- "docs_src/lifespan/tutorial002.py"
```
`database` をモジュールレベルに置いている理由は 1 つだけです。サーバーの「外側」から覗けるようにするためです。
!!! check
3 つの時点で、3 つの値になります。
* サーバーの起動前、`database.connected` は `False` です。モジュールをインポートしただけでは何も接続されていません。
* 動いている間に `database_status` を呼び出すと、結果は `"connected"` です。
* サーバーを止めると `finally` ブロックが走り、`database.connected` は再び `False` になります。
処理は置いた場所でちょうど実行されました。`yield` の前後であって、インポート時でもリクエストごとでもありません。
## まとめ {#recap}
* `lifespan=` には、サーバーを受け取ってオブジェクトを 1 つ `yield` する `@asynccontextmanager` を渡します。
* `yield` の前のコードが起動処理です。その後の `finally` が終了処理です。
* 実行は 1 回だけで、サーバーの一生全体を囲みます。リクエストごとではありません。
* `yield` したものは、すべてのツール、リソース、プロンプトで `ctx.request_context.lifespan_context` として使えます。
* `ctx: Context[AppContext]` と書けば、ツールではそのアクセスに完全に型が付きます。リソースとプロンプトでは裸の `Context` を使います。
* `lifespan=` を渡さなければ空の `dict` です。`None` になることはありません。
呼び出しの途中で止まり、本人にしかわからないことをユーザーに尋ねるハンドラーについては、**[エリシテーション(elicitation)](elicitation.md)** を参照してください。

View file

@ -0,0 +1,79 @@
---
translation:
sections: [c93a3e1aefd77955, 7851abd5ec54393b, f49d1ca2f330f9cd, c03764bd9dfeef7b, 4a0391691a674ae4, 2df5cd279eabf9f5]
tool: 1
---
# ロギング {#logging}
ツールからのログ出力は、他のどの Python 関数でも同じやり方です。標準ライブラリを使います。
MCP にはプロトコルレベルの**ロギングのケイパビリティ**があります。サーバーは `Context` オブジェクトのメソッドを通じて、ログメッセージを通知としてクライアントへ送り出せました。仕様の 2026-07-28 版では**このケイパビリティが非推奨となり、代わりのものは用意されていません**。そのため、このドキュメントでは扱いません。非推奨になったものと、代わりにどうすればよいかの一覧は、**[非推奨の機能](../deprecated.md)**にあります。
代わりにやることは、他のどの Python プログラムでもやっていることと同じです。標準ライブラリを使います。
## ログを出すツール {#a-tool-that-logs}
```python title="server.py" hl_lines="1 5 13"
--8<-- "docs_src/logging/tutorial001.py"
```
* `logging.getLogger(__name__)` は、モジュール名にちなんだ名前のロガーを返します。冒頭で一度だけ作成してください。
* ツールの中では、他の関数と同じように `logger.info(...)` を呼び出します。注入するものも、`await` するものも、MCP 固有のものも何もありません。
!!! check
ツールを呼び出して、結果全体を見てみましょう。
```python
result.content # [TextContent(text="Found 3 books matching 'dune'.")]
result.structured_content # {'result': "Found 3 books matching 'dune'."}
```
ログの行はどこにもありません。ロギングは**サーバーを運用する人**のためのものです。モデルがそれを見ることはありません。モデルに何かを読ませたいなら、`return` してください。
## 出力先 {#where-it-goes}
**stdio** サーバーでは、この問いがいつも以上に重要です。ホストはサーバーをサブプロセスとして起動し、その **stdout** から MCP メッセージを読み取っています。標準エラーは自由に使えます。
標準ライブラリは最初から正しく動作します。ログ出力はデフォルトで `sys.stderr` に送られます。`logger.info(...)` の行はターミナル(またはホストがサブプロセスの stderr を集める場所)に届き、プロトコルのストリームはきれいなまま保たれます。
!!! tip
stdio サーバーで `print()` を使わないでください。`print` は **stdout** に書き込みますが、stdout はプロトコルのものです。サーバーの稼働中、SDK は実際に「フラッシュされた」stdout を stderr へ振り向けるので、通信路を壊すことはありません。しかし、ブロックバッファリングされたプロセスでの `print()` は、たいていフラッシュされないまま `sys.stdout` のバッファに残り、終了時にインタープリターがそれを吐き出すと、そのままプロトコルのストリームに流れ込みます。振り向けられた場合でも、その行はレベルもロガー名もなく、フィルターする手段もないまま、生の状態でログ出力の中に紛れ込みます。
`logger.debug("got here")` なら同じ 1 行の手間で、正しい場所に出力されます。
## レベル {#the-level}
`logging.basicConfig()` を自分で呼び出す必要はありません。`MCPServer` を構築した時点で、すでに呼び出されています。標準エラーに向けたハンドラーが、`log_level=` で渡したレベルで設定されます。つまり `MCPServer("Bookshop", log_level="DEBUG")` と書くだけで、`logger.debug(...)` の行が見えるようになります。
デフォルトは `"INFO"` です。
`logging.basicConfig()` は、すでに存在するハンドラーを置き換えることはありません。サーバーを作成する前に自分でロギングを設定していれば、その設定が優先されます。
## 試してみる {#try-it}
MCP Inspector でサーバーを実行してください。
```console
uv run mcp dev server.py
```
**Tools** タブから `search_books` を呼び出してください。Inspector に表示される結果は、戻り値だけです。次の行は、
```text
Searching for 'dune'
```
標準エラー、つまりターミナルに出力されました。通信上には現れません。
!!! info
本当に欲しいものが「トレーシング」(すべてのリクエスト、かかった時間、失敗したかどうか)なら、必要なのはログ行ではなくスパンです。サーバーはすでにスパンを出力しています。SDK はデフォルトで、すべてのメッセージを OpenTelemetry でトレースします。**[OpenTelemetry](../run/opentelemetry.md)** を参照してください。
## まとめ {#recap}
* MCP プロトコルのロギングのケイパビリティは 2026-07-28 版の仕様で非推奨となり、代わりのものはありません。これを土台にしないでください。
* モジュールレベルで `logger = logging.getLogger(__name__)`、ツールの中で `logger.info(...)`。パターンはこれだけです。
* ログ出力がモデルに届くことはありません。届くのは `return` した値だけです。
* 標準エラーは自由に使えますが、stdout はプロトコルのものです。SDK は稼働中、フラッシュされた紛れ込みの stdout 出力を stderr へ振り向けますが、フラッシュされていない `print()` は終了時に通信路へ流れ込むことがあり、振り向けられた行もラベルなしで届きます。すべてのレコードをフラッシュするハンドラーを持つ `logging` を使ってください。
* `MCPServer(..., log_level="DEBUG")` でレベルを設定でき、先に行ったロギングの設定はそのまま残されます。
サーバー上で何か(ツール一覧やリソース)が変わったことを接続中のクライアントに伝える方法は、**[サブスクリプション](subscriptions.md)**にあります。

View file

@ -0,0 +1,183 @@
---
translation:
sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b]
tool: 1
---
# マルチラウンドトリップ(multi-round-trip)リクエスト {#multi-round-trip-requests}
ツールが 1 回のラウンドトリップでは完了できないことがあります。選択、確認、認証情報など、ユーザーだけが持っているものが必要になる場合です。
2026-07-28 より前は、サーバーは**呼び返す**ことでそれを手に入れていました。つまり、元のリクエストを処理している途中で、エリシテーション(elicitation)やサンプリング呼び出しといった自分のリクエストをクライアントに向けて開いていました。2026-07-28 の仕様は、このバックチャネル(back-channel)を廃止します。
代わりに、サーバーは**返します**。
## 呼び返さずに返す {#return-dont-call-back}
サーバーは `tools/call` に対して、`CallToolResult` の代わりに **`InputRequiredResult`** で応答します。仕事をするのはそのうち 2 つのフィールドです。
* **`input_requests`**:サーバーがまだ必要としているもの。サーバーが選んだ名前をキーとする dict です。各値は `ElicitRequest`、`CreateMessageRequest`、`ListRootsRequest` のいずれかです。
* **`request_state`**:不透明なトークン。クライアントはリトライ時にこれをそのまま送り返します。これを読むのはサーバーだけです。
クライアントはそれぞれのリクエストに応えたうえで、**同じツールをもう一度**呼び出します。このとき回答を `input_responses` に、トークンを `request_state` に載せます。サーバーは足りなかったものを手に入れ、通常の `CallToolResult` を返します。
プロトコルはこれだけです。どの区間もクライアントからサーバーへの普通のリクエストです。逆向きに流れるものは一切ありません。
## サーバー側 {#the-server-side}
`@mcp.tool()` では、これを手で組み立てることはめったにありません。ユーザーに尋ねる依存関係(`Elicit`)、クライアントの LLM をサンプリングする依存関係(`Sample`)、クライアントのルート(roots)を一覧する依存関係(`ListRoots`)のいずれかを宣言すれば、SDK が `InputRequiredResult` を返してくれます。その形式は **[依存関係](dependencies.md)** のページで扱います。2 つの形式は混在できません。1 回の呼び出しには `input_responses`/`request_state` のチャネルが 1 つしかないため、`Resolve(...)` パラメーターを使うツールは、本体から `InputRequiredResult` を返すこともできません。`InputRequiredResult` を戻り値として宣言すると登録時に拒否され(`InvalidSignature`)、宣言せずに返すと実行時に呼び出しが失敗します。手動の形式は**低レベル**の `Server` で、その `on_call_tool` ハンドラーはどちらの結果型を返してもかまいません。
```python title="server.py" hl_lines="43-46"
--8<-- "docs_src/mrtr/tutorial001.py"
```
* `on_call_tool` の型は `-> CallToolResult | InputRequiredResult` です。2 つ目を返すこと、それがサーバー側の API のすべてです。
* 最初の呼び出しでは `params.input_responses` が `None` なので、ガードが働き、ハンドラーは答える代わりに尋ねます。
* リトライ時には、クライアントが送った `ElicitResult` が、サーバーが `input_requests` で使ったのと**同じキー**(`"region"`)の下に入っています。
そのファイルの残り(明示的な `input_schema`、手組みの `CallToolResult`)は普通の低レベル `Server` で、**[低レベル Server](../advanced/low-level-server.md)** で扱っています。このページが付け加えるのは 2 つ目の戻り値の型だけです。
## ツール以外 {#beyond-tools}
`tools/call` は特別ではありません。2026-07-28 では、サーバーは `prompts/get` と `resources/read` にも同じように応答できます。`MCPServer` では、`@mcp.prompt()` 関数、または `@mcp.resource()` の**テンプレート**関数が、自分で `InputRequiredResult` を返し、リトライ時の回答をコンテキストから読み取ります。
```python title="server.py" hl_lines="20 22 24"
--8<-- "docs_src/mrtr/tutorial004.py"
```
* 1 回目は `InputRequiredResult` を返します。リトライ時には `ctx.input_responses` が同じキーの下に回答を保持しており、関数は通常の結果を返します。ここではプロンプトメッセージ、テンプレートリソースならリソースの内容です。
* 設定した `request_state` は、サーバー上の他のものと同じく、通信路に出る前に封印され、送り返されたときに検証されます。封印で何が得られるか、いつキーの設定が必要かは、下の **[`requestState` の保護](#protecting-requeststate)** で扱います。
* `@mcp.tool()` 関数も、依存関係の形式が合わない場合は、同じように結果を直接返せます。
* 静的な `@mcp.resource()` 関数は参加しません。`Context` を受け取らないので、リトライを読み取りようがないからです。尋ねられるのはテンプレートリソースだけです。
* 下の世代のルールはそのまま適用されます。2026 より前のセッションで `InputRequiredResult` を返すと、警告で説明しているのと同じ `-32603` になります。
## クライアント側 {#the-client-side}
`Client` がループを回してくれます。
サーバーが求める可能性のあるコールバック(`elicitation_callback`、`sampling_callback`、`list_roots_callback`)を登録し、ツールを呼び出します。`InputRequiredResult` が届くと、`Client` は `input_requests` の各エントリーを対応するコールバックに振り分け、回答とエコーバックした `request_state` を付けてリトライし、`CallToolResult` が返ってくるまで続けます。
```python title="client.py" hl_lines="11 12"
--8<-- "docs_src/mrtr/tutorial003.py"
```
* この `elicitation_callback` は、2026 より前のサーバーのバックチャネル `elicitation/create` が呼び出していたはずのものと同じです。`sampling/createMessage` に対する `sampling_callback`、`roots/list` に対する `list_roots_callback` も同様です。2026-07-28 では単独のサーバー→クライアント RPC はなくなりましたが、まったく同じ `ElicitRequest` / `CreateMessageRequest` / `ListRootsRequest` のペイロードが `input_requests` の中に載り、同じ 3 つのコールバックに振り分けられます。1 組のコールバックで両方の世代に対応できます。
* `call_tool` は素の `CallToolResult` を返します。途中のラウンドは呼び出し側からは見えません。
* `get_prompt` と `read_resource` も同じループを回します。
!!! check
コールバックを付けないままにすると、ループは 1 回目で失敗します。SDK の代役のコールバックはすべてのエリシテーションにエラーで答え、`call_tool` は *"Elicitation not supported"* というメッセージの `MCPError` を送出します。
ループには上限があります。`Client(..., input_required_max_rounds=10)` がデフォルトの上限で、それを超えて `InputRequiredResult` を返し続けるサーバーに対しては `call_tool` が例外を送出します。あるラウンドが `request_state` だけを載せていて `input_requests` がない場合、`Client` はリトライの前に短くスリープします(50ms から倍々に増えて上限 250ms)。そのため、単に「まだ終わっていない」と言っているだけのサーバーをビジーポーリングすることはありません。
### ループを自分で回す {#driving-the-loop-yourself}
自動ループは単一プロセスのクライアントには十分です。次のような場合は、代わりに自分でループを握ってください。
* クライアントが**分散**している場合。ユーザーに質問を表示するプロセスが `call_tool` を呼んだプロセスではなく、別のワーカーがリトライを発行します。`request_state` はその境界を越えて自分のストレージ経由で持ち運べる永続化可能なトークンであり、`input_responses` は向こう側がそれと一緒に送り返すものです。
* 各ラウンドを**検査**したい場合。すべての `input_requests` エントリーを記録・監査する、特定の種類のリクエストを拒否する、区間の間に独自のバックオフを適用する、などです。
* ラウンド数ではなく**実時間**で上限をかけたい場合。`input_required_max_rounds` に頼る代わりに、自分のループを `anyio.fail_after(...)` で包みます。
下層のセッションに降りると、`allow_input_required=True` がユニオン型をそのまま渡してくれます。
```python title="client.py" hl_lines="12 13 19"
--8<-- "docs_src/mrtr/tutorial002.py"
```
* `client.session.call_tool(..., allow_input_required=True)` は戻り値の型を `CallToolResult | InputRequiredResult` に広げます。それを絞り直すのが `isinstance` です。
* `request_state` はこれで自分の手の中にあります。区間の間で書き留めておけば、新しいプロセスから会話を再開できます。
* `input_requests` の各エントリーについて、`input_responses` の**同じキー**の下に `InputResponse` を置きます。`fulfil` が UI の入る場所です。この例では回答をハードコードしています。
* どの区間でも、ツール名も `arguments` も同じです。リトライは元の呼び出しをもう一度実行するものであり、新しいメソッドではありません。
## `requestState` の保護 {#protecting-requeststate}
ここまでは `request_state` をエコーとして扱ってきましたし、通信上はまさにそれだけのものです。しかしクライアントは区間の間それを保持します(プロセスをまたいで書き留めることは、まさに前のセクションが認めたことです)。そのため、戻ってくるものは**クライアントが供給する入力**です。改変されているかもしれず、期限切れかもしれず、まったく別の呼び出しから抜き取られたものかもしれません。仕様はサーバーに対し、この状態が認可、リソースアクセス、ビジネスロジックに影響しうる場合は常に、状態の完全性を保護し、検証に失敗したラウンドを拒否することを要求しています。
`MCPServer` はデフォルトでこれを保護します。どのサーバーも、送り出す `requestState` を封印し、すべてのエコーを検証します。リゾルバーの状態も手組みの状態も同様で、プロセス起動時に生成されたキーを使います。設定するものは何もなく、平文を書き、平文を読みます。通信路に載るのは不透明な暗号化トークンだけです。
デフォルトのキーはプロセスとともに生まれて消えます。単一プロセスを超えてデプロイする前に、これだけは知っておく必要があります。
```python
from mcp.server.mcpserver import MCPServer, RequestStateSecurity
# Multi-instance or restart-surviving: one or more shared secret keys (>= 32 bytes each).
mcp = MCPServer("fleet", request_state_security=RequestStateSecurity(keys=[key]))
```
* **デフォルト(設定なし)**は単一プロセスに向いています。stdio、または HTTP ワーカーがちょうど 1 つの場合です。別のワーカー、ロードバランサーの背後の別インスタンス、再起動後の同じサーバーに届いたリトライは、そのプロセスが持っていないキーで封印されています。クライアントは下記の固定の拒否を受け取り、フローを最初からやり直さなければなりません。
* **`keys=[...]`** は、リトライが**別のインスタンス**に届く可能性がある場合(マルチワーカーの `uvicorn`、ロードバランスされた HTTP)や、再起動をまたいで生き残る必要がある場合に必須です。どのインスタンスも、兄弟インスタンスが発行したものを検証できます。仕組みは同じで、生成されたものの代わりに自分のシークレットを使うだけです。
* KMS や既存のトークンサービスなど独自の暗号を使うなら、`keys` の代わりに `RequestStateSecurity(codec=...)` を渡します。その契約は下の **[独自の暗号を持ち込む](#bring-your-own-crypto)** で扱います。
### 封印が運ぶもの {#what-the-seal-carries}
デフォルトでも設定済みでも、通信路上の `requestState` は暗号化され認証されたトークンです。自分のコードがそれを目にすることはありません。ハンドラーとリゾルバーは平文を書き、平文を読みます(`ctx.request_state`)。SDK が送り出すときに封印し、受け取るときに検証します。完全性に加え、各トークンは次のものに束縛されます。
* **時間枠。** ラウンドごとに新しい有効期限で封印し直すため、`RequestStateSecurity(ttl=...)`(デフォルト 600 秒)が制限するのはフロー全体ではなく、ラウンドごとの考慮時間です。
* **認証されたプリンシパル。** SDK が検証した OAuth アクセストークンをリクエストが載せている場合、状態はトークンのクライアント、発行者、サブジェクトに束縛されます。あるユーザー向けに発行された状態は、両者が 1 つの OAuth クライアントを共有していても、別のユーザーの下では失敗します。サブジェクトを供給しないベリファイアーは、束縛をクライアントの識別情報だけに弱めます。URL ベースのクライアント ID の下では、それはそのクライアントソフトウェアのすべてのユーザーで共有されます。認証が SDK の外(前段のプロキシ)で終端されている場合や、トランスポートが認証なしの場合は、束縛するプリンシパルがないためこのチェックは働きません。`RequestStateSecurity(bind_principal=...)` で独自のアイデンティティシグナルから供給すれば別です。トークンベリファイアーがどの要素を供給するにせよ、一貫して供給しなければなりません。あるリクエストではサブジェクトを含め、別のリクエストでは省くベリファイアーは、フローの途中でプリンシパルを変えてしまい、進行中のラウンドは拒否されます。
* **元のリクエスト。** メソッド、ツール名またはプロンプト名(あるいはリソース URI)、そして引数のダイジェストです。別のツール、別の引数、別のメソッドに対してリプレイされたトークンは失敗します。
* **尋ねた質問そのもの。** リゾルバーの回答はすべて、クライアントに表示されたレンダリング済みの質問に固定されます。最初に届いたラウンドでも、記録済みの回答を後で再利用するときでも同じです。メッセージの文言を変えたりスキーマを変えたりして再デプロイすると、サーバーは古い回答を消費する代わりに尋ね直します。同じ固定は逆向きにも効きます。メッセージは呼び出しごとのデータではなく、ツールの引数から導出してください。タイムスタンプやライブのレートから組み立てたメッセージはラウンドごとにレンダリングが変わるため、記録済みの回答はどれも古く見え、クライアントのラウンド上限で呼び出しが終わるまでサーバーは尋ね直し続けます。
これらはすべて SDK の仕事であり、自分の仕事ではありません。独自のコーデックを持ち込んでも、コーデックの仕事ではありません。
### キーのローテーション {#rotating-keys}
`keys[0]` が新しい状態を封印し、リスト内のすべてのキーが検証に使われます。ダウンタイムなしのローテーションは 3 段階で、各段階を完全にロールアウトしてから次に進みます。
```python
RequestStateSecurity(keys=[OLD, NEW]) # 1: every instance learns to verify NEW; OLD still mints
RequestStateSecurity(keys=[NEW, OLD]) # 2: NEW mints; in-flight OLD state keeps verifying
RequestStateSecurity(keys=[NEW]) # 3: one ttl after phase 2 is fully out, retire OLD
```
発行側を先に昇格させないでください。まだ検証できないインスタンスがあるキーで発行すると、ロールアウトの途中で進行中のラウンドが落ちます。
キーのスコープは 1 つのサービスです。封印されたエンベロープはサーバーの名前もオーディエンスクレームとして載せているため、たまたまシークレットを共有している別のサービスが発行したトークンは、いずれにせよ拒否されます。このクレームの識別力は名前次第なので、明示的なポリシーを与えられたサーバーは本物の名前を持つか、`RequestStateSecurity(audience=...)` を設定しなければなりません。名前のないサーバーは構築時に例外を送出します。`audience=` は、あるサービスが別のサービスの発行した状態を受け入れなければならない、意図的なマルチサービス構成にも使えます。(設定なしのデフォルトは対象外です。そのキーはプロセスの外に出ることがないので、オーディエンスクレームが付け加えるものはありません。)
### 独自の暗号を持ち込む {#bring-your-own-crypto}
`RequestStateSecurity(codec=...)` は、`seal(bytes) -> str` と `unseal(str) -> bytes` を持ち、自分が発行していないトークンに対しては `InvalidRequestState` を送出するものなら何でも受け取ります。典型的な形は KMS に対するエンベロープ暗号化で、起動時にデータキーを一度アンラップし、トークンごとの暗号処理はローカルに保ちます。
```python title="server.py" hl_lines="12 26-27 34-35 38"
--8<-- "docs_src/mrtr/tutorial005.py"
```
TTL、プリンシパルの束縛、リクエストの束縛はコーデックの仕事**ではありません**。SDK はどのコーデックについても、`seal` の前にそれらをペイロードに刻み込み、`unseal` の後で再検証します。コーデックの義務は完全性(改ざんされていれば送出する)と、理想的には機密性だけです。
### 検証に失敗したとき {#when-verification-fails}
受信側の失敗はすべて、改ざん、期限切れ、別のリクエストやプリンシパルに対するリプレイ、このサーバーが知らないキーでの封印のいずれであっても、同じ答えを受け取ります。
```json
{"code": -32602, "message": "Invalid or expired requestState"}
```
どの原因にも固定のメッセージが 1 つなので、どのチェックが失敗したかが通信上に漏れることはありません。本当の理由はサーバーのログに出ます。`tools/call`、`prompts/get`、`resources/read` に届く `requestState` はすべてチェックされ、状態を発行しないハンドラー宛てに届いたものも含まれます。実際に最も多い拒否は攻撃者ではありません。デフォルトのプロセスローカルなキーが、再起動前や別インスタンスからのリトライと出会うケースです。クライアントはフローをやり直し、それが問題になる場合の対策が `keys=[...]` です。
### 手組みの状態 {#hand-built-state}
自分で設定する `request_state`(ツール、プロンプト、リソーステンプレートの関数から `InputRequiredResult` を返す場合)は、リゾルバーの状態と同じ仕組みで封印・検証され、コードの変更は一切不要です。平文を書き、平文を読むだけで、上記のすべての束縛が適用されます。
設定済みであっても SDK が代わりに固定できない唯一のものは、質問の同一性です。状態の中にある回答が、こちらで定義したどの質問に属するのかを SDK は知りません。質問をキーにして回答を保存するなら、独自の質問識別子を状態に含め、リトライ時にそれをチェックしてください。
低レベルの `Server` は何も付いてこない層です。`MCPServer` と違い、自分で境界を追加するまで何も封印されず、それまでは `request_state` が書いたとおりに通信路を渡ります。1 行のオプトインは **[低レベル Server](../advanced/low-level-server.md#the-other-handlers)** に示しています。
## 2026-07-28 の結果型 {#a-2026-07-28-result}
`InputRequiredResult` はプロトコルバージョン **2026-07-28** にしか存在しません。インメモリの `Client(server)` はそれを代わりにネゴシエートしてくれます。通信路越しでは `mode="auto"` がそれを検出します。接続後、`client.protocol_version` で何が得られたかが分かります。
!!! warning
2026 より前のセッションには `InputRequiredResult` を入れる場所がありません。`mode="legacy"` の接続でハンドラーからこれを返すと、ランナーはネゴシエートされたバージョンにシリアライズできず、クライアントには `-32603` *"Handler returned an invalid result"* エラーが返ります。両方の世代に対応するサーバーは、これを使う前に `ctx.protocol_version` をチェックしなければなりません。
!!! info
**URL モードのエリシテーション**は、2026 の接続ではまさにこの仕組みに載ります。`input_requests` のエントリーは、params が `ElicitRequestURLParams` である `ElicitRequest` です。ユーザーが帯域外のフローを終えると、クライアントが呼び出しをリトライします。同じループで、新しい API はありません。高レベルサーバー側の話は **[エリシテーション](elicitation.md)** にあります。
## まとめ {#recap}
* 2026-07-28 では、呼び出しの途中で入力が必要なサーバーは `InputRequiredResult` を**返します**。クライアントへのリクエストを開くことは決してありません。
* `input_requests` は必要としているものです。`request_state` はサーバーだけが読む不透明な再開トークンです。
* `Client` がリトライループを回してくれます。`elicitation_callback` / `sampling_callback` / `list_roots_callback` を登録すれば、`call_tool` は素の `CallToolResult` を返します。`input_required_max_rounds`(デフォルト 10)が上限をかけます。
* ラウンドを検査したり永続化したりするには、`client.session.call_tool(..., allow_input_required=True)` を使い、`while isinstance(result, InputRequiredResult)` ループを自分で握ります。
* `@mcp.tool()` では、ユーザーに尋ねる依存関係がこの結果を作ってくれます(**[依存関係](dependencies.md)**)。**低レベル**の `Server` が手動の形式です。
* プロンプトとリソースも参加します。`@mcp.prompt()` またはテンプレートの `@mcp.resource()` 関数は自分で `InputRequiredResult` を返し、リトライ時に `ctx.input_responses` を読みます。
* `requestState` はクライアントが供給する入力として戻ってくるため、`MCPServer` はデフォルトで、リゾルバーの状態も手組みの状態も同様に、プロセスローカルなキーで封印します。マルチインスタンスのデプロイでは `RequestStateSecurity(keys=[...])`(またはカスタムコーデック)を渡し、どのインスタンスも兄弟インスタンスが発行したものを検証できるようにします。封印はすべてのトークンを時間枠と元のリクエストに束縛します。さらに、SDK が検証した認証をリクエストが載せている場合や、`bind_principal=` で独自のアイデンティティシグナルを供給している場合は、認証されたプリンシパルにも束縛します(**[`requestState` の保護](#protecting-requeststate)**)。
これがサーバー起点のサンプリングや、プッシュ型のバックチャネルの残りを置き換える仕組みです。**[非推奨の機能](../deprecated.md)** を参照してください。

View file

@ -0,0 +1,112 @@
---
translation:
sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007]
tool: 1
---
# 進捗 {#progress}
30 秒かかるツールが 30 秒間なにも言わなければ、壊れているように見えます。
**進捗通知**はそれを解決します。ツールはどこまで進んだかを報告し、クライアントはそれを使って何を描くかを決めます。プログレスバー、スピナー、ログの 1 行などです。
## ツールから報告する {#report-it-from-the-tool}
**`Context`** パラメーターを受け取り、`report_progress` を呼び出してください。
```python title="server.py" hl_lines="8 11"
--8<-- "docs_src/progress/tutorial001.py"
```
引数は 3 つで、その意味は自分で決めます。
* `progress`:どこまで進んだか。仕様では、報告のたびに**増加する**ことが必須です。同じ値を繰り返したり、減らしたりしないでください。
* `total`:全体でどれだけあるか(わかっている場合)。省略可能です。
* `message`:「この」ステップについての、人が読める 1 行。省略可能です。
`ctx` は型ヒントによって注入され、モデルからは決して見えません。`import_catalog` の入力スキーマにあるプロパティは `urls` の 1 つだけです。**[Context](context.md)** のページはこのオブジェクトについて詳しく扱っています。進捗はそれが提供するものの 1 つです。
## クライアントで受け取る {#listen-for-it-from-the-client}
クライアントは、`call_tool` に `progress_callback=` を渡すことで、**呼び出しごとに**オプトインします。
```python title="client.py" hl_lines="7 16"
import anyio
from mcp import Client
from server import mcp
async def show(progress: float, total: float | None, message: str | None) -> None:
print(f"{message} ({progress}/{total})")
async def main() -> None:
async with Client(mcp) as client:
result = await client.call_tool(
"import_catalog",
{"urls": ["https://example.com/a.json", "https://example.com/b.json"]},
progress_callback=show,
)
print(result.structured_content)
anyio.run(main)
```
コールバックは `async` 関数で、サーバーが報告したものをそのまま受け取ります。`progress`、`total`、`message` です。
!!! info
`Client(mcp)` はサーバーオブジェクトにメモリ内で直接接続します。**[テスト](../get-started/testing.md)** のページの土台になっているのと同じクライアントです。`progress_callback` は、`Client` がどのトランスポートを使っていても同じパラメーターです。これから目にする「タイミング」はメモリ内接続のものです。メモリ内接続はコールバックをインラインで実行するため、すべての報告が `call_tool` が返る前に届きます。実際のトランスポートでは通知と結果の到着順は保証されず、遅いコールバックは `call_tool` が返ったあともまだ実行中のことがあります。
### 試してみる {#try-it}
`client.py` を `server.py` の隣に置いて、実行してください。
```console
python client.py
```
```text
Imported https://example.com/a.json (1/2)
Imported https://example.com/b.json (2/2)
{'result': 'Imported 2 records.'}
```
サーバー側の `await ctx.report_progress(...)` はそれぞれ、クライアント側で順番どおりに `show` の 1 回の呼び出しになり、2 行とも `call_tool` が返る**前に**出力されました。進捗は結果にまとめられるのではなく、ツールがまだ動いている間にストリーミングされます。
!!! warning
`progress_callback` は `Client` ではなく、**呼び出し**に属します。そのためのコンストラクター引数はありません。呼び出しごとに必要なコールバックが違うからです。ある呼び出しはダウンロードバーを動かし、次の呼び出しはログの 1 行を出します。
!!! check
今度は `progress_callback=show` を削除して、もう一度実行してください。
```text
{'result': 'Imported 2 records.'}
```
エラーも警告もなく、結果は同じです。`report_progress` は、**呼び出し側が進捗を要求しなかったときは何もしません**。ですから無条件に報告すればよく、誰かが聞いているかどうかを気にする必要はありません。
## 全体量がわからないとき {#when-you-dont-know-the-total}
`total` は分母がわかっているときのためのものです。わからないことも多いでしょう。フィードを読み尽くしているとき、カーソルをたどっているとき、長さヘッダーのないものをダウンロードしているときなどです。
その場合は省略してください。
```python title="server.py" hl_lines="20"
--8<-- "docs_src/progress/tutorial002.py"
```
コールバックは `total=None` を受け取ります。クライアントはそれでも「活動中」であることは表示できます(「3 imported so far...」など)が、パーセンテージは表示できません。見栄えのよいバーのために全体量をでっち上げないでください。
!!! tip
`progress` は特定の何かを数える必要はありません。バイト、行、ページ。ユーザーにとってわかりやすい単位を選び、守れる `total` だけを約束してください。
## まとめ {#recap}
* `Context` を受け取るツールならどこからでも `await ctx.report_progress(progress, total=None, message=None)` を呼べます。
* クライアントは `call_tool` に `progress_callback=` を渡します。呼び出しごとであり、`Client` には渡しません。
* コールバックは `async (progress, total, message) -> None` で、ツールがまだ実行中の間に呼ばれます。
* 呼び出しにコールバックがなければ、`report_progress` は何もしません。無条件に報告してください。
* わからないときは `total` を省略します。コールバックは `None` を受け取ります。
進捗は、実行中のツールが「ユーザー」に見せるものです。サーバーを運用する「自分」のために記録する行は、別のチャネルです。**[ロギング](logging.md)** を参照してください。

View file

@ -0,0 +1,51 @@
---
translation:
sections: [5c82b20cbd65ded0, 9dc22632be79a533, 1fb8f452e990c456, 42666ab914ff0cb1, c4e0cb3667fd5ff9]
tool: 1
---
# サンプリングとルート {#sampling-and-roots}
ハンドラーは、接続しているクライアントにさらに 2 つのことを要求できます。1 つはクライアント自身のモデルによる補完、つまり**サンプリング**です。もう 1 つはクライアントのワークスペースフォルダー、つまり**ルート**(roots)です。
どちらも、SDK が話すすべてのプロトコルバージョンで引き続き動作します。ただし、これらを前提に設計する前に、次の警告を読んでください。
!!! warning "2026-07-28 仕様で非推奨"
サンプリングとルートは `2026-07-28` で非推奨になりました([SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2577))。引き続き完全に機能し、削除の対象になるまで少なくとも 12 か月は仕様に残りますが、新しい実装はこれらを土台にすべきではありません。推奨される移行先は次のとおりです。サンプリングの代わりに LLM プロバイダーの API と直接統合し、ルートの代わりにツールのパラメーター、リソース URI、またはサーバー設定でディレクトリを渡します。SDK 全体の一覧は **[非推奨の機能](../deprecated.md)** にあります。
## サンプリング:クライアントのモデルを借りる {#sampling-borrow-the-clients-model}
リゾルバーが `Sample(...)` を返すと、ツールは補完結果を受け取ります。これは **[依存関係](dependencies.md)** で `Elicit` を動かしているのと同じ依存関係のしくみを通ります。
```python title="server.py" hl_lines="10-15 19"
--8<-- "docs_src/sampling_and_roots/tutorial001.py"
```
* `Sample(messages, max_tokens=...)` は `sampling/createMessage` のパラメーターをそのまま反映しています。注入される値はクライアントの `CreateMessageResult` です。`tools` または `tool_choice` を渡すと、代わりに `CreateMessageResultWithTools` になります。
* クライアントは `sampling` ケイパビリティを宣言している必要があります(`tools` または `tool_choice` を渡す場合は `sampling.tools`)。宣言していない場合、クライアントが処理できないリクエストを送る代わりに、呼び出しは `-32021` のプロトコルエラーで失敗します。バックチャネル(back-channel)のない 2026 年より前のセッションでは、送る経路がそもそもないため、いつものバックチャネルなしのエラーで失敗します。
* `2026-07-28` では、リクエストはマルチラウンドトリップ(multi-round-trip)のフローの中で配送されます(**[マルチラウンドトリップリクエスト](multi-round-trip.md)**)。`2025-11-25` では、クライアントへの単独のリクエストです。コードはどちらでも同じですが、マルチラウンドトリップのルールに注意してください。リクエストは再試行の各ラウンドでまったく同じ内容にならなければならないため、ツールの引数やその他の安定したデータだけから組み立ててください。
* `include_context` には触れないでください。`"none"` 以外の値はそれ自体が非推奨で(SEP-2596)、ほとんどのクライアントが宣言しないケイパビリティを必要とします。
## ルート:これはどこに置くべきか {#roots-where-should-this-go}
ルートは、サーバーが操作してよいとクライアントが示すフォルダーです。参考情報としての案内であり、アクセス制御のしくみではありません。リゾルバーは `ListRoots()` を返します。
```python title="server.py" hl_lines="10-11 15"
--8<-- "docs_src/sampling_and_roots/tutorial002.py"
```
* 注入される `ListRootsResult` は `Root` のリストを持ちます。それぞれが `file://` URI と、省略可能な表示名です。
* 条件はサンプリングと同じです。`roots` ケイパビリティが宣言されていなければ、リクエストを送る代わりに呼び出しは `-32021` で失敗します。
通信路の反対側では、クライアントはすでに持っているコールバックで両方のリクエストに応答します。`sampling_callback` と `list_roots_callback` で、**[クライアントのコールバック](../client/callbacks.md)** で説明しています。
## 2025 年世代の接続では {#on-2025-era-connections}
`ctx.session.create_message(...)` と `ctx.session.list_roots()` は、セッションを直接操作するコードのために今も存在します。これらはバックチャネルが存在する場所(2025 年世代の、ステートレスではない接続)でのみ動作し、呼び出すと非推奨の警告が出ます。上で紹介したリゾルバーのマーカーがサポートされる形です。ネゴシエートされたバージョンから配送方法を選び、警告も出しません。
## まとめ {#recap}
* リゾルバーから `Sample(...)` または `ListRoots()` を返します。ツールは、ほかの依存関係と同じように `CreateMessageResult` または `ListRootsResult` を受け取ります。
* クライアントは対応するケイパビリティを宣言しなければなりません。そうでなければ、リクエストは送られず、呼び出しは `-32021` で失敗します。
* どちらの機能も `2026-07-28` で非推奨です。当面は完全に機能しますが、新しい設計には向きません。サンプリングよりプロバイダーの API を、ルートより明示的なパラメーターを選んでください。
遅いツールがどこまで進んだかを報告するには、**[進捗](progress.md)** を参照してください。

View file

@ -0,0 +1,152 @@
---
translation:
sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883]
tool: 1
---
# サブスクリプション {#subscriptions}
サーバーのカタログは固定ではありません。ツールは実行時に現れますし、リソース URI の背後にある内容も変わります。
クライアントがそれを知る手段が**サブスクリプション**です。クライアントは `subscriptions/listen` リクエストを 1 つ送り、そのリクエストへのレスポンス自体がストリームになります。開いたままになり、クライアントが求めた変更通知を運びます。
## ツールから発行する {#publish-it-from-the-tool}
サーバー側でやることは 1 行だけです。変更を発行します。
```python title="server.py" hl_lines="20 32"
--8<-- "docs_src/subscriptions/tutorial001.py"
```
* `await ctx.notify_resource_updated("board://sprint")` は、その URI をサブスクライブした開いているストリームすべてに届きます。それ以外には届きません。
* `await ctx.notify_tools_changed()` は、ツール一覧の変更を求めたストリームすべてに届きます。これを受け取ったクライアントは `tools/list` をもう一度呼び出し、今度は `sprint_report` が見えます。
* 兄弟にあたるのが `notify_prompts_changed()` と `notify_resources_changed()` です。
* サブスクライバーがいなければ、何も起こりません。アイドル状態のサーバーへの発行は no-op なので、誰かが聞いているかどうかを確認することはありません。何が変わったかを述べるだけです。
`MCPServer` は `subscriptions/listen` を代わりに処理します。通信上の義務(最初のフレームとしての確認応答、ストリームごとのフィルタリング、全フレームへのサブスクリプション ID の付与)は SDK の仕事です。
!!! check
実際の通信では、フィルターに `board://sprint` を指定したストリームは、`complete_task` の実行後に次のようになります。
```json
{"method": "notifications/subscriptions/acknowledged",
"params": {"notifications": {"resourceSubscriptions": ["board://sprint"]}, "_meta": {"io.modelcontextprotocol/subscriptionId": "listen-1"}}}
{"method": "notifications/resources/updated",
"params": {"uri": "board://sprint", "_meta": {"io.modelcontextprotocol/subscriptionId": "listen-1"}}}
```
更新が運んでいないものに注目してください。ボードそのものです。どのフレームも `_meta` の下に listen リクエストの JSON-RPC id を持ち、その id がサブスクリプション ID です。これを発番するのはクライアントです。Python の `Client` は `"listen-1"` のような文字列を使いますが、他のクライアントは整数を使うこともあります。
## 求められたものだけ {#only-what-was-asked-for}
フィルターは契約です。ツール一覧の変更と 1 つのリソース URI を要求したストリームは、その 2 種類だけを受け取り、他は何も受け取りません。プロンプトの変更を発行しても、そのストリームは沈黙したままです。
`MCPServer` はリソース URI を文字列として完全一致で照合するので、`board://sprint` を指定したストリームには `board://sprint/tasks/1` のことは何も届きません。仕様では、サブスクライブされた URI のサブリソースの変更をサーバーが報告することを認めています。`MCPServer` がそうすることはありませんが、クライアントはそれを想定して作られています。
このストリームには、当てはまらないことが 2 つあります。
* **リプレイログではありません。** 切断されたストリームは失われ、誰も接続していない間に発行されたイベントはキューに入りません。クライアントは listen し直して再取得します。
* **2025 年世代の経路ではありません。** `resources/subscribe` を呼び出したクライアントには `ctx.session.send_resource_updated(uri)` で届けます。`notify_*` メソッドが届くのは `subscriptions/listen` のストリームだけです。
## 誰が監視できるかを決める {#deciding-who-may-watch}
デフォルトでは、要求された種類と URI はすべて受け入れられます。つまり、どの呼び出し側も、発行されるどの URI でも監視できます。読み取りハンドラーが参照されることはありません。誰も読み取っていないからです。`files://{name}` ハンドラーなら拒否するはずの呼び出し側でも、`files://payroll.csv` のストリームを開き、それが変わったこと、そしていつ変わったかを知ることができます。内容を知ることは決してありませんし、何が存在するかを探ることもできません。未知の URI も受け入れられ、単に一度も発火しないだけだからです。狭いとはいえ現実に存在する隙なので、マルチテナントのサーバーからユーザーごとの URI を発行する前にゲートを設けてください。
ゲートはミドルウェアです。SDK が確認応答する前に `subscriptions/listen` リクエストを見て、呼び出し側が読み取れないものを求めたときに拒否します。
```python title="server.py" hl_lines="19-26 29"
--8<-- "docs_src/subscriptions/tutorial006.py"
```
* `ctx.params` は生のリクエストなので、ミドルウェアは自分でそれを `SubscriptionsListenRequestParams` として検証し、クライアントが求めたフィルターを読み取ります。
* 拒否は `call_next(ctx)` の前に `MCPError` を送出することで行います。クライアントはそのエラーを受け取り、ストリームは得られず、接続はそのまま続きます。メッセージは URI を挙げない一様なものにして、拒否によってどの URI が保護されているかが確認されることのないようにしてください。
* 1 つの `can_access(user, uri)` が両方の問いに答えます。リソースハンドラーは `resources/read` でそれを問い合わせ、ミドルウェアは `subscriptions/listen` で問い合わせます。テーブルをデータベースや RBAC システムに置き換えても、両者の足並みはそろったままです。
* 判定はストリームの寿命のあいだ有効です。イベントごとの再チェックはないので、呼び出し側のアクセス権がストリームの途中で失効しうる場合(期限切れになるトークンなど)は、失効した時点でその呼び出し側の接続を終了してください。
ミドルウェアの契約の全体は、他に何をラップするか、なぜ暫定扱いなのかも含めて、**[ミドルウェア](../advanced/middleware.md)** にあります。
## クライアント側 {#the-client-end}
そのストリームの反対側で、ボードを追いかけるクライアントがこちらです。
```python title="client.py" hl_lines="15"
--8<-- "docs_src/subscriptions/tutorial003.py"
```
`client.listen(...)` に入るとリクエストが送られ、サーバーの確認応答を待つので、ブロックが始まる時点でストリームは生きています。型付きのイベントはどれも再取得の合図であって、ペイロードではありません。契約の全体が 1 画面に収まっています。クライアント側のそれ以外のことは専用のページにあります。メインのフローと並行しての監視、ストリームの終了、listen のやり直しです。「クライアント」の下の **[サブスクリプション](../client/subscriptions.md)** を参照してください。
## 1 プロセスを超えてスケールする {#scaling-past-one-process}
発行はハンドラーから開いているストリームへ、`SubscriptionBus` を経由して伝わります。デフォルトはインメモリで、1 つのプロセスとその中のすべてのストリームです。ロードバランサーの背後でレプリカを動かすまでは、これが正解です。レプリカを動かすと、クライアントのストリームは 1 つのレプリカに固定され、別のレプリカでの発行がそこに届かなければならないからです。
その継ぎ目は自分で実装します。pub/sub バックエンドの上に 2 つのメソッドを載せるだけです。
```python
from collections.abc import Callable
from redis.asyncio import Redis
from mcp.server.mcpserver import MCPServer
from mcp.server.subscriptions import ServerEvent # SubscriptionBus is a Protocol: no base class
class RedisSubscriptionBus:
def __init__(self, redis: Redis) -> None:
self._redis = redis
self._listeners: dict[object, Callable[[ServerEvent], None]] = {}
async def publish(self, event: ServerEvent) -> None:
await self._redis.publish("mcp-events", encode(event)) # to every replica
def subscribe(self, listener: Callable[[ServerEvent], None]) -> Callable[[], None]:
token = object()
self._listeners[token] = listener
def unsubscribe() -> None:
self._listeners.pop(token, None)
return unsubscribe
mcp = MCPServer("Sprint Board", subscriptions=RedisSubscriptionBus(redis))
```
`encode` は自分で用意します。各レプリカで到着したメッセージをデコードし、登録されたすべてのリスナーを呼び出すリーダータスクも同様です。リスナーは同期的で、例外を送出してはならず、サーバーのイベントループ上で動きます。
バスが運ぶのは型付きの `ServerEvent` 値(小さなデータクラスが 4 つ)であって、JSON-RPC ではありません。ID の付与、フィルタリング、ストリームのライフサイクルは SDK に残るので、バスの実装がプロトコルを壊すことはできません。できるのはプロセス間でイベントを移動させることだけです。
リクエストの外から発行するには、参照を手元に持てるようにバスを自分で組み立てます。何も渡さないと `MCPServer` は内部で 1 つ作りますが、それを公開しません。
```python
from mcp.server.subscriptions import InMemorySubscriptionBus, ToolsListChanged
bus = InMemorySubscriptionBus()
mcp = MCPServer("Sprint Board", subscriptions=bus)
async def tools_reloaded() -> None:
await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere
```
## 低レベルでの組み立て {#the-low-level-composition}
低レベルの `Server` には、あらかじめ配線されたものは何もありません。同じ部品を 3 行で組み立てます。
```python title="server.py" hl_lines="8-9 47"
--8<-- "docs_src/subscriptions/tutorial002.py"
```
* バスは自分のものなので、直接そこへ発行します。`await bus.publish(ResourceUpdated(uri=...))` です。ハンドラーから届く場所に置いてください。ここではモジュールスコープ、大きなアプリではライフスパンです。
* `ListenHandler(bus)` は `MCPServer` が登録するのと同じハンドラーで、`on_subscriptions_listen=` は普通のハンドラースロットです。別のセマンティクスが欲しければそのスロットに独自の callable を入れてください。その場合、仕様上の義務は自分に移ります。まず確認応答し、すべてのフレームにサブスクリプション ID を付与し、フィルター外のものは何も配信しないことです。
* `ListenHandler.close()` は開いているすべてのストリームを正常に終了させます。各ストリームは最後のフレームとして listen リクエストの result を受け取ります。これは、サーバーが意図的にサブスクリプションを終了したことを示す仕様上の方法です。ストリームのフラッシュが終わる前に戻るので、トランスポートを破棄する前に少し待ってください。これを呼ばなければ、ストリームはクライアントが切断したときに終わります。
## まとめ {#recap}
* クライアントは `subscriptions/listen` リクエスト 1 つでオプトインし、そのレスポンスがストリームです。それを処理する機能は組み込まれています。
* 発行は `ctx.notify_*` で行い、ID の付与、フィルタリング、ライフサイクルの処理は SDK が担当します。
* イベントは合図であって、ペイロードではありません。両端とも再取得します。
* クライアント側は `async with client.listen(...)` です。詳しくは「クライアント」の下の **[サブスクリプション](../client/subscriptions.md)** を参照してください。
* 低レベルの `Server` では同じ部品を自分で組み立てます。バス、`ListenHandler(bus)`、`on_subscriptions_listen` スロットです。
* スケールアウトとは、`SubscriptionBus`(メソッド 2 つ)を実装し、`MCPServer(subscriptions=...)` として渡すことです。
これらすべてを処理するサーバーを、レプリカ 1 つでも 20 でも動かす方法は、**[デプロイとスケール](../run/deploy.md)** にあります。