docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
50 changed files with 89737 additions and 54 deletions
121
i18n/ja/pages/advanced/apps.md
Normal file
121
i18n/ja/pages/advanced/apps.md
Normal 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
|
||||
```
|
||||
172
i18n/ja/pages/advanced/extensions.md
Normal file
172
i18n/ja/pages/advanced/extensions.md
Normal 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 つが触れた可能性のあるものを「すべて」把握できます。
|
||||
22
i18n/ja/pages/advanced/index.md
Normal file
22
i18n/ja/pages/advanced/index.md
Normal 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)** には、それぞれ専用のトップレベルページがあります。
|
||||
|
||||
このセクションが必要かどうか迷っているなら、必要ありません。
|
||||
206
i18n/ja/pages/advanced/low-level-server.md
Normal file
206
i18n/ja/pages/advanced/low-level-server.md
Normal 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)** です。
|
||||
84
i18n/ja/pages/advanced/middleware.md
Normal file
84
i18n/ja/pages/advanced/middleware.md
Normal 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)**は、そもそもそのリクエストを実行させるかどうかを決めるものです。
|
||||
81
i18n/ja/pages/advanced/pagination.md
Normal file
81
i18n/ja/pages/advanced/pagination.md
Normal 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)** にあります。
|
||||
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)** のページを参照してください。
|
||||
86
i18n/ja/pages/deprecated.md
Normal file
86
i18n/ja/pages/deprecated.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
---
|
||||
translation:
|
||||
sections: [20541a40dbdd5980, 01262a123ad9501d, 429db5b574a2ac08, 56b2d49da412cb28, 6a1717123fe4513c]
|
||||
tool: 1
|
||||
---
|
||||
# 非推奨の機能 {#deprecated-features}
|
||||
|
||||
2026-07-28 の仕様では、5 つのものが役目を終えます。SDK は今もその 5 つすべてを実装しており、そのすべてに**非推奨の警告**が付くようになりました。
|
||||
|
||||
下の表は、非推奨になった機能それぞれについて、なくなる理由と、代わりに土台にすべきものを挙げています。
|
||||
|
||||
## 非推奨になるもの {#what-is-deprecated}
|
||||
|
||||
| 非推奨 | 理由 | 代わりにすること |
|
||||
|---|---|---|
|
||||
| **ルート(roots)**:`ctx.session.list_roots()`、`client.send_roots_list_changed()`、`Client(...)` に渡す `list_roots_callback=` | [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) がこのケイパビリティを非推奨にします。 | パスを通常のツール引数やリソース URI として受け取るか、`InputRequiredResult` に `ListRootsRequest` を埋め込みます(**[マルチラウンドトリップ(multi-round-trip)リクエスト](handlers/multi-round-trip.md)** を参照)。 |
|
||||
| **サーバー起点のサンプリング**:`ctx.session.create_message()`、`Client(...)` に渡す `sampling_callback=` | SEP-2577 がこのケイパビリティを非推奨にします。 | `InputRequiredResult` を返し、クライアントに呼び出しを再試行させます(**[マルチラウンドトリップリクエスト](handlers/multi-round-trip.md)** を参照)。 |
|
||||
| **プロトコルのロギング**:`ctx.log()`、`ctx.debug()`、`ctx.info()`、`ctx.warning()`、`ctx.error()`、`ctx.session.send_log_message()`、`client.set_logging_level()` | SEP-2577 がこのケイパビリティを非推奨にします。プロトコル内でこれに代わるものはありません。 | stderr へ出力する通常の `import logging`(**[ロギング](handlers/logging.md)** を参照)。 |
|
||||
| **`ping`**:`client.send_ping()` | 単なる非推奨ではなく、プロトコルから**削除**されました。2026-07-28 には `ping` メソッドがありません。 | 何もありません。`mode="legacy"` の接続に対してしか動作しません。 |
|
||||
| **クライアントからサーバーへの進捗**:`client.send_progress_notification()` | 2026-07-28 では、進捗はサーバーからクライアントへの方向だけになります。 | 送るものはありません。進捗はサーバー側が `ctx.report_progress()` で報告します(**[進捗](handlers/progress.md)** を参照)。 |
|
||||
|
||||
この表から 3 つのことがわかります。
|
||||
|
||||
* ルート、サンプリング、ロギングはひとまとまりです。**SEP-2577** という 1 つの提案が、3 つのケイパビリティを一度にすべて非推奨にしています。
|
||||
* サンプリングとルートには、より根深い共通の問題があります。どちらも**サーバー**が**クライアント**に**リクエスト**を送る場面だという点です。2026-07-28 が **[マルチラウンドトリップリクエスト](handlers/multi-round-trip.md)** で置き換えるのは、まさにこの方向の通信全体です。なくなるのは単独の RPC メソッド(`sampling/createMessage`、`roots/list`、プッシュ型の `elicitation/create`)です。`CreateMessageRequest` / `ListRootsRequest` / `ElicitRequest` というペイロード型は `InputRequiredResult.input_requests` に埋め込まれる形で残り、クライアント側ではこれまでと同じコールバックに届きます。
|
||||
* `ping` だけは毛色が違います。プロトコルはこれを非推奨にするのではなく、削除します。SDK のメソッドは依然として警告を出し(メッセージは「deprecated」ではなく「removed」と述べます)、現行仕様の接続で呼び出すと「Method not found」が返ります。
|
||||
|
||||
## 非推奨は勧告にすぎない {#deprecated-is-advisory}
|
||||
|
||||
今日の時点で壊れるものはありません。
|
||||
|
||||
上に挙げたメソッドはすべて、**2025-11-25 以前**で交渉されたセッションに対しては引き続き動作します。クライアントで `mode="legacy"` を固定すれば、2026 年より前とまったく同じ挙動になります。通信上の変更はなく、ケイパビリティの交渉も変わりません。
|
||||
|
||||
変わるのは、それぞれが最初に実行されたときに、目に見える警告が出る点です。
|
||||
|
||||
```text
|
||||
MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
|
||||
```
|
||||
|
||||
`MCPDeprecationWarning` は `UserWarning` のサブクラスであり、`DeprecationWarning` のサブクラス**ではありません**。これは意図的なものです。Python のデフォルトのフィルターは、`__main__` として直接実行されるコードでしか `DeprecationWarning` を表示しません。ライブラリが何かを非推奨にしても 2 年間誰も気づかない、というのはこの仕組みのせいです。この警告は `-W` フラグなしで、どこでも表示されます。
|
||||
|
||||
!!! warning
|
||||
「勧告にすぎない」のは通信路の手前までです。サンプリングとルートはサーバーからクライアントへの「リクエスト」であり、2026-07-28 のセッションにはそれを運ぶチャネルがありません。現行仕様の接続のツール内で `ctx.session.create_message()` を呼び出すと、警告はやはり出ますが、そのあと送信がエラーで失敗します。
|
||||
|
||||
```text
|
||||
Cannot send 'sampling/createMessage': this transport context has no back-channel
|
||||
for server-initiated requests.
|
||||
```
|
||||
|
||||
シグナルは 2 つ、この順番です。`MCPDeprecationWarning` は、どの接続でもメソッドを呼び出した瞬間に発生します。エラーは、そのあと SDK が送信を試みたときに返ってくるものです。この 2 つの機能がエンドツーエンドで動作するのは、対応するコールバックをクライアントが登録した `mode="legacy"` の接続だけです。
|
||||
|
||||
## 警告を抑止する {#silencing-the-warning}
|
||||
|
||||
新しいコードでは、しないでください。
|
||||
|
||||
ただし、保守しているサーバーが実際に 2026 年より前のクライアントを相手にしているなら、ログを静かに保つ正当な理由があります。最初の非推奨の呼び出しが実行される前に、このカテゴリをフィルターしてください。
|
||||
|
||||
```python
|
||||
import warnings
|
||||
|
||||
from mcp import MCPDeprecationWarning
|
||||
|
||||
warnings.filterwarnings("ignore", category=MCPDeprecationWarning)
|
||||
```
|
||||
|
||||
API はこれだけです。メソッドごとのスイッチはありませんし、必要もありません。カテゴリが 1 つである利点は、1 行で黙らせ、1 行で元に戻せることです。
|
||||
|
||||
!!! check
|
||||
フィルターを逆向きにかければ、無料で回帰テストが手に入ります。pytest の設定の `filterwarnings` に `"error::mcp.MCPDeprecationWarning"` を追加すると、非推奨の呼び出しは警告ではなく**例外を送出**します。まだ `ctx.info()` を呼んでいる `old_log` という名前のツールは通らなくなり、次のように報告し始めます。
|
||||
|
||||
```text
|
||||
Error executing tool old_log: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
|
||||
```
|
||||
|
||||
pytest の設定を 1 行足すだけで、非推奨の呼び出しがテストを失敗させずにコードベースへ紛れ込むことは二度とありません。
|
||||
|
||||
## まとめ {#recap}
|
||||
|
||||
* 2026-07-28 の仕様は、**ルート**、サーバー起点の**サンプリング**、プロトコルの**ロギング**を非推奨にし(いずれも [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577))、**進捗**をサーバーからクライアントへの方向に限定し、**`ping`** を削除します。
|
||||
* 「代わりにすること」の列が次の行き先を示しています。サンプリングとルートには **[マルチラウンドトリップリクエスト](handlers/multi-round-trip.md)**、ロギングには **[ロギング](handlers/logging.md)**、進捗には **[進捗](handlers/progress.md)** です。`ping` には何も必要ありません。
|
||||
* 非推奨は勧告にすぎません。通信上の変更はなく、2026 年より前のセッションに対してはすべてが引き続き動作します。そして目に見える `MCPDeprecationWarning` が出ます(`UserWarning` なので、デフォルトで有効です)。
|
||||
* サンプリングとルートにはさらに、2026-07-28 のセッションにはないバックチャネル(back-channel)が必要です。現行仕様の接続では警告を出し、そのあと例外を送出します。
|
||||
* `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` でカテゴリ全体を黙らせます。pytest で `"error::mcp.MCPDeprecationWarning"` を指定すれば、テストの失敗に変わります。
|
||||
* 新しいコードは、これらのどれの上にも築くべきではありません。
|
||||
|
||||
このドキュメントのほかのページはすべて、現行の API を扱っています。
|
||||
139
i18n/ja/pages/get-started/first-steps.md
Normal file
139
i18n/ja/pages/get-started/first-steps.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686]
|
||||
tool: 1
|
||||
---
|
||||
# 最初のステップ {#first-steps}
|
||||
|
||||
**[トップページ](../index.md)** は駆け足です。サーバーを書き、実行し、ツールを呼び出します。
|
||||
|
||||
このページではじっくり進めます。サーバーが公開できる 3 種類のものをすべて取り上げ、途中で出てくるものすべてに名前を付けていきます。
|
||||
|
||||
## ホスト、クライアント、サーバー {#host-client-and-server}
|
||||
|
||||
ここから先、どのページにも登場する言葉が 3 つあります。
|
||||
|
||||
* **ホスト**は LLM アプリケーションです。Claude、IDE、エージェントランタイムなどがこれにあたります。ユーザーが対話している相手です。
|
||||
* **クライアント**はホストの中にあり、MCP を話します。ホストは、接続するサーバーごとにクライアントを 1 つずつ動かします。
|
||||
* **サーバー**は、この SDK で作るものです。クライアントに対して何かを公開します。モデルと直接やり取りすることは決してありません。
|
||||
|
||||
自分で書くのはサーバーです。ホストは別の誰かが作る製品です。SDK には `Client` も用意されています。サーバーのテストに使うもので、このページの後半にも登場します。
|
||||
|
||||
## 3 つのプリミティブ {#the-three-primitives}
|
||||
|
||||
サーバーが公開するものは、ちょうど 3 種類です。それらを分けるのは、**誰が使うと決めるのか**という点です。
|
||||
|
||||
| プリミティブ | 制御する主体 | どんなものか | 例 |
|
||||
|---------------|-----------------|-----------------------------------------------------|------------------------------------|
|
||||
| **ツール** | モデル | アクションを起こすためにモデルが呼び出す関数 | API 呼び出し、データベースへの書き込み |
|
||||
| **リソース** | アプリケーション | ホストがモデルのコンテキストに読み込むデータ | ファイルの内容、API のレスポンス |
|
||||
| **プロンプト** | ユーザー | ユーザーが名前で呼び出す、再利用可能なメッセージテンプレート | スラッシュコマンド、メニュー項目 |
|
||||
|
||||
「制御する主体」こそが、この区分の核心です。ツールが実行されるのは、**モデル**が呼び出すと決めたからです。リソースが添付されるのは、**アプリケーション**がモデルに必要だと判断したからです。プロンプトが実行されるのは、**ユーザー**が選んだからです。
|
||||
|
||||
!!! info
|
||||
Web API を作ったことがあれば、勘どころはもうほとんどつかめています。**リソース**は `GET`(データを読み込み、何も変更しない)で、**ツール**は `POST`(処理を行い、副作用を持つことがある)です。**プロンプト**に HTTP の対応物はありません。ユーザーが名前を指定して実行する、保存済みのクエリに近いものです。
|
||||
|
||||
## 1 つのサーバーで 3 つすべて {#one-server-all-three}
|
||||
|
||||
```python title="server.py" hl_lines="6 12 18"
|
||||
--8<-- "docs_src/first_steps/tutorial001.py"
|
||||
```
|
||||
|
||||
ごく普通の関数が 3 つ、デコレーターが 3 つです。どのデコレーターも、それだけで登録が完結します。
|
||||
|
||||
* `@mcp.tool()` は `add` を**ツール**にします。
|
||||
* `@mcp.resource("greeting://{name}")` は `greeting` を**リソーステンプレート**にします。URI の中の `{name}` が関数のパラメーターです。
|
||||
* `@mcp.prompt()` は `summarize` を**プロンプト**にします。返した文字列がユーザーメッセージになります。
|
||||
|
||||
それ以外のもの(名前、説明、引数のスキーマ)は、SDK が関数そのものから読み取ります。関数名、docstring、型ヒントからです。どれも別途宣言してはいません。
|
||||
|
||||
!!! tip
|
||||
SDK の 2 つの半分には、インポートパスも 2 つあります。`from mcp import Client` と `from mcp.server import MCPServer` です。`from mcp import MCPServer` はありません。
|
||||
|
||||
### 試してみる {#try-it}
|
||||
|
||||
MCP Inspector で実行してください。
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
出力された URL を開いてください。Inspector にはプリミティブごとにタブが 1 つずつあります。順に見ていきましょう。
|
||||
|
||||
**Tools** タブには項目が 1 つあります。`add` で、説明は *Add two numbers.* です。フォームには必須の整数フィールドが 2 つあり、1 つは `a` 用、もう 1 つは `b` 用です。値を入力して呼び出すと、結果は `3` です。Inspector はこのフォームを `a: int, b: int` から組み立てました。ほかのどのクライアントも同じことをします。
|
||||
|
||||
**Resources** タブでは、*Resources* の一覧は空です。`greeting` は **Resource Templates** の下にあります。`greeting://{name}` にはパラメーターがあり、誰かが `name` を指定するまでは一覧に載せられる単体のリソースが存在しないからです。`World` を指定して読み取ると、こう返ってきます。
|
||||
|
||||
```text
|
||||
Hello, World!
|
||||
```
|
||||
|
||||
**Prompts** タブにも項目が 1 つあります。`summarize` で、必須の引数 `text` を 1 つだけ取ります。適当なテキストを渡して取得すると、`role: user` を持ち、レンダリングされた文字列を内容とするメッセージが 1 つ返ってきます。プロンプトとはそれだけのものです。メッセージを組み立てる関数にすぎません。
|
||||
|
||||
Inspector はサーバーを **stdio** で実行しました。MCP サーバーが話せるトランスポートの 1 つです。トランスポートを選ぶのはまだ先で、そのためのページが **[サーバーの実行](../run/index.md)** です。
|
||||
|
||||
## ケイパビリティ {#capabilities}
|
||||
|
||||
Inspector にはタブが 3 つありました。3 つあると、どうやってわかったのでしょうか。
|
||||
|
||||
クライアントが接続すると、サーバーは自身の**ケイパビリティ**を宣言します。どの系統のリクエストに応答するか、ということです。クライアントはこの宣言をもとに、そもそも何を要求するかを決めます。この宣言を自分で書いてはいません。`MCPServer` が代わりに宣言します。
|
||||
|
||||
自分の目で確かめてみましょう。SDK の `Client` はサーバーオブジェクトをそのまま受け取り、**インメモリ**で接続します(サブプロセスもポートも使いません)。
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
print(client.server_capabilities.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```text
|
||||
{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}}
|
||||
```
|
||||
|
||||
この辞書が、サーバーが宣言した**ケイパビリティ**です。接続してくるどのクライアントも、最初にこれを知ります。
|
||||
|
||||
| ケイパビリティ | クライアントが呼び出せるようになるもの |
|
||||
|-------------|------------------------------------------------------------|
|
||||
| `tools` | `tools/list`, `tools/call` |
|
||||
| `resources` | `resources/list`, `resources/templates/list`, `resources/read` |
|
||||
| `prompts` | `prompts/list`, `prompts/get` |
|
||||
|
||||
`MCPServer` は 3 つのプリミティブすべてを提供するので、3 つとも常に宣言されます。
|
||||
|
||||
ここにないものにも注目してください。`completions`(リソーステンプレートとプロンプトの引数の自動補完)には自分で書くハンドラーが必要ですが、このサーバーにはありません。そのためこのケイパビリティは宣言されず、行儀のよいクライアントなら要求もしません。オプションのものはすべてこのルールに従います。登録すればケイパビリティが現れます。**[補完](../servers/completions.md)** のページがそれを実証しています。
|
||||
|
||||
!!! info
|
||||
`Client(mcp)` は、このドキュメントのすべてのサンプルをテストしているのと同じインメモリクライアントで、自分のサーバーをテストするときにもこれを使います。まるごと 1 ページを割いています。**[テスト](testing.md)** です。
|
||||
|
||||
## 書かなかったもの {#what-you-did-not-write}
|
||||
|
||||
このページを振り返ってみてください。書いたのは小さな Python 関数 3 つです。次のものは書いて**いません**。
|
||||
|
||||
* JSON Schema。`a: int, b: int` がそのまま `add` のスキーマです。
|
||||
* リクエストハンドラー。`tools/list`、`resources/read`、`prompts/get` は、すべて代わりに処理されます。
|
||||
* ケイパビリティの宣言。`MCPServer` が代わりに作りました。
|
||||
* プロトコルのコードを 1 行も。バージョンのネゴシエーション、JSON-RPC のフレーミング、ケイパビリティの交換は、すべて `mcp dev` と `Client(mcp)` の内部で行われ、目にすることはありませんでした。
|
||||
|
||||
この比率こそが、この SDK の存在意義です。
|
||||
|
||||
## まとめ {#recap}
|
||||
|
||||
* **ホスト**は LLM アプリ、**クライアント**はそのうち MCP を話す部分、**サーバー**は自分で作るものです。
|
||||
* ツールは**モデル**が、リソースは**アプリケーション**が、プロンプトは**ユーザー**が制御します。
|
||||
* デコレーターはプリミティブごとに 1 つです。`@mcp.tool()`、`@mcp.resource(uri)`、`@mcp.prompt()`。名前、説明、スキーマは関数から取られます。
|
||||
* `{param}` を含む URI はリソース**テンプレート**を作り、具体的なリソースとは別に一覧表示されます。
|
||||
* サーバーの**ケイパビリティ**は代わりに宣言され、クライアントはサーバーが宣言したものだけを要求します。
|
||||
* `Client(mcp)` はサーバーオブジェクトにインメモリで接続します。初日から使えるテストハーネスです。
|
||||
|
||||
次は **[実際のホストに接続する](real-host.md)** です。このサーバーを Claude Desktop や IDE の中で、本当に動かします。その次は **[テスト](testing.md)** です。1 ページ、インメモリクライアント 1 つで、動くかどうかを当て推量することはもうありません。そのあとは各プリミティブに専用のページがあり、まずはモデルが動かすもの、**[ツール](../servers/tools.md)** から始まります。
|
||||
53
i18n/ja/pages/get-started/index.md
Normal file
53
i18n/ja/pages/get-started/index.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ed4a756b4c53c585, 97e2fb315b7fe398, 4d04f1c6f4bf6c1d, 577d73078fc62baf]
|
||||
tool: 1
|
||||
---
|
||||
# はじめに {#get-started}
|
||||
|
||||
MCP が初めてでも、この SDK が初めてでも、ここから始めてください。ここにあるページでは、何もない状態から、テスト済みの動くサーバーができあがるまでを案内します。[SDK をインストール](installation.md)し、[最初のサーバー](first-steps.md)を作り、[実際のホストに接続](real-host.md)して、インメモリクライアントで[テスト](testing.md)します。
|
||||
|
||||
## コードを実行する {#run-the-code}
|
||||
|
||||
コードブロックはすべて、そのままコピーして使えます。どれも完結した、実際に動くファイルです。
|
||||
|
||||
一緒に進めるには、コードブロックを `server.py` に貼り付けて、MCP Inspector で開いてください。
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
コードを自分で書き(またはコピーし)、編集し、ローカルで実行することを**強くおすすめします**。自分のエディターで使ってみてこそ、肝心な点が実感できます。書く量がどれほど少ないか、自動補完が効くこと、そして何も実行しないうちに型チェックがミスを見つけてくれることです。
|
||||
|
||||
## 推測に頼る必要はない {#you-will-not-be-guessing}
|
||||
|
||||
このドキュメントの例はすべて、SDK 自身のリポジトリの [`docs_src/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/docs_src) 以下に置かれた完結したファイルです。そのどれもが、SDK のテストスイートによって**インメモリクライアント**を通じて実行されています。
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_add() -> None:
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("add", {"a": 1, "b": 2})
|
||||
assert result.structured_content == {"result": 3}
|
||||
```
|
||||
|
||||
サブプロセスも、ポートも、トランスポートもありません。`Client(mcp)` がサーバーオブジェクトに直接接続します。
|
||||
|
||||
SDK への変更によってこれらのページの例が壊れた場合、ページが壊れるより先に CI が赤くなります。ここで読むコードが、実際に動くコードそのものです。
|
||||
|
||||
この仕組みは[テスト](testing.md)で実際に使うことになります。自分のサーバーをテストする方法も、まさにこれです。
|
||||
|
||||
## 次に進む先 {#where-to-go-next}
|
||||
|
||||
サーバーが動くようになれば、残りのドキュメントは講座ではなくリファレンスです。どのページも単独で完結しているので、必要なところへ直接進んでください。
|
||||
|
||||
* サーバーが公開するもの(ツール、リソース、プロンプト)は **[サーバー](../servers/index.md)** です。
|
||||
* 登録した関数の中で使えるものは **[ハンドラーの中で](../handlers/index.md)** です。
|
||||
* クライアントから使えるようにする方法(stdio、HTTP、既存の FastAPI アプリ)は **[サーバーの実行](../run/index.md)** です。
|
||||
* 反対側、つまり MCP サーバーを「使う」側のアプリケーションを作る方法は **[クライアント](../client/index.md)** です。
|
||||
45
i18n/ja/pages/get-started/installation.md
Normal file
45
i18n/ja/pages/get-started/installation.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
translation:
|
||||
sections: [6e2f9bab94d5ed36, 8cf653388f69e28b, 6fd9ea2f65de0df6]
|
||||
tool: 1
|
||||
---
|
||||
# インストール {#installation}
|
||||
|
||||
Python SDK は PyPI 上で [`mcp`](https://pypi.org/project/mcp/) として公開されています。**Python 3.10 以上**が必要です。
|
||||
|
||||
このドキュメントは、現在の安定版リリースラインである **v2** について説明しています。
|
||||
|
||||
=== "uv"
|
||||
|
||||
```bash
|
||||
uv add "mcp[cli]"
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
|
||||
```bash
|
||||
pip install "mcp[cli]"
|
||||
```
|
||||
|
||||
!!! note "v1 から移行する場合"
|
||||
v2 は破壊的変更を含むメジャーバージョンです。変更点はすべて **[移行ガイド](../migration.md)** にまとめてあります。自分のパッケージが `mcp` に依存していて、まだ移行の準備ができていない場合は、バージョンの上限として `<2` を付けておいてください(たとえば `mcp>=1.28,<2`)。そうすれば、バージョンを固定しない依存解決でも 1.x 系にとどまります。
|
||||
|
||||
## インストールされるもの {#what-gets-installed}
|
||||
|
||||
SDK を使うだけなら、以下の内容を知っている必要はありません。それぞれの依存関係が何のためにあるのか気になる場合のために、まとめておきます。
|
||||
|
||||
* `mcp-types`:すべてのプロトコル型(リクエスト、結果、コンテンツブロック)を独立したパッケージにしたもので、SDK と足並みをそろえてバージョン管理されます。`mcp` に依存するコードは、`mcp.types` というエイリアス経由でこれをインポートします(このドキュメントに出てくる `from mcp.types import ...` はすべてそうです)。`mcp_types` を直接インポートするのは、SDK なしで `mcp-types` をインストールするプロジェクトだけにしてください。
|
||||
* [`anyio`](https://anyio.readthedocs.io/):非同期ランタイムです。SDK 全体が anyio を前提に書かれているので、`asyncio` でも `trio` でも動きます。
|
||||
* [`pydantic`](https://docs.pydantic.dev/):`mcp.types` のあらゆるモデルの土台であり、スキーマの生成と検証もすべて担っています。
|
||||
* [`httpx2`](https://pypi.org/project/httpx2/):Streamable HTTP と SSE のクライアント側トランスポートを支える HTTP クライアントで、Server-Sent Events のサポートを内蔵しています。
|
||||
* [`starlette`](https://www.starlette.io/)、[`uvicorn`](https://www.uvicorn.org/)、[`sse-starlette`](https://pypi.org/project/sse-starlette/)、[`python-multipart`](https://pypi.org/project/python-multipart/):HTTP のサーバー側トランスポートです。
|
||||
* [`jsonschema`](https://pypi.org/project/jsonschema/):ツールの構造化出力を、宣言された出力スキーマに照らして検証します。
|
||||
* [`pyjwt[crypto]`](https://pyjwt.readthedocs.io/):認可のための OAuth トークン処理を担います。
|
||||
* [`opentelemetry-api`](https://opentelemetry-python.readthedocs.io/):軽量な API だけです。そのため、OpenTelemetry の SDK とエクスポーターを自分でインストールしない限り、この SDK のトレーシングミドルウェアにコストは発生しません。
|
||||
* [`typing-extensions`](https://typing-extensions.readthedocs.io/) と [`typing-inspection`](https://pypi.org/project/typing-inspection/):Python 3.10 でも新しい型付け機能を使えるようにします。
|
||||
* [`pywin32`](https://pypi.org/project/pywin32/):Windows 専用で、`stdio` のサブプロセス管理に使われます。
|
||||
|
||||
## オプションの extras {#optional-extras}
|
||||
|
||||
* `mcp[cli]` は、`mcp` コマンドラインツール(`mcp dev`、`mcp run`、`mcp install`)のために [`typer`](https://typer.tiangolo.com/) と [`python-dotenv`](https://pypi.org/project/python-dotenv/) を追加します。開発中は入れておきたいところですが、デプロイしたサーバーでは必要ないかもしれません。
|
||||
* `mcp[rich]` は、サーバーのログを見やすくするために [`rich`](https://rich.readthedocs.io/) を追加します。
|
||||
168
i18n/ja/pages/get-started/real-host.md
Normal file
168
i18n/ja/pages/get-started/real-host.md
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
---
|
||||
translation:
|
||||
sections: [3c4f2f06b4e978b6, 22520eecae3d1961, f4e1709db18d635a, 2eb57992049671d9, 1ba83e9af37cc1b4, 4822586344b08d9e, 1c93afef72478992, b6b448f9eddd51dc, fe55370fd931815b]
|
||||
tool: 1
|
||||
---
|
||||
# 実際のホストに接続する {#connect-to-a-real-host}
|
||||
|
||||
**ホスト**とは、サーバーが最終的にその中で動くことになるアプリケーションのことです。Claude Desktop、Claude Code、IDE などがそうです。ユーザーがやり取りする相手はホストです。その内部では、MCP **クライアント**がサーバーを子プロセスとして起動し、そのプロセスの stdin と stdout を介してサーバーと通信します。
|
||||
|
||||
つまり、ホストに接続するためにやることは 1 つだけです。**サーバーを起動するコマンド**をホストに伝えます。このページに出てくるもの(2 つの CLI コマンドと 3 つの JSON ファイル)はすべて、その同じコマンドの置き場所が違うだけです。
|
||||
|
||||
## 1 つのサーバー、すべてのホスト {#one-server-every-host}
|
||||
|
||||
```python title="server.py" hl_lines="3 33-34"
|
||||
--8<-- "docs_src/real_host/tutorial001.py"
|
||||
```
|
||||
|
||||
ツール 2 つとリソース 1 つが、1 つのファイルに収まっています。このファイルについて、以降のどのホストにも関わる点が 3 つあります。
|
||||
|
||||
* 引数なしの `mcp.run()` は **stdio** サーバーを起動します。ブロックし、stdin でプロトコルメッセージを読み、stdout に書き出します。これが、このページのどのホストも話すトランスポートです。ホストはこのファイルを子プロセスとして起動し、その 2 本のパイプを所有します。だからこそ、接続は常に「これがコマンドです」と伝えるだけで済みます。ポートを選ぶことはなく、どこかのポートで待ち受けるものもありません。
|
||||
* `run()` は `if __name__ == "__main__":` の下にあります。以降のものはすべてこのファイルを実行するのではなく**インポート**するので、ガードのない `run()` だと、何かがモジュールを読み込んだ瞬間にサーバーが起動してしまいます。
|
||||
* サーバーオブジェクトは `mcp` という名前のモジュールレベルのグローバル変数です。これは `mcp run` が探す名前です(`server` と `app` でも動きます)。別の名前を付けた場合は、`mcp run server.py:bookshop` のように明示的に指定します。
|
||||
|
||||
このページの Python はこれが最後の 1 行です。ここから下はすべてホストの設定です。
|
||||
|
||||
## 起動コマンド {#the-launch-command}
|
||||
|
||||
以降のどのホストにも同じコマンドを渡します。
|
||||
|
||||
```bash
|
||||
uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
どのホストにも 1 つのコマンドで済むのは、`uv run --with` がその場で SDK を新しい環境へ解決してくれるからです。どのディレクトリからでも動き、プロジェクトも、有効化すべき仮想環境も要りません。このことがほかのどこよりもここで効いてくるのは、ホストがサーバーを起動するのがシェルからではなく、ほぼ空の環境でホスト自身の作業ディレクトリからだからです。
|
||||
|
||||
このコマンドは、`mcp install` が Claude Desktop の設定に書き込んでくれるコマンドでもあります(後述)。そのため、手で入力するものとツールが生成するものは、ツールが付け加える正確なバージョン固定を除いて一致します。
|
||||
|
||||
!!! tip "ホストが `uv` を見つけられない場合"
|
||||
ホストは最小限の `PATH` でサーバーを起動するため、そこに `uv` が入っていないことがあります。`uv` とだけ書いた部分を、`which uv`(macOS/Linux)または `where uv`(Windows)で得られる絶対パスに置き換えてください。`mcp install` が書き込むのもまさにこの形です。
|
||||
|
||||
!!! note "このページはローカルの話"
|
||||
ここで扱うものはすべて、ホストと同じマシン上でサーバーを動かします。ホストがファイルを stdio 経由で起動する形です。個人用のツールや 1 台のマシンで使うツールなら、まさにこれが正解です。ファイルを持って**いない**人たちにサーバーを渡すには、コマンドではなく **URL** を配ります。つまり、同じ `mcp` オブジェクトを Streamable HTTP で提供します。**[サーバーの実行](../run/index.md)** はその判断を 1 つの表にまとめており、**[デプロイとスケール](../run/deploy.md)** はそこから実際のホスト名に至るまでの道のりです。
|
||||
|
||||
また、ホストとは内部に MCP クライアントを持つアプリケーションにすぎないので、自分の Python コードがホストの役を演じることもできます。**[クライアントのトランスポート](../client/transports.md)** ではこの同じファイルを `stdio_client(...)` でサブプロセスとして起動し、**[テスト](testing.md)** ではプロセスを一切使わずにメモリ内で接続します。
|
||||
|
||||
## Claude Desktop {#claude-desktop}
|
||||
|
||||
SDK が代わりに設定してくれる唯一のホストです。
|
||||
|
||||
```bash
|
||||
uv run mcp install server.py
|
||||
```
|
||||
|
||||
これだけです。`mcp install` はファイルをインポートしてサーバーの名前を読み取り、Claude Desktop の設定ファイルを探し出し、そこに起動コマンドを書き込みます。その過程でパスを絶対パスに変換してくれるので、自分で変換する必要はありません。
|
||||
|
||||
謎めいたところは何もありません。書き込まれるエントリは次のとおりです。
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"Bookshop": {
|
||||
"command": "/absolute/path/to/uv",
|
||||
"args": [
|
||||
"run",
|
||||
"--frozen",
|
||||
"--with",
|
||||
"mcp[cli]==2.0.0",
|
||||
"mcp",
|
||||
"run",
|
||||
"/absolute/path/to/server.py"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
これは前の節の起動コマンドに 3 つの要素を加えたものです。`uv` への絶対パス、たまたま近くにあるロックファイルを `uv` が書き換えることのないようにする `--frozen`、そしてインストール済みの `mcp` のバージョンへの正確な固定です。書き込み先は `claude_desktop_config.json` で、このファイルは次の場所にあります。
|
||||
|
||||
* **macOS**:`~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
* **Windows**:`%APPDATA%\Claude\claude_desktop_config.json`
|
||||
|
||||
このファイルは手で書くこともできます。`mcp install` があるのは、手で書くときにありがちなミス(相対パス)を避けるためです。
|
||||
|
||||
Claude Desktop を(ウィンドウだけでなく)完全に終了し、もう一度開いてください。
|
||||
|
||||
!!! warning
|
||||
Claude Desktop の設定「ディレクトリ」がまだ存在しない場合、`mcp install` は `Claude app not found` で失敗します。Claude Desktop をインストールして一度起動してください。ディレクトリはそのときに作られます。
|
||||
|
||||
!!! tip
|
||||
Claude Desktop はサーバーを自身のプロセスで起動するので、シェルの環境変数はそこにはありません。`uv run mcp install server.py -v API_KEY=abc123`(または `-f .env`)とすると、それらがエントリの `env` フィールドに記録されます。`--name` はエントリ名を上書きします。デフォルトはサーバーの `name` です。
|
||||
|
||||
## Claude Code {#claude-code}
|
||||
|
||||
編集するファイルはありません。`claude` CLI でサーバーを登録してください。`--` の後ろはすべて起動コマンドです。
|
||||
|
||||
```bash
|
||||
claude mcp add bookshop -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
Claude Code のセッション内で `/mcp` を実行し、`bookshop` が接続されていてそのツールが一覧表示されることを確認してください。
|
||||
|
||||
## Cursor {#cursor}
|
||||
|
||||
プロジェクトのルートに `.cursor/mcp.json` を作成してください。
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"bookshop": {
|
||||
"command": "uv",
|
||||
"args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Claude Desktop が使うのと同じ `mcpServers` キーの下に、同じ `command` と `args` を置きます。サーバーは Cursor の MCP 設定に表示され、両方のツールが一覧に並びます。
|
||||
|
||||
## VS Code {#vs-code}
|
||||
|
||||
プロジェクトのルートに `.vscode/mcp.json` を作成してください。
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"bookshop": {
|
||||
"type": "stdio",
|
||||
"command": "uv",
|
||||
"args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Cursor のファイルとの違いは 2 つだけです。ラッパーのキーが `mcpServers` ではなく `servers` であること、そして各エントリが `type` を宣言することです。信頼を確認するプロンプトを承認すると、コマンドパレットの **MCP: List Servers** に `bookshop` が実行中として表示されます。
|
||||
|
||||
!!! note
|
||||
VS Code 1.99 以降と、サインイン済みの **GitHub Copilot** 拡張機能が必要です(Copilot Free で十分です)。また、Copilot Chat は **Agent** モードでなければなりません。ほかのモードはツールを呼び出さないからです。
|
||||
|
||||
## 表示されないとき {#it-doesnt-show-up}
|
||||
|
||||
ホストの設定に手を付ける前に、起動コマンドを自分で実行してみてください。
|
||||
|
||||
```bash
|
||||
uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
何も表示されず、コマンドも戻ってきません。この沈黙は正しい動作です。stdio サーバーは、ホストが先に stdin で話しかけてくるのを待っています(止めるには `Ctrl-C`)。本当のバグはトレースバックや即座の終了のほうで、こうして実行すれば、ホスト越しに推測する代わりにそれを直接読めます。
|
||||
|
||||
このコマンドがじっと待機するようになったら、残る原因はほぼ決まって次の 3 つのどれかです。
|
||||
|
||||
* **相対パス。** ホストがサーバーを起動するのは、登録したときのディレクトリではなく、ホスト自身の作業ディレクトリからです。`/absolute/path/to/server.py` が必要なところに `server.py` と書くのが、飛び抜けて多い失敗です。ホストが `uv` も見つけられないなら、そのパスも絶対パスにする必要があります。
|
||||
* **ホストがまだ古い設定で動いている。** ホストは起動時に設定を読み込みます。特に Claude Desktop は、`claude_desktop_config.json` の編集を反映させるには、ウィンドウを閉じるだけでなく「完全に終了」してから開き直す必要があります。
|
||||
* **退避される期間の外で、何かが stdout に届いた。** stdio では、stdout がプロトコルそのものです。SDK はサービス中、フラッシュされた余計な出力を stderr に退避させます。しかし、それ以前に stdout へフラッシュされた出力(echo するラッパースクリプトや、バッファリングなしのプロセスでのインポート時の `print()`)や、インタープリター終了時に書き出されるバッファ済みの `print()` は別です。これらは壊れたメッセージをホストに渡してしまい、ホストは接続を切ります。ログ出力にはデフォルトの `logging` 設定を使ってください。その stderr ハンドラーはレコードごとにフラッシュします。独自のハンドラーも stdout を避ける必要があります。詳しくは **[ロギング](../handlers/logging.md)** を参照してください。
|
||||
|
||||
Claude Desktop はサーバーごとにログを残します。`mcp-server-<NAME>.log` がサーバーの stderr で、接続についての `mcp.log` と並んで、macOS では `~/Library/Logs/Claude`、Windows では `%APPDATA%\Claude\logs` の下にあります。
|
||||
|
||||
この 3 つに当てはまらない場合は、**[トラブルシューティング](../troubleshooting.md)** のページを参照してください。
|
||||
|
||||
## まとめ {#recap}
|
||||
|
||||
* **ホスト**(Claude Desktop や IDE)は MCP クライアントを動かし、そのクライアントがサーバーを子プロセスとして stdio 経由で起動します。接続とは、起動コマンドを 1 つ渡すことです。
|
||||
* そのコマンドは `uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py` です。有効化する venv は不要で、どのディレクトリからでも動きます。
|
||||
* **Claude Desktop** は、`mcp install` が代わりに設定してくれる唯一のホストです。その同じコマンド(`uv` への絶対パス、`--frozen`、インストール済みバージョンへの正確な固定を加えたもの)を `claude_desktop_config.json` に書き込むので、自分で書く必要はありません。
|
||||
* **Claude Code** は `claude mcp add bookshop -- <launch command>` です。**Cursor** は `mcpServers` の下に書く `.cursor/mcp.json` です。**VS Code** は `servers` の下に書く `.vscode/mcp.json` で、各エントリに `type` を付けます。
|
||||
* どこでも絶対パスを使い、設定を編集したらホストを再起動し、SDK 以外のものには決して stdout に書き込ませないでください。
|
||||
|
||||
このページのどのホストも、同じファイルに同じコマンドで接続しました。そのファイルが何を「公開」できるかが、このドキュメントの残りのテーマです。**[ツール](../servers/tools.md)**、**[リソース](../servers/resources.md)**、そして stdio 以外のあらゆるトランスポートを扱う **[サーバーの実行](../run/index.md)** へと続きます。
|
||||
96
i18n/ja/pages/get-started/testing.md
Normal file
96
i18n/ja/pages/get-started/testing.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
translation:
|
||||
sections: ['4926721070127497', c52a1de2b6b32f40, 2e410b412c25f314, 627195f7159e24ef]
|
||||
tool: 1
|
||||
---
|
||||
# テスト {#testing}
|
||||
|
||||
Python SDK には、**インメモリトランスポート**を備えた `Client` クラスが付属しています。サーバーオブジェクトを渡せば、そのサーバーに直接接続します。
|
||||
|
||||
サブプロセスも、ポートも要りません。トランスポートすら使いません。FastAPI の `TestClient` と同じ発想です。
|
||||
|
||||
## 基本的な使い方 {#basic-usage}
|
||||
|
||||
ツールを 1 つだけ持つシンプルなサーバーがあるとします。
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/testing/tutorial001.py"
|
||||
```
|
||||
|
||||
以下のテストを実行するには、追加の(開発用)依存関係が 2 つ必要です。
|
||||
|
||||
=== "uv"
|
||||
|
||||
```bash
|
||||
uv add --dev pytest inline-snapshot
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
|
||||
```bash
|
||||
pip install pytest inline-snapshot
|
||||
```
|
||||
|
||||
!!! info
|
||||
このドキュメントは、[`pytest`](https://docs.pytest.org/en/stable/) をすでに知っていることを前提にしています。
|
||||
|
||||
[`inline-snapshot`](https://15r10nk.github.io/inline-snapshot/latest/) は、以下のテストで結果オブジェクト全体を 1 行でアサートするために使っているライブラリです。テストの出力を、コードにあるとおりの `snapshot(...)` リテラルとして記録します。使いたくない場合は import を削除し、ほかのテストと同じように、関心のあるフィールド(`result.content[0].text == "3"`)をアサートしてください。
|
||||
|
||||
テストは次のとおりです。
|
||||
|
||||
```python title="test_server.py"
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
from mcp import Client
|
||||
from mcp.types import CallToolResult, TextContent
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend(): # (1)!
|
||||
return "asyncio"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client(): # (2)!
|
||||
async with Client(mcp, raise_exceptions=True) as c:
|
||||
yield c
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_call_add_tool(client: Client):
|
||||
result = await client.call_tool("add", {"a": 1, "b": 2})
|
||||
# Drop the server identity stamp in `_meta`; it is not what this test is about.
|
||||
result.meta = None
|
||||
assert result == snapshot(
|
||||
CallToolResult(
|
||||
content=[TextContent(type="text", text="3")],
|
||||
structured_content={"result": 3},
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
1. `trio` を使っている場合は、代わりに `"trio"` を返してください。詳しくは [anyio のドキュメント](https://anyio.readthedocs.io/en/stable/testing.html#specifying-the-backends-to-run-on) を参照してください。
|
||||
2. このフィクスチャは接続済みのクライアントを yield します。`client` を受け取るテストごとに、同じサーバーへの新しいインメモリ接続が用意されます。
|
||||
|
||||
これで準備完了です。あとはテストを拡張して、さらに多くのシナリオをカバーしていけます。
|
||||
|
||||
## なぜ `raise_exceptions=True` なのか {#why-raise_exceptionstrue}
|
||||
|
||||
問題が起こりうる場所は 2 種類あり、このフラグが関わるのはそのうちの一方だけです。
|
||||
|
||||
**ツール**の内部で発生した例外は、プロトコル上の失敗ではありません。`is_error=True` の付いた通常の結果になり、モデルがそのメッセージを読みます。`raise_exceptions` はこの挙動を変えません。指定してもしなくても、`call_tool` は同じ `is_error=True` の結果を返します。これについては専用のページがあります。**[エラーの処理](../servers/handling-errors.md)** を参照してください。
|
||||
|
||||
ツール本体の**外側**で起きた失敗は事情が異なります。`Client(mcp)` で得られる接続では、クライアントの目に触れる前に、サーバーがその失敗を汎用の `"Internal server error"` にサニタイズします。予期しないクラッシュの詳細は、リモートの呼び出し側に決して漏らしてはいけないからです。しかしテストでは、これはまさに望まない挙動です。そして `raise_exceptions=True` が変えるのはまさにこの点で、テストからはサニタイズ後のメッセージではなく本来のメッセージが見えるようになります。
|
||||
|
||||
テストでは有効にしたままにしてください。本番コードでは意味を持ちません。
|
||||
|
||||
## デフォルトはインプロセス {#in-process-by-default}
|
||||
|
||||
!!! note
|
||||
`Client(mcp)` はインプロセスで接続し、デフォルトでは**プロトコルの世代を問いません**。サーバーを調べ、適切なプロトコル経路を選びます。テストがレガシー固有のセマンティクス(サンプリングやエリシテーション(elicitation)のプッシュ、`message_handler`)を検証する場合は `mode="legacy"` に固定し、その場合は `raise_exceptions=True` を外してください。レガシー接続はそもそもサニタイズを行わず、このフラグを付けると失敗がテストの中ではなくサーバータスクの中で再送出されてしまうからです。
|
||||
|
||||
この 1 行こそが、このドキュメントが「掲載している例は動く」と約束できる理由でもあります。すべてのサンプルファイルは SDK 自身のテストスイートで実行されており、そのほぼすべてがまさにこのクライアントを経由しています。SDK が自分自身に対して使っているのと同じツールを使っているわけです。
|
||||
|
||||
これで、きちんと動く、テスト済みのサーバーが手元にあります。実際のアプリケーション(Claude Desktop や IDE)に組み込む方法は **[実際のホストに接続する](real-host.md)** に、それ以外の提供方法はすべて **[サーバーの実行](../run/index.md)** にまとまっています。
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue