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
6 changed files with 89737 additions and 54 deletions

View file

@ -0,0 +1,121 @@
---
translation:
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
tool: 1
---
# MCP Apps {#mcp-apps}
**MCP App** とは、見た目を持つツールのことです。データと並んで、ツールがホストに対話型の画面として描画させる HTML ドキュメントを指し示します。
構成要素は 2 つで、常にこの 2 つです。
1. **ツール**。ほかのツールと同じように、処理を行ってデータを返します。
2. **`ui://` リソース**。ホストがそのツールのために表示する HTML を収めます。
ツールは `_meta.ui.resourceUri` でリソースを参照します。ホストはそれを `resources/read` で取得し、**サンドボックス化された iframe** に描画し、ツールの結果を `postMessage` 経由でその iframe に送り込みます。サーバーが `ui/*` メッセージを送受信することは一切ありません。そのやり取りはホストと iframe の間のものです。サーバーが提供するのはツールと HTML ドキュメントだけで、演出はホストが担当します。
SDK はこれを組み込みの `Apps` 拡張(`io.modelcontextprotocol/ui`)として提供しています。[拡張](extensions.md)になじみがなければ、先にそのページにざっと目を通してください。1 分で済みます。それから戻ってきてください。
## 見た目のある時計 {#a-clock-with-a-face}
```python title="server.py" hl_lines="19 22 30 32"
--8<-- "docs_src/apps/tutorial001.py"
```
やることは 4 つです。
* `Apps()`:1 つのインスタンスが、UI に紐づくツールとそのリソースをまとめて保持します。
* `@apps.tool(resource_uri="ui://clock/app.html")`:通常のツールに `_meta.ui.resourceUri` の印を加えたものです。`@mcp.tool()` が受け付けるもの(name、title、description など)はすべてそのまま渡せます。
* `apps.add_html_resource("ui://clock/app.html", CLOCK_HTML)`:対応するリソースで、`text/html;profile=mcp-app` として提供されます。この MIME タイプこそが、ホストに「これはアプリなので描画せよ」と伝える目印です。
* `MCPServer("clock", extensions=[apps])`:オプトインします。これでサーバーは `capabilities.extensions` の下で `io.modelcontextprotocol/ui` を公開します。
HTML 自体はホストの `postMessage` を待ち受けて結果を表示します。本格的なアプリでは、HTML の中で公式の [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) ブラウザー SDK を使ってください。生のメッセージイベントの代わりに `ontoolresult`、`callServerTool`、`getHostContext`、`onhostcontextchanged` が使えます。
## グレースフルデグラデーション {#graceful-degradation}
すべてのクライアントがアプリを描画するわけではありません。それが何を意味するかについて、仕様は率直です。
> ツールは、UI が利用できる場合でも、意味のある `content` 配列を返さ**なければなりません**。
モデルが読むのは `content` で、iframe は人間のためのものです。UI に対応したホストでもテキストの結果はモデルに渡されますし、テキスト専用のクライアントはそれ「だけ」を受け取ります。ですから定番のパターンは「1 つのツール、2 つの答え」です。もう一度 `get_time` を見てください。
```python title="server.py" hl_lines="23-27"
--8<-- "docs_src/apps/tutorial001.py"
```
`client_supports_apps(ctx)` が `True` になるのは、クライアントが `io.modelcontextprotocol/ui` 拡張を宣言し、**かつ** `mimeTypes` 設定に `text/html;profile=mcp-app` を含めている場合だけです。このフィールドは必須なので、省略したクライアントは該当しません。同じファイルの `main()` が宣言しているのはまさにこれです。ネゴシエーションのクライアント側であり、その結果リッチな答えが返ってきます。
!!! warning
`"[Rendered UI]"` のようなプレースホルダーを唯一のコンテンツとして返さないでください。フォールバックのテキストが役に立たなければ、そのツールはテキスト専用のすべてのクライアントにとっても、モデル自身にとっても役に立ちません。きちんと文を書いてください。
## iframe を厳しく制限する {#locking-the-iframe-down}
セキュリティのメタデータはリソース側が持ちます。iframe が何を読み込めるか、どのブラウザー権限を要求するか、どのようにフレーム内に表示されたいか、です。
```python title="server.py" hl_lines="9 19-22"
--8<-- "docs_src/apps/tutorial002.py"
```
`csp` と `permissions` は**ホストへの要望**であって、サーバーの振る舞いではありません。ホストはそれらをもとに iframe の Content-Security-Policy と Permissions-Policy を組み立てますが、拒否することもあります。許可されたと決めつけず、JS 側で機能検出してください。
`ResourceCsp` をフィールドごとに示します(Python の名前、通信上のキー、ホストがそれで何をするか)。
| Python | 通信上のキー(`_meta.ui.csp`) | 制御対象 |
|---|---|---|
| `connect_domains` | `connectDomains` | `connect-src`:`fetch` や XHR の接続先 |
| `resource_domains` | `resourceDomains` | `img-src`、`style-src` など:静的アセット |
| `frame_domains` | `frameDomains` | `frame-src`:入れ子の iframe |
| `base_uri_domains` | `baseUriDomains` | `base-uri`:`<base>` が指せる先 |
`ResourcePermissions`:各フィールドが iframe 用のブラウザー権限を要求します。
| Python | 通信上のキー(`_meta.ui.permissions`) |
|---|---|
| `camera` | `camera` |
| `microphone` | `microphone` |
| `geolocation` | `geolocation` |
| `clipboard_write` | `clipboardWrite` |
!!! note
CSP と権限は**リソース**に置くもので、ツールには決して置きません。仕様のツールメタデータにはそれらの入る場所がなく、そこに置いてもホストは無視します。SDK ではこの間違いをそもそも表現できないようにしています。`@apps.tool()` には `csp` パラメーターが存在しません。
### 可視性 {#visibility}
ツールに `visibility=["app"]` を付けると、「これはモデルのためではなく iframe のために存在する」という意味になります。
* `"model"`:モデルが呼び出せます。
* `"app"`:iframe が(`callServerTool` 経由で)呼び出せます。
* 省略:両方。これがデフォルトです。
フィルタリングは**ホスト**の仕事です。サーバーはアプリ専用のツールもほかのツールと同じように `tools/list` に載せ、ホストがそれをモデルから隠します。サーバー側でフィルタリングしないでください。
## SDK が強制するルール {#the-rules-the-sdk-enforces}
これらはすべて、本番ではなく起動時に失敗します。
* `resource_uri` やリソース URI が `ui://...` でない場合、デコレート時または登録時に `ValueError` になります。
* **対応する登録済みリソースのない** URI に紐づけられたツールは、`MCPServer(extensions=[apps])` が拡張を取り込む時点で `ValueError` になります。`resources/read` で 404 になる HTML を公開するツールは設定ミスなので、構築を拒否します。
* `@apps.tool()` に `meta={"ui": ...}` を渡すと `ValueError` になります。`_meta["ui"]` はデコレーターの管轄です。`resource_uri=` と `visibility=` で指定してください。ほかの `meta=` キーは問題なく一緒にマージされます。
現時点では、TypeScript の ext-apps SDK も FastMCP もこれらをどれも検出しません。ホストより先に自分で気づけるほうがよいと考えています。
## インライン HTML の先へ {#beyond-inline-html}
`add_html_resource` はよくあるケース、つまり HTML の文字列を扱います。それ以外、たとえばディスク上の HTML や生成されたコンテンツでは、リソースを自分で組み立てて渡してください。
```python title="server.py" hl_lines="12 18"
--8<-- "docs_src/apps/tutorial003.py"
```
`add_resource` は、リソースに MIME タイプが明示されていなければ `text/html;profile=mcp-app` を補い、明示的な不一致は拒否します。ほかの MIME タイプの `ui://` リソースは、どのホストも描画しないリソースだからです。
!!! tip
非推奨のフラットな `_meta["ui/resourceUri"]` キーをまだ読んでいる GA 前のホストを対象にしていますか? 自分でマージしてください。`@apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"})` と書きます。入れ子の `ui` オブジェクトが仕様の形で、フラットなキーはいずれなくなります。
## 動かしてみる {#see-it-run}
`examples/stories/` の `apps` ストーリーは、このページを実行可能なペアにしたものです。UI に紐づく時計ツールを持つサーバーと、Apps をネゴシエートしてツールの `_meta.ui.resourceUri` を読み、HTML を取得してツールを呼び出すクライアントです。
```bash
uv run python -m stories.apps.client
```

View file

@ -0,0 +1,172 @@
---
translation:
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
tool: 1
---
# 拡張機能 {#extensions}
**拡張機能**とは、1 つの識別子の下にまとめられた、オプトイン式の MCP の振る舞い一式です。
サーバー側では、ツール、リソース、新しいリクエストメソッドを提供でき、`tools/call` をラップすることもできます。クライアント側では、追加の `tools/call` の結果形状を引き受け(claim)、ベンダー通知を監視できます。それぞれの側が自分の `capabilities.extensions` でアドバタイズし、求めなかった人にとっては何も変わりません。これが契約です([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133))。そして黄金律が 1 つあります。**拡張機能はデフォルトでオフです**。
## 拡張機能を使う {#using-an-extension}
構築時にインスタンスを渡します。
```python title="server.py"
--8<-- "docs_src/extensions/tutorial001.py"
```
これで完了です。サーバーは `capabilities.extensions` の下で `io.modelcontextprotocol/ui` をアドバタイズし、拡張機能が提供するものをすべて配信するようになります。
`Apps` は組み込みのリファレンス拡張機能で、専用のページがあります。**[MCP Apps](apps.md)** を参照してください。
!!! note
拡張機能は構築時に固定されます。後から呼び出す `add_extension` はありません。クライアントが接続している間、サーバーのケイパビリティマップは変わるべきではないからです。
ケイパビリティマップは `server/discover` に載って運ばれます。これは **2026-07-28** の経路です。レガシーの `initialize` ハンドシェイクにはこれを載せる場所がないため、レガシークライアントには拡張機能がそもそも見えません。それを前提に設計してください。拡張機能はサーバーを「補強する」ものであり、サーバーを使う唯一の手段になってはいけません。
## 独自の拡張機能を書く {#writing-your-own}
`Extension` をサブクラス化し、必要なものだけをオーバーライドします。どのメソッドにもデフォルトがあります。
### 識別子 {#the-identifier}
```python
--8<-- "docs_src/extensions/tutorial002.py"
```
識別子は、仕様の `_meta` キーの文法に従った `vendor-prefix/name` 形式の文字列です。ドット区切りのラベル(それぞれ英字で始まり、英字または数字で終わる)、スラッシュ、そして名前が続きます。**クラスが定義された時点で**検証されるため、タイプミスがサーバーの起動まで放置されることはありません。
```text
TypeError: Stamps.identifier must be a `vendor-prefix/name` string
(reverse-DNS prefix required), got 'stamps'
```
プレフィックスには自分が管理するドメインを使ってください。`io.modelcontextprotocol/*` は MCP プロジェクト自身が仕様化する拡張機能用です。
### ツールの提供 {#contributing-tools}
役に立つ最小の拡張機能は、ツール 1 つと設定マップ 1 つです。
```python title="server.py" hl_lines="17 19-20 22-23 26"
--8<-- "docs_src/extensions/tutorial003.py"
```
* `tools()` は `ToolBinding` を返します。サーバーはそれぞれを、自分で `mcp.add_tool(...)` を呼んだ場合とまったく同じように登録します。スキーマ生成も、`Context` の注入も、何もかも同じです。
* `settings()` は `capabilities.extensions["com.example/stamps"]` にアドバタイズされる値です。設定なしで拡張機能をアドバタイズするには `{}`(デフォルト)を返してください。
* 拡張機能がサーバーを受け取ることはありません。提供するものをデータとして宣言し、`MCPServer` がそれを消費します。書き換えられる `self.server` はありません。
そして `main()` がその証明です。`mcp` に直接つなぐインメモリのクライアントです。
```python title="server.py" hl_lines="29-34"
--8<-- "docs_src/extensions/tutorial003.py"
```
### 独自メソッドの提供 {#serving-your-own-methods}
拡張機能は**新しいリクエストメソッド**を登録できます。仕様のメソッドと並んで配信される、独自の動詞(verb)です。
```python title="server.py" hl_lines="16-22 31 40-48"
--8<-- "docs_src/extensions/tutorial004.py"
```
* `SearchParams` は `RequestParams` をサブクラス化しているため、2026 の `_meta` エンベロープが一様にパースされ、ハンドラーが受け取るのは検証済みのパラメーターであって、生の dict ではありません。クライアントが制御できる値には上限を設けてください。`Field(ge=1, le=100)` は、コードが何かを割り当てる前に、ばかげた `limit` を拒否します。
* `require_client_extension(ctx, EXTENSION_ID)` がゲートです。拡張機能を宣言しなかったクライアントには `-32021`(必須のクライアントケイパビリティの欠如)エラーが返り、仕様が求める機械可読な `requiredCapabilities` ペイロードが付きます。
* `protocol_versions=frozenset({"2026-07-28"})` はメソッドを通信路上の 1 つのバージョンに固定します。他のバージョンではクライアントは `METHOD_NOT_FOUND` を受け取ります。そのバージョンにメソッドが存在しないのとまったく同じです。そのクライアントにとっては、実際に存在しません。
メソッドは**厳密に追加のみ**です。SDK はこれを実行時ではなく構築時に強制します。
* 仕様で定義されたメソッド(`tools/list`、`completion/complete` など)に対する `MethodBinding` は、バインディングの構築時に `ValueError` を送出します。コアの動詞はサーバーのものです。
* 2 つの拡張機能が同じメソッドをバインドすると、2 つ目の登録時に送出されます。後勝ちはプラグイン同士が互いを壊す原因です。SDK はそうしません。
* 空の `protocol_versions` セットも送出します。決して配信できないメソッドはバグであって、設定ではありません。
### クライアント側 {#the-client-side}
同じファイルの `main()` に、クライアント側の話がすべて、その両半分とも入っています。
```python title="server.py" hl_lines="54-58"
--8<-- "docs_src/extensions/tutorial004.py"
```
* `Client(..., extensions=[advertise(EXTENSION_ID)])` が拡張機能を宣言します。宣言は `ClientCapabilities.extensions` になります。2026-07-28 接続では、このマップはリクエストごとの `_meta` エンベロープで運ばれるため、サーバーは**すべての**リクエストでそれを見ます。レガシー接続では `initialize` ハンドシェイクに載ります。サーバーのコードはどちらでも気にしません。`require_client_extension(ctx, ...)` と `ctx.session.check_client_capability(...)` は、どちらの経路でも正しい情報源を読みます。
* ベンダーメソッドは 1 層下がって `client.session.send_request(...)` を使います。`Client` がファーストクラスのメソッドを増やすのは仕様の動詞に対してだけです。`send_request` はどんな `Request` サブクラスも受け付けるため、ベンダーリクエストはそのまま渡せます。
### `tools/call` のインターセプト {#intercepting-toolscall}
唯一の介入型フックです。ツール呼び出しを監視、短絡、または拒否するには `intercept_tool_call` をオーバーライドします。
```python title="server.py" hl_lines="17-24"
--8<-- "docs_src/extensions/tutorial005.py"
```
* `params` は検証済みの `CallToolRequestParams` です。生の JSON に触れずに `params.name` と `params.arguments` が手に入ります。どのツール呼び出しが実行されるかを決めるのもこれです。書き換えたコンテキストを `call_next` に渡して変わるのは、ハンドラーが `ctx` 上で観測するものであり、ツールの呼び出しではありません。通信路レベルのリクエスト書き換えは[ミドルウェア](middleware.md)の仕事です。
* `call_next(ctx)` はチェーンの残りを実行し、ハンドラーの結果を返します。そのまま返す(監視)、別のものを返す(置換)、または `MCPError` を送出する(拒否)のいずれかです。何を返しても、2026 年世代の `serverInfo` アイデンティティスタンプを含め、ハンドラーの結果と同様にシリアライズされるため、短絡するインターセプターが匿名またはスキーマ外のレスポンスを生み出すことはありません。
* 複数の拡張機能がある場合、インターセプターは登録順にネストします。`extensions=[...]` の最初の拡張機能が最も外側です。
* デフォルトの実装は素通しで、拡張機能がこのフックを一切オーバーライドしないサーバーでは、素の `tools/call` ハンドラーがそのまま保たれます。使わないもののコストを払うことはありません。
このフックがラップするのは `tools/call` だけです。すべてのメッセージに関わる処理には[ミドルウェア](middleware.md)を使ってください。それがミドルウェアの役目です。
## クライアント拡張機能を使う {#using-a-client-extension}
**クライアント拡張機能**は、同じ契約を利用する側から見たものです。1 つの識別子の下にまとめられたクライアント側の振る舞い一式です。インスタンスを `Client(extensions=[...])` に渡し、通常どおりツールを呼び出します。
```python title="client.py" hl_lines="66-68"
--8<-- "docs_src/extensions/tutorial006.py"
```
`call_tool("buy", ...)` は、他のすべての呼び出しと同様にプレーンな `CallToolResult` を返します。拡張機能が変えたのは次の点です。サーバーは `buy` に対して、最終結果の代わりに `receipt` という**結果の形状**で答えられるようになり、`call_tool` が戻る前に `Receipts` がそれを完了させます(ここでは後続の呼び出しでレシートを引き換えます)。呼び出し側のコードは何も変わりません。
拡張機能を外せば、このどれも存在しません。サーバーのゲートは宣言しなかったクライアントを拒否し(エラー -32021)、ゲートを省いたサーバーから届いた引き受け対象の形状は検証に失敗します。認識できない `resultType` に対して仕様が求めるとおりです。通信路の両端で、デフォルトはオフです。
クライアント側の振る舞いを**一切持たない**識別子をアドバタイズするには(サーバーがケイパビリティでゲートし、クライアントは何もしない、上の検索クライアントのような場合)、`advertise()` を使います。
```python
from mcp.client import advertise
client = Client(mcp, extensions=[advertise("com.example/search")])
```
## クライアント拡張機能を書く {#writing-a-client-extension}
`ClientExtension` をサブクラス化し、必要なものだけをオーバーライドします。提供できるものは 3 種類で、それぞれにデフォルトがあります。`settings()`、`claims()`、`notifications()` です。
```python title="client.py" hl_lines="17-18 43-44 46-47"
--8<-- "docs_src/extensions/tutorial006.py"
```
* 識別子はサーバー側と同じ文法に従い、クラスの定義時に検証されます。
* `claims()` は `ResultClaim` を返します。通信上のタグ、それをパースするモデル、それを完了させるリゾルバーの組です。モデルは `result_type: Literal["receipt"]` でタグを固定しなければならず、その動詞のコア結果型をサブクラス化してはいけません。どちらも引き受けの構築時に強制されます。`receipt_token` のようなベンダーフィールドはそのまま通信路を流れます。差し替えられた形状はそのままの形でクライアントに届きます。
* リゾルバーはパース済みのモデルと `ClaimContext` を受け取ります。`ctx.session` は `client.session` と同じ公開ハンドルなので、後続の処理は通常のセッション呼び出しです。戻り値はその動詞の通常の `CallToolResult` です。
* `settings()` は `ClientCapabilities.extensions[identifier]` にアドバタイズされる値で、`Client` の構築時に一度だけ読み取られます。
`notifications()` は、監視するベンダーのサーバー通知を宣言します。
```python
def notifications(self) -> Sequence[NotificationBinding[Any]]:
return [NotificationBinding(method="notifications/receipts", params_type=ReceiptEvent, handler=self.on_receipt)]
```
ハンドラーは検証済みのパラメーターをディスパッチ順に 1 つずつ受け取ります。監視するだけで、拒否も返信もできません。
目立たないルールが 2 つあります。引き受けが有効なのは 2026-07-28 接続だけで、ケイパビリティのアドバタイズもそれに従います。レガシー接続では引き受けは消え、識別子も一緒にアドバタイズから外れるため、自分が拒否してしまう形状を持つ拡張機能をクライアントがアドバタイズすることはありません。また、リゾルバーではなく自分で引き受け対象の形状を受け取りたいときは、`client.session.call_tool(..., allow_claimed=True)` を呼び出してください。このフラグがないと、セッション層の呼び出し側に届いた引き受け対象の形状は `UnexpectedClaimedResult` を送出します。
### 拡張機能の動詞 {#extension-verbs}
拡張機能独自のリクエストメソッドには、クライアント側の登録は不要です。ベンダーリクエスト型は `mcp.types.Request` をサブクラス化し、[独自メソッドの提供](#serving-your-own-methods)と同様に `client.session.send_request` を通ります。追加が 1 つあります。パラメーターのキーを `Mcp-Name` ヘッダーに載せなければならない場合(tasks のような拡張機能の仕様では、その動詞にこれが必要です)、リクエスト型は `name_param` を宣言します。
```python title="client.py" hl_lines="22-25 46-47"
--8<-- "docs_src/extensions/tutorial007.py"
```
セッションはどの送信経路でも `params["jobId"]` を `Mcp-Name` に反映し、値が欠けている場合は必須ヘッダーを黙って省くのではなく、はっきりとエラーになります。
## 拡張機能にできないこと {#what-an-extension-cannot-do}
提供できる範囲は意図的に**閉じて**います。サーバー側では、設定、ツール、リソース、メソッド、`tools/call` のインターセプター 1 つ。クライアント側では、設定、結果の引き受け、通知のバインディング。拡張機能には次のことができません。
* **ホストの内部に手を伸ばすこと。** データを宣言するだけで、サーバーやクライアントへの参照は持ちません。
* **コアの振る舞いを置き換えること。** 仕様のメソッドとコアの結果タグは構築時に拒否されます(`initialize` はランナーが完全に予約しています)。コアの語彙に隠れた通知バインディングは、代わりに警告を出して沈黙します。
* **後から登録すること。** `MCPServer(...)` や `Client(...)` が戻った後は、拡張機能の集合はそのまま確定です。
これらの壁と戦っているなら、書いているのは拡張機能ではありません。フォークです。壁こそが機能です。`extensions=[Apps(), Stamps()]` を読んだユーザーは、この 2 つが触れた可能性のあるものを「すべて」把握できます。

View file

@ -0,0 +1,22 @@
---
translation:
sections: [ca6988b7503cd2d3]
tool: 1
---
# 高度なトピック {#advanced}
通常のサーバーやクライアントに必要なものは、すべて上のセクションのいずれかにトピック別にまとまっています。このセクションは、`MCPServer` の便利なレイヤーがかえって邪魔になるときに使う抜け道です。
* **[低レベルの Server](low-level-server.md)**:`MCPServer` の土台になっているクラスです。スキーマは手書き、ハンドラーは `on_*`、代わりにチェックしてくれるものは何もなく、独自のカスタム JSON-RPC メソッドも定義できます。
* **[ページネーション](pagination.md)** と **[ミドルウェア](middleware.md)**:どちらも低レベルの `Server` でしかできないことです。
* **[拡張機能](extensions.md)** と **[MCP Apps](apps.md)**:プロトコルの拡張のための領域です。拡張パッケージをサーバーに組み込むことも、自分で書くこともできます。
ここにありそうだと思われるもののいくつかは、実際に使う場所のほうに置かれています。
* **認可**は **[サーバーの実行](../run/index.md)** の下にあります。サーバーを保護するのはデプロイする場所だからです。
* **OAuth**、**ID アサーション**、**複数のサーバー**への接続、そしてレスポンスの**キャッシュ**は、すべて **[クライアント](../client/index.md)** の下にあります。
* **マルチラウンドトリップ(multi-round-trip)リクエスト**と**サブスクリプション**は **[ハンドラーの中で](../handlers/index.md)** の下にあります。どちらもハンドラーが「行う」ことだからです。
* **URI テンプレート**は **[サーバー](../servers/index.md)** の下、リソースの隣にあります。
* **[プロトコルバージョン](../protocol-versions.md)** と **[非推奨の機能](../deprecated.md)** には、それぞれ専用のトップレベルページがあります。
このセクションが必要かどうか迷っているなら、必要ありません。

View file

@ -0,0 +1,206 @@
---
translation:
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
tool: 1
---
# 低レベルの Server {#the-low-level-server}
`@mcp.tool()` はひとつの層です。その下には 2 つ目のサーバークラス `Server` があり、生の MCP を話します。プロトコルオブジェクトを渡すと、それをそのまま通信路に載せます。
`MCPServer` はその上に作られています。便利な層が邪魔になるときは、下に降ります。
* Python のシグネチャから導出されたものではなく、**正確な**スキーマ(ファイルから読み込んだもの、データベースから生成したもの)を出力する必要がある。
* 結果を完全に制御する必要がある。`_meta`、`is_error`、`structured_content` のすべてのキー。
* MCP が定義していないメソッドを扱う必要がある。
それ以外はすべて、`MCPServer` のままで構いません。
## 同じツールを手書きする {#the-same-tool-by-hand}
これは **[ツール](../servers/tools.md)** が `@mcp.tool()` を使って 9 行で書いている `search_books` ツールから、糖衣構文を取り除いたものです。
```python title="server.py" hl_lines="22 26 32"
--8<-- "docs_src/lowlevel/tutorial001.py"
```
変わったのは 3 つで、それが低レベル API のすべてです。
* **ハンドラーはコンストラクターのパラメーターです。** `on_list_tools=` と `on_call_tool=` を `Server(...)` に渡します。この層にデコレーターはなく、すべてのハンドラーが同じ形 `async (ctx, params) -> result` をしています。
* **入力スキーマは自分で書きます。** `Tool.input_schema` は素の JSON Schema の `dict` です。誰も型ヒントから導出してはくれません。導出元になる型ヒントがないからです。
* **結果は自分で組み立てます。** `CallToolResult(content=[TextContent(...)])` を手で書きます。ラップされるものも、変換されるものも、戻り値のアノテーションから推論されるものもありません。
`params` はパース済みのリクエストです。`CallToolRequestParams` からは `.name` と `.arguments` が取れます。`ctx` は `ServerRequestContext` です。クライアントに話しかけるための `ctx.session`、`ctx.lifespan_context`、`ctx.request_id`、そして受信したリクエストの `_meta` である `ctx.meta` があります。
!!! info
FastAPI を使ったことがあれば、この関係はもう知っています。`MCPServer` はデコレーターと型ヒントの層で、`Server` はその下の Starlette です。両者は競合するものではありません。`MCPServer` は `Server` を構築し、まさにこのようなハンドラーをそこに登録します。
### 試してみる {#try-it}
これには Inspector がありません。`mcp dev` と `mcp run` は `MCPServer` しか受け付けないからです。インメモリの `Client` は気にしません。`MCPServer` を受け取るのとまったく同じように、低レベルの `Server` を受け取ります。
```python title="main.py"
import asyncio
from mcp import Client
from server import server
async def main() -> None:
async with Client(server) as client:
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
print(result.content)
asyncio.run(main())
```
```text
[TextContent(type='text', text="Found 3 books matching 'dune' (showing up to 5).", annotations=None, meta=None)]
```
`@mcp.tool()` 版が出力したのと同じテキストです。正直に言うと、違いが 2 つあります。
* `result.structured_content` は `None` です。高レベルのサーバーは `-> str` を `{"result": ...}` にラップしてくれますが、ここでは自分で組み立てなかったものを誰も組み立ててくれません。
* `list_tools` は**自分で**打ち込んだスキーマを一字一句そのまま返します。高レベル版にはすべてのプロパティに `"title": "Query"` があり、ルートに `"title": "search_booksArguments"` がありました。Pydantic の産物です。この層では、通信上に現れるものはすべて自分が載せたものです。
## 何もチェックされない {#nothing-is-checked-for-you}
`MCPServer` は、生成したスキーマに照らして呼び出しを検証し、関数が実行される前に不正な引数を拒否します(**[ツール](../servers/tools.md)**)。
`Server` はそれをしません。`input_schema` はクライアントに「公開」されますが、`params.arguments` に「適用」されることは決してありません。
!!! check
`limit` なしで `search_books` を呼び出すと、`args["limit"]` が `KeyError` を送出します。クライアントに見えるのは次のとおりです。
```text
MCPError: Internal server error
```
JSON-RPC エラー、コード `-32603`、メッセージは意図的に一般的なものです。SDK はトレースバックをリモートの呼び出し側に漏らしません。モデルは自分が何を間違えたのか知ることができないので、再試行もできません。(テストでは、`raise_exceptions=True` を指定すると代わりに本当の例外が表に出ます。**[テスト](../get-started/testing.md)** を参照してください。)
これは一般化できます。低レベルのハンドラーから送出された例外は**常に**プロトコルエラーであり、`is_error=True` のツール結果になることはありません。モデルに失敗を読ませて回復させたいなら、`params.arguments` を自分で検証し、`CallToolResult(content=[TextContent(...)], is_error=True)` を返してください。この 2 種類の失敗が **[エラーの処理](../servers/handling-errors.md)** の主題です。
## 2 つのツール、1 つのハンドラー {#two-tools-one-handler}
`on_call_tool` はサーバー上のすべてのツールの唯一の入り口です。`params.name` で振り分けます。
```python title="server.py" hl_lines="38-43"
--8<-- "docs_src/lowlevel/tutorial002.py"
```
* `list_tools` は両方を公開します。`call_tool` は名前でディスパッチします。
* `else` 分岐は重要です。`Server` は、一度もリストに載せていない名前への `tools/call` でも、そのままハンドラーに転送してしまいます。そこで例外を送出すると、呼び出しは上と同じ `-32603` になります。
## 構造化出力を手書きする {#structured-output-by-hand}
`Tool` に `output_schema` を宣言し、結果に `structured_content` を載せます。どちらも自分の責任です。
```python title="server.py" hl_lines="19-23 36"
--8<-- "docs_src/lowlevel/tutorial003.py"
```
呼び出すと、結果は両方の表現を持ちます。
```json
{
"content": [{"type": "text", "text": "Found 3 books matching 'dune'."}],
"structuredContent": {"matches": 3, "query": "dune"},
"isError": false,
"resultType": "complete",
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}}
}
```
`_meta` ブロックはサーバーの識別スタンプです。SDK は 2026 年世代のすべての結果にこれを追加し、`version` はコンストラクターの値を使います(何も設定していないサーバーは空文字列を報告します)。自身を識別してはならないサーバーは、返す結果を所有するミドルウェアでこのキーを取り除けます。
サーバーは 2 つのフィールドを比較しません。この SDK の `Client` は比較します。宣言した `output_schema` を満たさない `structured_content` を返すと、`call_tool` は `Invalid structured content returned by tool search_books` で始まり、続けて `jsonschema` の失敗内容を引用する `RuntimeError` を送出します。スキーマを約束するのは簡単ですが、守るのは自分の仕事です。戻り値の型とスキーマの全段階については **[構造化出力](../servers/structured-output.md)** を参照してください。
## `_meta`:モデルではなくアプリケーションのために {#\_meta-for-the-application-not-the-model}
`content` は答えのうちモデルが読む部分です。`structured_content` は同じ答えを型付きデータにしたものです。`_meta` は 3 つ目のチャネルで、答えの一部ではまったくなく、**クライアントアプリケーション**のために結果に同乗するデータです。
レコード ID、トレース ID など、UI が必要としプロンプトが必要としないものに使います。
```python title="server.py" hl_lines="37"
--8<-- "docs_src/lowlevel/tutorial004.py"
```
* 構築するときは通信路上の名前である `_meta=` を使います。クライアントは `result.meta` として読み出します。
* キーには名前空間を付けてください(`bookshop/record_ids`)。`io.modelcontextprotocol/*` のキーはプロトコルが予約しています。
!!! warning
`_meta` はサーバーとクライアントアプリケーションの間の取り決めであり、何がモデルに届くかについての保証ではありません。何を描画するかはホストが決めます。ツール結果のどの部分にも、決して秘密情報を入れないでください。
## ケイパビリティはハンドラーに従う {#capabilities-follow-your-handlers}
`Server` は、ハンドラーを渡したメソッド群だけを正確に公開します。上の `Bookshop` は `on_list_tools` と `on_call_tool` だけを渡しているので、接続したクライアントには次のように見えます。
```json
{"tools": {"listChanged": false}}
```
`resources` も `prompts` もありません。裏付けるものがないからです。`on_list_prompts` を渡せば `prompts` が現れ、`on_completion` を渡せば `completions` が現れます。
`MCPServer` は、何かを登録したかどうかにかかわらず、常にツール、リソース、プロンプトを公開します。そのマネージャーが常に存在するからです。この層では、宣言とはコンストラクター呼び出しそのものです。
## ライフスパンのジェネリック {#the-lifespan-generic}
`Server` は、そのライフスパンが yield する型についてジェネリックです。一度アノテーションを付ければ、そのオブジェクトは現れる場所すべてで型が付きます。
```python title="server.py" hl_lines="24-26 44-45 50"
--8<-- "docs_src/lowlevel/tutorial005.py"
```
* ライフスパンは `Callable[[Server[Catalog]], AbstractAsyncContextManager[Catalog]]` です。`async` ジェネレーターに `@asynccontextmanager` を付けると、まさにそれが得られます。
* `yield` したものが `ctx.lifespan_context` になり、ハンドラーに `ServerRequestContext[Catalog]` とアノテーションが付いているので、`.search(...)` が補完され、型チェックされます。
* サーバーの起動時に一度入り、停止時に一度出ます。起動、後始末、そして同じ考え方の `MCPServer` 版については **[ライフスパン](../handlers/lifespan.md)** を参照してください。
`lifespan=` がなければ、`ctx.lifespan_context` は空の `dict` です。
## 独自のメソッド {#a-method-of-your-own}
コンストラクターは MCP が定義するメソッドを扱います。`add_request_handler` はそれ以外のすべてを扱います。
```python title="server.py" hl_lines="35-36 39-40 43-44 48"
--8<-- "docs_src/lowlevel/tutorial006.py"
```
* 最初の引数はメソッド文字列です。通知には対になる `add_notification_handler` があります。
* `params_type` は、受信した `params` をハンドラーの実行**前**に検証するためのモデルです。つまり、カスタムメソッドはツールが受けられない検証を受けられます。`_meta` フィールドがほかのメソッドと同じようにパースされるよう、`RequestParams` をサブクラス化してください。
* ハンドラーは `BaseModel`、`dict`、または `None` を返します。SDK がそれを JSON-RPC の結果にシリアライズします。
正直な注意点が 1 つあります。高レベルの `Client` には MCP が定義するメソッドの動詞しかないので、`client.reindex()` はありません。ベンダーメソッドは、その存在をすでに知っている相手のためのものです。一緒に配布するクライアントや、JSON-RPC を話す自前の別のサービスなどです。
自分のものにできないメソッドが 1 つあります。
```text
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
use Server.middleware to observe or wrap initialization
```
ハンドシェイクはランナーのものです。`server/discover`、`ping`、その他すべての組み込みは自由に置き換えられます。
!!! tip
このエラーで言及されている `Server.middleware` は、`initialize` を含む**すべての**受信メッセージをラップします。新しいメソッドに応答するのではなく、トラフィックを観察したり書き換えたりしたいなら、**[ミドルウェア](middleware.md)** から始めてください。
## その他のハンドラー {#the-other-handlers}
以下はどれも、ここまでで身につけた語彙で理解できる考え方です。それぞれに専用のページがあります。
* `on_call_tool`、`on_get_prompt`、`on_read_resource` は、通常の結果の代わりに `InputRequiredResult` を返して呼び出しを一時停止し、クライアントに入力を求めることができます。**[マルチラウンドトリップ(multi-round-trip)リクエスト](../handlers/multi-round-trip.md)** を参照してください。この層らしく、何も代わりにインストールされません。`MCPServer` はデフォルトで `requestState` を封印しますが、ここでは設定した `request_state` は書いたとおりに通信路を渡ります。`server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))` でオプトインするまではそうです。この 1 行(どちらの名前も `mcp.server.request_state` からインポートします)で、`MCPServer` が行うのとまったく同じ封印と検証が得られます(**[`requestState` の保護](../handlers/multi-round-trip.md#protecting-requeststate)**)。
* `on_list_resources`、`on_read_resource`、`on_list_prompts`、`on_get_prompt`、`on_completion` は、ほかのプリミティブ向けの同じ `(ctx, params) -> result` の形です。
* `on_subscriptions_listen` は 2026-07-28 の `subscriptions/listen` ストリームを提供します。`SubscriptionBus` の上に構築した `ListenHandler` を渡し、ほかのハンドラーからバスにイベントを発行してください。全体の組み立て方については **[サブスクリプション](../handlers/subscriptions.md)** を参照してください。
* `server.streamable_http_app()` は `MCPServer` のものと同じ Starlette アプリを返します。**[サーバーの実行](../run/index.md)** がほかの ASGI アプリをデプロイするのと同じ方法でデプロイしてください。この層には `server.run(transport=...)` はありません。`server.run(read_stream, write_stream, server.create_initialization_options())` が 1 組のストリーム上で 1 つの接続を駆動し、その 1 行がすべてです。
## まとめ {#recap}
* 低レベルの `Server` はハンドラーを `on_*` の**コンストラクターパラメーター**として受け取ります。すべてのハンドラーは `async (ctx, params) -> result` です。
* `input_schema` の dict は自分で書き、`CallToolResult` は自分で組み立てます。導出も、ラップも、検証も、代わりにしてくれるものはありません。
* ハンドラー内の例外は `-32603` のプロトコルエラーです。モデルが読めるツールエラーは、`is_error=True` を付けて**自分で**返す `CallToolResult` です。
* 結果の `_meta` はモデルではなくクライアントアプリケーション宛てです。
* `Server[T]` はライフスパンが yield するものについてジェネリックで、`ctx.lifespan_context` は型付きの `T` です。
* `add_request_handler(method, params_type, handler)` は任意のメソッドを提供します。`initialize` は予約されています。
* `Server` が公開するケイパビリティは、どのハンドラーを登録したかから導出されます。
`Client(server)` が両方のサーバーを同じように扱ったのは、両者がまさに同じプロトコルだからであり、それこそが要点です。さらに下の層はクラスですらありません。**[ミドルウェア](middleware.md)** です。

View file

@ -0,0 +1,84 @@
---
translation:
sections: [6048b4f308edbb8c, 068bda0f21ee9c1b, c3e565b61acd75c5, c62422b159c6ed09, 47204fab253cc45c]
tool: 1
---
# ミドルウェア {#middleware}
**ミドルウェア**とは、サーバーが受け取るすべてのメッセージを包み込む 1 つの非同期関数です。
`async (ctx, call_next)` の形で書き、`server.middleware` に追加します。API はこれだけです。
!!! warning
ミドルウェアのリストは、ソース上で**暫定(provisional)**とマークされています。シグネチャやセマンティクスは 2.x のマイナーリリースで変わる可能性があります。メッセージを「観察」する(計時、ログ、トレース)ため、あるいは「拒否」するために使ってください。サーバーの土台にはしないでください。
`MCPServer` は構築時にこのリストを受け取り(`MCPServer(name, middleware=[...])`)、`mcp.middleware` として公開します。低レベルの `Server` も同じリストを `server.middleware` として公開します。以下の例では低レベルの `Server` を使います。`Server(name, on_call_tool=...)` に馴染みがなければ、先に**[低レベルの Server](low-level-server.md)** を読んでください。
## 計時ミドルウェア {#a-timing-middleware}
サーバー 1 つ、ツール 1 つ、そして各メッセージにかかった時間をログに出すミドルウェア 1 つです。
```python title="server.py" hl_lines="39-45 49"
--8<-- "docs_src/middleware/tutorial001.py"
```
* `ctx` はハンドラーが受け取るのと同じ `ServerRequestContext` です。`ctx.method` は生のメソッド文字列、`ctx.params` はバリデーション**前**の生のパラメーターです。
* `call_next(ctx)` はチェーンの残り、つまりバリデーション、ハンドラーの検索、ハンドラー本体を実行します。返ってきたものをそのまま返せば、レスポンスには手が加わりません。
* `try`/`finally` は意図的なものです。ハンドラーが例外を送出しても計時されます。失敗は `call_next` から出てくる例外としてミドルウェアに届くからです。
* `server.middleware.append(...)` で登録します。リストは外側から順に実行されるので、`middleware[0]` が通信路に最も近いミドルウェアです。
### 試してみる {#try-it}
クライアントを接続し、ツールを一覧し、1 つ呼び出してください。ログには **3 行**出ます。
```text
server/discover took 18.3 ms
tools/list took 0.1 ms
tools/call took 0.1 ms
```
呼び出しは 2 回なのに、行は 3 つです。最初の行は `server/discover`、つまり何かを要求する前に、クライアントが接続をセットアップするために送ったリクエストです。
ここがポイントです。ミドルウェアは受信する**すべての**メッセージを包みます。
* 接続のセットアップ。`server/discover`、あるいはレガシーセッションでは `initialize` と `notifications/initialized` です。
* すべてのリクエストとすべての通知。通知の場合は `ctx.request_id is None` であり、`call_next(ctx)` は `None` を返し、何を返しても破棄されます。
* サーバーにハンドラーがないメソッドでさえ対象です。`call_next` は `MCPError(-32601, "Method not found")` を送出し、それがミドルウェアを「通り抜けて」クライアントへ向かいます。
## ミドルウェアの中でできること {#what-you-can-do-inside-one}
ためらうべき度合いが小さいものから順に並べます。
* **観察する。** 時間を計る、数える、ログに出す。上の例がこれです。
* **拒否する。** `call_next(ctx)` を呼ぶ「代わりに」`MCPError` を送出すると、そのメッセージ 1 つに JSON-RPC エラーで応答します。接続は維持され、次のメッセージは通ります。サーバーが呼び出し側ごとに `subscriptions/listen` を制御するのはこの方法です。サブスクリプションのページの**[誰が監視できるかを決める](../handlers/subscriptions.md#deciding-who-may-watch)**で順を追って説明しています。
* **書き換える。** `ctx` はデータクラスです。`await call_next(dataclasses.replace(ctx, params=...))` とすると、クライアントが送ったものとは異なるパラメーターをチェーンの残りに渡せます。`initialize` に対しては決して行わないでください。クライアントが受け取る結果は書き換えたパラメーターから組み立てられますが、サーバーは元の通信路上のパラメーターから接続状態を確定します。両者が、ネゴシエートした内容について食い違ったままハンドシェイクを終える可能性があります。
* **応答する。** `call_next(ctx)` を呼ばずに結果を返すと、それがレスポンスとしてクライアントへ送られます。`call_next` が渡してくるのは完成した送信形式であり、パイプラインは返したものに一切手を加えないので、エンベロープ全体が自分の責任になります。2026 年世代の接続ではこれに `serverInfo` の `_meta` スタンプが含まれます。SDK はハンドラーの結果にはこれを付けますが、ミドルウェアが返すものには付けません。
!!! check
`initialize` もミドルウェアが包むものの 1 つであり、ミドルウェアはそのための「唯一の」フックです。`add_request_handler` で乗っ取ろうとすると、SDK は拒否します。
```text
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
use Server.middleware to observe or wrap initialization
```
!!! warning
`initialize` はインラインで処理されます。ミドルウェアチェーンが返るまで、サーバーはそれ以上の受信メッセージを読みません。そのため、`initialize` の処理中にサーバーからクライアントへのリクエスト(`ctx.session.send_request(...)` やエリシテーション(elicitation))を await すると、**接続がデッドロックします**。待っているレスポンスは決して読まれないからです。送りっぱなしの通知は問題ありません。
## デフォルトで有効な唯一のミドルウェア {#the-one-middleware-that-ships-on-by-default}
SDK が同梱するミドルウェアはちょうど 1 つで、すでにサーバーのリストに載っています。すべてのメッセージに対して OpenTelemetry のスパンを発行するミドルウェアです。自分で追加する必要はなく、ほとんどの場合は意識することもありません。エクスポーターをインストールするまでは何もしません。専用のページがあります。**[OpenTelemetry](../run/opentelemetry.md)** を参照してください。
!!! info
ASGI ミドルウェアを書いたことがあれば、この形はもう知っています。Starlette の `(scope, receive, send)` が `(ctx, call_next)` になり、トランスポートの「後」で、生の HTTP リクエストではなくデコード済みのメッセージに対して動きます。2 つは組み合わせられます。`streamable_http_app()` 上の Starlette ミドルウェアは HTTP を見て、こちらは MCP を見ます。
## まとめ {#recap}
* ミドルウェアは `async (ctx, call_next) -> result` です。`MCPServer(middleware=[...])` として渡すか(または `mcp.middleware` に追加し)、低レベルの `Server` では `server.middleware` に追加します。
* 受信する**すべての**メッセージ(`server/discover`、`initialize`、リクエスト、通知、未知のメソッド)を包み、外側から順に実行されます。
* `ctx.request_id is None` で、通知とリクエストを見分けます。
* `call_next` を呼ぶ代わりに例外を送出すると、メッセージを 1 つ拒否できます。接続は維持されます。
* SDK 自身の OpenTelemetry トレースもミドルウェアであり、すでにリストに載っています。**[OpenTelemetry](../run/opentelemetry.md)** を参照してください。
* この仕組み全体が暫定です。観察には使っても、その上に何かを築かないでください。
リクエストを包むものはこれですべてです。**[認可](../run/authorization.md)**は、そもそもそのリクエストを実行させるかどうかを決めるものです。

View file

@ -0,0 +1,81 @@
---
translation:
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
tool: 1
---
# ページネーション {#pagination}
ほとんどのサーバーには必要ありません。
`MCPServer` はすべての `list_*` リクエストに対して、持っているものを全部 1 ページにまとめ、`next_cursor=None` で返します。ツールやリソース、プロンプトが数十個程度なら、それが正しい答えであり、設定することは何もありません。
ページネーションは、リソース一覧が実質的にデータベースであるようなサーバーのためのものです。数千行もあり、1 つのレスポンスにシリアライズするわけにはいかない場合です。プロトコルの答えは**カーソル**です。サーバーはページと不透明なトークンを返し、クライアントはそのトークンを送り返して次のページを取得します。
`@mcp.resource()` にはそのためのフックがありません。ページングするには、**[低レベルの Server](low-level-server.md)** の上で、リストハンドラーを自分で書きます。
## ページングするサーバー {#a-server-that-pages}
```python title="server.py" hl_lines="12 15-16"
--8<-- "docs_src/pagination/tutorial001.py"
```
* 低レベルの `Server` では、ハンドラーはデコレーターではなくコンストラクターの引数です。`on_list_resources` がすべての `resources/list` リクエストに応答します。接続の仕組みはこれだけです。
* ページングするハンドラーはすべて `params: PaginatedRequestParams | None` という型で、この例は両方を受け付けます。ただし接続越しでは、SDK が `None` を渡すことはありません(`params` メンバーのないリクエストは、デフォルト値を持つモデルとしてハンドラーに届きます)。したがって重要なシグナルは `params.cursor is None`、つまり**先頭から始める**ということです。
* カーソルが「何であるか」は自分で決めます。ここでは文字列として表現したオフセットです。タイムスタンプでも主キーでも base64 のかたまりでもよく、返すときに発行でき、戻ってきたときに認識できるものなら何でもかまいません。
* `next_cursor=None` が「これが最後のページでした」と伝える方法です。件数も、合計も、`has_more` もありません。`None` がシグナルのすべてです。
!!! tip
`PAGE_SIZE` を 10 にしているのは、例を読みやすくするためです。実際の値はエンドポイントごとに選んでください。1 行のリソースが並ぶ一覧なら 1 ページ 500 件でも問題ありませんが、大きなプロンプトテンプレートの一覧ではそうはいきません。クライアントに選択の余地はなく、それは意図された設計です。
### 試してみる {#try-it}
`Client(server)` は、`MCPServer` に接続するのとまったく同じように、低レベルの `Server` にメモリ内で接続します。
引数なしで `list_resources()` を呼び出してください。`book-1` から `book-10` までの 10 個のリソースが返り、`next_cursor` は文字列 `"10"` です。
それを `list_resources(cursor="10")` として返すと、最初のリソースは `book-11` になり、新しい `next_cursor` は `"20"` です。
10 ページ目は `next_cursor` が `None` に設定されて返ってきます。これで完了です。
## クライアントのループ {#the-client-loop}
`Client` のすべての `list_*` メソッド(`list_tools`、`list_resources`、`list_resource_templates`、`list_prompts`)は `cursor=` キーワードを受け取ります。ページングされた一覧をすべて取り出すには、`while True` を 1 つ書くだけです。
```python title="client.py" hl_lines="26-32"
--8<-- "docs_src/pagination/tutorial002.py"
```
* `cursor` は `None` から始まるので、最初のリクエストにはカーソルがありません。
* `next_cursor` を見る**前に** extend してください。最後のページにもリソースはあります。
* `next_cursor is None` が出口です。それ以外はそのまま、手を加えずに `cursor=` に戻します。
その `main()` を実行すると `100 resources` と表示されます。10 件ずつの 10 ページが、10 ページあることなど知らないループによってつなぎ合わされた結果です。
これは **[クライアント](../client/index.md)** がすべての `list_*` メソッドについて示しているのと同じループで、ページングしないサーバーに対してもコストはかかりません。最初のレスポンスで `next_cursor` が `None` になり、ループは 1 回だけ回ります。
## 3 つのルール {#the-three-rules}
**カーソルは不透明です。** クライアントはカーソルを解析したり、組み立てたり、推測したりしてはいけません。カーソルの正当な出どころは、前のページの `next_cursor` をそのまま使うことだけです。
**ページサイズはサーバーが決めます。** プロトコルに `limit=` はありません。別のページサイズが必要なら、サーバーを変更します。
**ページングを無視するクライアントもそのまま動きます。** `list_resources()` を 1 回呼び、最初の 10 件を受け取り、捨ててしまった `next_cursor` に気づくことはありません。何も壊れません。見えるものが少ないだけです。
!!! check
不透明とは本当に不透明ということです。カーソルをでっち上げても(`list_resources(cursor="page-2")`)、プロトコルにできることは何もありません。このサーバーは `int("page-2")` を試み、ハンドラーが例外を送出し、クライアントに返ってくるのは次のとおりです。
```text
MCPError(-32603, 'Internal server error', None)
```
サーバーから受け取ったものではないカーソルはバグであり、機能要望ではありません。
## まとめ {#recap}
* `MCPServer` はすべてを 1 ページで返します。ページネーションはオプトインであり、低レベルの `Server` でオプトインします。
* `on_list_resources`(および `on_list_tools`、`on_list_prompts`、`on_list_resource_templates`)は `PaginatedRequestParams | None` を受け取ります。最初のページでは `params.cursor` が `None` です。
* ページと `next_cursor` を返します。後で認識できる任意の文字列か、残りが何もないときは `None` です。
* クライアントのループは、`cursor=` を渡し、蓄積し、`next_cursor is None` になるまで繰り返します。
* カーソルは不透明で、ページサイズはサーバーが決め、ページングしないクライアントも 1 ページ目は受け取れます。
手書きの `Server` API の残り(`on_call_tool`、`input_schema` の dict、`_meta`)は **[低レベルの Server](low-level-server.md)** にあります。