docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
6 changed files with 89737 additions and 54 deletions
121
i18n/zh-hant/pages/advanced/apps.md
Normal file
121
i18n/zh-hant/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 文件,由 MCP 主機(host)把它繪製成可互動的介面。
|
||||
|
||||
兩個部分,永遠都是兩個部分:
|
||||
|
||||
1. **一個工具**,負責做事並回傳資料,跟任何其他工具一樣。
|
||||
2. **一個 `ui://` 資源**,裡面裝著主機要為它顯示的 HTML。
|
||||
|
||||
工具帶有一個指向該資源的 `_meta.ui.resourceUri` 參照。主機用 `resources/read` 取得它,在**沙箱化的 iframe** 裡繪製,再透過 `postMessage` 把工具的結果推進那個 iframe。伺服器從頭到尾不會送出或收到任何 `ui/*` 訊息:那些流量是主機和 iframe 之間的事。你提供一個工具和一份 HTML 文件,場面由主機負責。
|
||||
|
||||
SDK 以內建的 `Apps` 擴充功能(`io.modelcontextprotocol/ui`)提供這項功能。如果還不熟悉[擴充功能](extensions.md),先快速看過那一頁。一分鐘就好,看完再回來。
|
||||
|
||||
## 有錶面的時鐘 {#a-clock-with-a-face}
|
||||
|
||||
```python title="server.py" hl_lines="19 22 30 32"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
四個動作:
|
||||
|
||||
* `Apps()`:一個實例容納所有綁定 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 型別告訴主機「這是個 app,把它繪製出來」。
|
||||
* `MCPServer("clock", extensions=[apps])`:選擇加入。伺服器現在會在 `capabilities.extensions` 底下宣告 `io.modelcontextprotocol/ui`。
|
||||
|
||||
HTML 本身會監聽主機的 `postMessage` 並顯示結果。真正的 app 請在 HTML 裡使用官方的 [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) 瀏覽器 SDK。它提供 `ontoolresult`、`callServerTool`、`getHostContext` 和 `onhostcontextchanged`,不用自己處理原始的訊息事件。
|
||||
|
||||
## 優雅降級 {#graceful-degradation}
|
||||
|
||||
不是每個用戶端都會繪製 app。規格對這代表什麼講得很直白:
|
||||
|
||||
> Tools **MUST** return a meaningful `content` array even when UI is available.
|
||||
|
||||
模型讀的是 `content`;iframe 是給人看的。支援 UI 的主機照樣會把文字結果餵給模型,而純文字用戶端**只**會拿到那個。所以標準做法是一個工具,兩種答案。再看一次 `get_time`:
|
||||
|
||||
```python title="server.py" hl_lines="23-27"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
只有當用戶端宣告了 `io.modelcontextprotocol/ui` 擴充功能,**而且**在它的 `mimeTypes` 設定裡列出 `text/html;profile=mcp-app` 時,`client_supports_apps(ctx)` 才會是 `True`。這個欄位是必填的,所以省略它的用戶端不算數。同一個檔案裡的 `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` 裡照常列出僅限 app 的工具;主機負責對模型隱藏它們。不要在伺服器端過濾。
|
||||
|
||||
## SDK 強制執行的規則 {#the-rules-the-sdk-enforces}
|
||||
|
||||
這些全都在啟動時就失敗,不會等到上線:
|
||||
|
||||
* `resource_uri` 或資源 URI 不是 `ui://...`,會在裝飾/註冊時引發 `ValueError`。
|
||||
* 工具綁定到一個**沒有對應已註冊資源**的 URI,會在 `MCPServer(extensions=[apps])` 取用這個擴充功能時引發 `ValueError`。一個宣稱有 HTML、`resources/read` 卻 404 的工具是設定錯誤,所以它拒絕建構。
|
||||
* `@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"
|
||||
```
|
||||
|
||||
資源沒有明確設定 MIME 型別時,`add_resource` 會補上 `text/html;profile=mcp-app`;明確設定卻不相符的則會拒絕:掛在任何其他 MIME 型別底下的 `ui://` 資源,沒有任何主機會繪製。
|
||||
|
||||
!!! tip
|
||||
目標是某個 GA 前的主機,還在讀已棄用的扁平 `_meta["ui/resourceUri"]` 鍵?自己合併進去:`@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/zh-hant/pages/advanced/extensions.md
Normal file
172
i18n/zh-hant/pages/advanced/extensions.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
---
|
||||
translation:
|
||||
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
|
||||
tool: 1
|
||||
---
|
||||
# 擴充功能 {#extensions}
|
||||
|
||||
**擴充功能**是掛在單一識別碼之下、需要主動啟用的一組 MCP 行為。
|
||||
|
||||
在伺服器上,它可以貢獻工具、資源和新的請求方法,也可以包裹 `tools/call`。在用戶端上,它可以認領額外的 `tools/call` 結果形狀,並觀察廠商通知。兩端各自在自己的 `capabilities.extensions` 底下宣告,對沒有要求它的人來說什麼都不會改變。這就是契約([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)),而它有一條黃金法則:**擴充功能預設是關閉的**。
|
||||
|
||||
## 使用擴充功能 {#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}
|
||||
|
||||
最小的有用擴充功能是一個工具加上一個設定對映表:
|
||||
|
||||
```python title="server.py" hl_lines="17 19-20 22-23 26"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
* `tools()` 回傳 `ToolBinding`。伺服器註冊每一個的方式,和你自己呼叫 `mcp.add_tool(...)` 完全一樣:同樣的 schema 產生、同樣的 `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}
|
||||
|
||||
擴充功能可以註冊**新的請求方法**:屬於它自己的動詞,和規格定義的方法並列提供:
|
||||
|
||||
```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"})` 把這個方法釘在單一線路版本上。在其他任何版本,用戶端會收到 `METHOD_NOT_FOUND`,就像這個方法在那裡不存在一樣。對那個用戶端來說,它確實不存在。
|
||||
|
||||
方法是**嚴格附加的**。SDK 在建構時就強制這一點,而不是在執行時:
|
||||
|
||||
* 為規格定義的方法(`tools/list`、`completion/complete`……)建立的 `MethodBinding`,在繫結建構時就會引發 `ValueError`。核心動詞屬於伺服器。
|
||||
* 兩個擴充功能繫結同一個方法時,第二個註冊時會引發例外。後寫者勝出正是外掛互相搞壞對方的方式;我們不這麼做。
|
||||
* 空的 `protocol_versions` 集合也會引發例外:一個永遠無法被提供的方法是 bug,不是設定。
|
||||
|
||||
### 用戶端這一側 {#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(...)` 在兩條路徑上都會讀取正確的來源。
|
||||
* 廠商方法要往下一層用 `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` 身分戳記,所以短路的攔截器永遠不會產生匿名或不符 schema 的回應。
|
||||
* 有多個擴充功能時,攔截器依註冊順序巢狀套疊:`extensions=[...]` 裡的第一個擴充功能在最外層。
|
||||
* 預設實作是直接放行,而擴充功能從未覆寫這個掛鉤的伺服器,會保持原本的 `tools/call` 處理函式不動。沒用到的東西不用付出代價。
|
||||
|
||||
這個掛鉤只包裹 `tools/call`,別無其他。需要處理每一則訊息的事情,請用[中介軟體](middleware.md)。那正是它的用途。
|
||||
|
||||
## 使用用戶端擴充功能 {#using-a-client-extension}
|
||||
|
||||
**用戶端擴充功能**是從使用端看的同一份契約:掛在單一識別碼之下的一組用戶端行為。把實例傳給 `Client(extensions=[...])`,然後照常呼叫工具:
|
||||
|
||||
```python title="client.py" hl_lines="66-68"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
`call_tool("buy", ...)` 回傳普通的 `CallToolResult`,和其他所有呼叫一樣。擴充功能改變的是:伺服器現在可以用 `receipt` **結果形狀**來回應 `buy`,而不是最終結果,而 `Receipts` 會在 `call_tool` 回傳之前把它完成(這裡是透過後續呼叫兌換收據)。呼叫端的程式碼完全不用動。
|
||||
|
||||
拿掉這個擴充功能,這一切就不存在:伺服器的關卡會拒絕沒有宣告它的用戶端(錯誤 -32021),而來自跳過關卡的伺服器的認領形狀會驗證失敗,完全符合規格對無法辨識的 `resultType` 的要求。預設關閉,線路的兩端都是。
|
||||
|
||||
要宣告一個**沒有**任何用戶端行為的識別碼(伺服器以這個能力為關卡,用戶端什麼都不做,就像上面的搜尋用戶端),使用 `advertise()`:
|
||||
|
||||
```python
|
||||
from mcp.client import advertise
|
||||
|
||||
client = Client(mcp, extensions=[advertise("com.example/search")])
|
||||
```
|
||||
|
||||
## 撰寫用戶端擴充功能 {#writing-a-client-extension}
|
||||
|
||||
繼承 `ClientExtension`,只覆寫需要的部分。三種貢獻類型,各有預設實作:`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` 是同一個公開控制柄,所以後續動作就是一般的工作階段(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)]
|
||||
```
|
||||
|
||||
處理函式一次收到一個驗證過的參數,依分派順序。它只觀察;不能否決或回覆。
|
||||
|
||||
兩條低調的規則。認領只在 2026-07-28 連線上生效,而能力宣告跟著它們走:在舊版連線上,認領會消失,識別碼也會跟著從宣告中移除,所以用戶端永遠不會宣告一個它會拒絕其形狀的擴充功能。另外,當你想自己拿到認領的形狀而不是交給解析器時,呼叫 `client.session.call_tool(..., allow_claimed=True)`;沒有這個旗標時,認領形狀抵達工作階段層的呼叫端會引發 `UnexpectedClaimedResult`。
|
||||
|
||||
### 擴充功能動詞 {#extension-verbs}
|
||||
|
||||
擴充功能自己的請求方法不需要用戶端註冊。廠商請求型別繼承 `mcp.types.Request`,並透過 `client.session.send_request` 送出,如[提供自己的方法](#serving-your-own-methods)所示。多一件事:當某個參數鍵必須搭上 `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` 攔截器。在用戶端上:設定、結果認領、通知繫結。擴充功能不能:
|
||||
|
||||
* **伸手進外層的伺服器或用戶端。**它只宣告資料,不持有任何伺服器或用戶端的參考。
|
||||
* **取代核心行為。**規格方法和核心結果標籤在建構時就會被拒絕(`initialize` 直接由執行器保留);被核心詞彙遮蔽的通知繫結則會靜默並發出警告。
|
||||
* **延後註冊。**`MCPServer(...)` 或 `Client(...)` 回傳之後,擴充功能集合就定了。
|
||||
|
||||
如果你在跟這些牆對抗,你寫的就不是擴充功能,而是 fork。這些牆本身就是功能:讀到 `extensions=[Apps(), Stamps()]` 的使用者,就知道這兩個東西**所有**可能碰過的地方。
|
||||
22
i18n/zh-hant/pages/advanced/index.md
Normal file
22
i18n/zh-hant/pages/advanced/index.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ca6988b7503cd2d3]
|
||||
tool: 1
|
||||
---
|
||||
# 進階 {#advanced}
|
||||
|
||||
一般的伺服器或用戶端需要的東西,在前面的章節裡都有對應的主題可循。這一節是當 `MCPServer` 的便利層反而礙事時,可以拿來用的逃生門:
|
||||
|
||||
* **[低階 Server](low-level-server.md)**:`MCPServer` 建構於其上的類別。手寫的 schema、`on_*` 處理函式、沒有任何東西會幫你檢查,還可以加上自訂的 JSON-RPC 方法。
|
||||
* **[分頁](pagination.md)** 和 **[中介軟體](middleware.md)**:兩件**只**能在低階 `Server` 上做的事。
|
||||
* **[擴充功能](extensions.md)** 和 **[MCP Apps](apps.md)**:協定的擴充介面。把擴充功能套件組合進伺服器,或自己寫一個。
|
||||
|
||||
有幾樣東西你可能理所當然會來這裡找,但它們其實放在實際會用到的地方:
|
||||
|
||||
* **授權** 放在 **[執行伺服器](../run/index.md)** 底下,因為伺服器部署在哪裡,就在哪裡保護它。
|
||||
* **OAuth**、**身分斷言**、連接 **多個伺服器**,以及回應 **快取**,都在 **[用戶端](../client/index.md)** 底下。
|
||||
* **多輪往返(multi-round-trip)請求** 和 **訂閱** 放在 **[在處理函式內部](../handlers/index.md)** 底下,因為兩者都是處理函式 **會做** 的事。
|
||||
* **URI 範本** 放在 **[伺服器](../servers/index.md)** 底下,就在資源旁邊。
|
||||
* **[協定版本](../protocol-versions.md)** 和 **[已棄用的功能](../deprecated.md)** 則各有自己的頂層頁面。
|
||||
|
||||
如果不確定自己需不需要這一節,那就是不需要。
|
||||
206
i18n/zh-hant/pages/advanced/low-level-server.md
Normal file
206
i18n/zh-hant/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()` 是一層包裝。底下還有第二個伺服器類別 `Server`,講的是原始的 MCP:把協定物件交給它,它就原封不動地放上線路。
|
||||
|
||||
`MCPServer` 就是建構在它之上。當便利層礙事時,才往下走:
|
||||
|
||||
* 需要送出**精確**的 schema(從檔案載入、從資料庫產生),而不是從 Python 簽章推導出來的。
|
||||
* 需要完全掌控結果:`_meta`、`is_error`、`structured_content` 的每一個鍵。
|
||||
* 需要處理 MCP 沒有定義的方法。
|
||||
|
||||
其他情況,就留在 `MCPServer`。
|
||||
|
||||
## 同一個工具,手工打造 {#the-same-tool-by-hand}
|
||||
|
||||
這是 **[工具](../servers/tools.md)** 用九行 `@mcp.tool()` 寫出的 `search_books` 工具,拿掉語法糖之後的樣子:
|
||||
|
||||
```python title="server.py" hl_lines="22 26 32"
|
||||
--8<-- "docs_src/lowlevel/tutorial001.py"
|
||||
```
|
||||
|
||||
改了三件事,而這三件事就是整個低階 API:
|
||||
|
||||
* **處理函式是建構子參數。** `on_list_tools=` 和 `on_call_tool=` 傳進 `Server(...)`。這一層沒有裝飾器,而且每個處理函式的形狀都一樣:`async (ctx, params) -> result`。
|
||||
* **輸入 schema 自己寫。** `Tool.input_schema` 是普通的 JSON Schema `dict`。沒有人會從型別提示推導它,因為根本沒有型別提示可以推導。
|
||||
* **結果自己組。** `CallToolResult(content=[TextContent(...)])`,手動建立。沒有任何東西會被包裝、轉換,或從回傳註記推斷出來。
|
||||
|
||||
`params` 是解析後的請求:`CallToolRequestParams` 提供 `.name` 和 `.arguments`。`ctx` 是 `ServerRequestContext`:`ctx.session` 用來回頭和用戶端溝通,還有 `ctx.lifespan_context`、`ctx.request_id`,以及 `ctx.meta`,也就是請求傳入的 `_meta`。
|
||||
|
||||
!!! info
|
||||
如果用過 FastAPI,這個關係你早就認識了。`MCPServer` 是裝飾器加型別提示的那一層;`Server` 是底下的 Starlette。兩者不是競爭對手:`MCPServer` 會建立一個 `Server`,並在上面註冊和這些一模一樣的處理函式。
|
||||
|
||||
### 試試看 {#try-it}
|
||||
|
||||
這個沒有 Inspector 可用:`mcp dev` 和 `mcp run` 只接受 `MCPServer`。記憶體內的 `Client` 則不在乎;它接收低階 `Server` 的方式和接收 `MCPServer` 完全一樣:
|
||||
|
||||
```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()` 版本產生的文字一模一樣。坦白說有兩個差異:
|
||||
|
||||
* `result.structured_content` 是 `None`。高階伺服器會幫你把 `-> str` 包成 `{"result": ...}`;在這裡,你沒建的東西,沒有人會替你建。
|
||||
* `list_tools` 回傳的是**你**打出來的 schema,一字不差。高階版本每個屬性上都有 `"title": "Query"`,根部還有一個 `"title": "search_booksArguments"`:那是 Pydantic 的產物。在這一層,線路上有的東西,都是你放上去的。
|
||||
|
||||
## 沒有人替你檢查 {#nothing-is-checked-for-you}
|
||||
|
||||
`MCPServer` 會在函式執行之前就拒絕錯誤的引數,依照它產生的 schema 驗證這次呼叫(**[工具](../servers/tools.md)**)。
|
||||
|
||||
`Server` 不做這件事。你的 `input_schema` 是**公告**給用戶端看的;從來不會**套用**到 `params.arguments` 上。
|
||||
|
||||
!!! check
|
||||
呼叫 `search_books` 時不帶 `limit`,`args["limit"]` 就會引發 `KeyError`。用戶端看到的是:
|
||||
|
||||
```text
|
||||
MCPError: Internal server error
|
||||
```
|
||||
|
||||
一個 JSON-RPC 錯誤,錯誤碼 `-32603`,訊息刻意寫得很籠統:SDK 不會把你的 traceback 洩漏給遠端呼叫端。模型永遠不知道自己哪裡做錯,所以無法重試。(在測試中,`raise_exceptions=True` 會改為浮現真正的例外;請見 **[測試](../get-started/testing.md)**。)
|
||||
|
||||
這可以推而廣之。從低階處理函式引發的例外**永遠**是協定錯誤,絕不會是 `is_error=True` 的工具結果。如果希望模型讀到失敗並恢復,就自己驗證 `params.arguments`,然後回傳 `CallToolResult(content=[TextContent(...)], is_error=True)`。這兩種失敗正是 **[處理錯誤](../servers/handling-errors.md)** 的主題。
|
||||
|
||||
## 兩個工具,一個處理函式 {#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` 取自建構子(沒設定的伺服器會回報空字串)。不能表明身分的伺服器可以用中介軟體把這個鍵拿掉,中介軟體擁有它回傳的結果。
|
||||
|
||||
伺服器從不比對這兩個欄位。這個 SDK 的 `Client` 會:回傳的 `structured_content` 如果不符合你宣告的 `output_schema`,`call_tool` 就會引發 `RuntimeError`,訊息以 `Invalid structured content returned by tool search_books` 開頭,接著引用 `jsonschema` 的失敗內容。承諾一個 schema 很便宜;守住承諾是你的事。回傳型別與 schema 的完整階梯請見 **[結構化輸出](../servers/structured-output.md)**。
|
||||
|
||||
## `_meta`:給應用程式,不是給模型 {#\_meta-for-the-application-not-the-model}
|
||||
|
||||
`content` 是答案中模型會讀的部分。`structured_content` 是同一個答案的型別化資料。`_meta` 是第三個管道:跟著結果一起送給**用戶端應用程式**的資料,完全不屬於答案的一部分。
|
||||
|
||||
用它放紀錄 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` 是你和用戶端應用程式之間的約定,不保證什麼會送到模型。要呈現什麼由 MCP 主機(host)決定。永遠不要在工具結果的任何部分放機密。
|
||||
|
||||
## 能力跟著處理函式走 {#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` 在處理函式執行**之前**用來驗證的模型,所以自訂方法**確實**享有工具沒有的驗證。繼承 `RequestParams`,讓 `_meta` 欄位和其他方法一樣解析。
|
||||
* 處理函式回傳 `BaseModel`、`dict` 或 `None`。SDK 會把它序列化成 JSON-RPC 結果。
|
||||
|
||||
一個坦白的提醒:高階 `Client` 只有對應 MCP 定義方法的動詞,所以沒有 `client.reindex()`。廠商方法是給已經知道它存在的對端用的:你同時發佈的用戶端,或是你自己另一個講 JSON-RPC 的服務。
|
||||
|
||||
有一個方法你不能占用:
|
||||
|
||||
```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))` 選擇加入:一行(兩個名稱都從 `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`,並從其他處理函式把事件發佈到 bus;完整的組合方式請見 **[訂閱](../handlers/subscriptions.md)**。
|
||||
* `server.streamable_http_app()` 回傳的 Starlette 應用程式和 `MCPServer` 的一樣;照 **[執行伺服器](../run/index.md)** 部署其他 ASGI 應用程式的方式部署它。這一層沒有 `server.run(transport=...)`:`server.run(read_stream, write_stream, server.create_initialization_options())` 透過一對串流驅動一條連線,而這一行就是全部。
|
||||
|
||||
## 重點回顧 {#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/zh-hant/pages/advanced/middleware.md
Normal file
84
i18n/zh-hant/pages/advanced/middleware.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
---
|
||||
translation:
|
||||
sections: [6048b4f308edbb8c, 068bda0f21ee9c1b, c3e565b61acd75c5, c62422b159c6ed09, 47204fab253cc45c]
|
||||
tool: 1
|
||||
---
|
||||
# 中介軟體 {#middleware}
|
||||
|
||||
**中介軟體(middleware)**是一個非同步函式,包住伺服器收到的每一則訊息。
|
||||
|
||||
寫成 `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}
|
||||
|
||||
一個伺服器、一個工具、一個中介軟體,記錄每則訊息花了多久:
|
||||
|
||||
```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}
|
||||
|
||||
連上一個用戶端,列出工具,呼叫其中一個。記錄裡會有**三**行:
|
||||
|
||||
```text
|
||||
server/discover took 18.3 ms
|
||||
tools/list took 0.1 ms
|
||||
tools/call took 0.1 ms
|
||||
```
|
||||
|
||||
呼叫了兩次,卻得到三行。第一行是 `server/discover`:這是用戶端為了建立連線而送出的請求,早在你要求任何東西之前。
|
||||
|
||||
重點就在這裡。中介軟體包住**每一則**傳入的訊息:
|
||||
|
||||
* 連線建立階段:`server/discover`,或在舊版工作階段(session)上的 `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`,那一則訊息就會以 JSON-RPC 錯誤回應。連線不會斷;下一則訊息照常通過。伺服器就是這樣依呼叫端控管 `subscriptions/listen` 的:訂閱頁面的 **[決定誰可以觀看](../handlers/subscriptions.md#deciding-who-may-watch)** 有逐步說明。
|
||||
* **改寫。**`ctx` 是一個 dataclass:`await call_next(dataclasses.replace(ctx, params=...))` 會把和用戶端送來的不同的參數交給鏈上剩下的部分。絕對不要對 `initialize` 這麼做:用戶端拿到的結果是根據你改寫後的參數建立的,但伺服器提交連線狀態時用的是線路上原本的參數。雙方可能在交握結束時,對彼此協商出的內容認知不一致。
|
||||
* **回答。**不呼叫 `call_next(ctx)` 就直接回傳一個結果,它會作為你的回應送到用戶端。`call_next` 交給你的是完成的線路格式,而管線絕不會修補你回傳的東西,所以整個封包都由你負責:在 2026 世代的連線上,這包括 `serverInfo` 的 `_meta` 戳記,SDK 會替處理函式的結果加上它,但不會替你的加。
|
||||
|
||||
!!! check
|
||||
`initialize` 是中介軟體包住的東西之一,而且這是它**唯一**的掛鉤點。試著用 `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)),會**讓連線死結**:你在等的回應永遠讀不到。射後不理的通知則沒問題。
|
||||
|
||||
## 唯一一個預設就啟用的中介軟體 {#the-one-middleware-that-ships-on-by-default}
|
||||
|
||||
SDK 只附帶一個中介軟體,而且它已經在伺服器的清單上了:為每則訊息發出一個 OpenTelemetry span 的那一個。不需要自己附加,大多數時候也不用去想它。在安裝匯出器之前它什麼都不做,而且有自己的頁面:**[OpenTelemetry](../run/opentelemetry.md)**。
|
||||
|
||||
!!! info
|
||||
如果寫過 ASGI 中介軟體,這個形狀你已經認得。Starlette 的 `(scope, receive, send)` 變成了 `(ctx, call_next)`,而且它在傳輸**之後**執行,處理的是解碼後的訊息而不是原始的 HTTP 請求。兩者可以組合:掛在 `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` 改為引發例外,就能拒絕一則訊息;連線會存活下來。
|
||||
* SDK 自己的 OpenTelemetry 追蹤也是一個中介軟體,已經在清單上。請見 **[OpenTelemetry](../run/opentelemetry.md)**。
|
||||
* 整個介面都是暫定的。用它來觀察;不要在它上面蓋東西。
|
||||
|
||||
以上就是包住請求的一切。至於請求到底能不能執行,則由 **[授權](../run/authorization.md)** 決定。
|
||||
81
i18n/zh-hant/pages/advanced/pagination.md
Normal file
81
i18n/zh-hant/pages/advanced/pagination.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
|
||||
tool: 1
|
||||
---
|
||||
# 分頁 {#pagination}
|
||||
|
||||
大多數伺服器永遠用不到這個。
|
||||
|
||||
`MCPServer` 回應每個 `list_*` 請求時,都把手上所有東西一次給完:一頁,`next_cursor=None`。對幾十個工具、資源或提示詞來說,這就是正確的答案,沒有什麼需要設定。
|
||||
|
||||
分頁是給資源清單其實是資料庫的那種伺服器用的:成千上萬列,它拒絕在一個回應裡全部序列化。協定的答案是**游標(cursor)**:伺服器回傳一頁加上一個不透明的 token,用戶端把這個 token 送回去,就能拿到下一頁。
|
||||
|
||||
`@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 blob:任何送出時能產生、送回來時認得出的東西都可以。
|
||||
* `next_cursor=None` 就是「那是最後一頁」的說法。沒有計數、沒有總數、沒有 `has_more`。`None` 就是全部的訊號。
|
||||
|
||||
!!! tip
|
||||
`PAGE_SIZE` 設成 10 是為了讓範例好讀。依端點各自挑選:一行就講完的資源清單,一頁 500 個也負擔得起;一堆肥大的提示詞範本清單就不行。用戶端對此沒有發言權,這是刻意的設計。
|
||||
|
||||
### 試試看 {#try-it}
|
||||
|
||||
`Client(server)` 在記憶體內連線到低階 `Server` 的方式,和連到 `MCPServer` 完全一樣。
|
||||
|
||||
不帶引數呼叫 `list_resources()`。會拿到十個資源,`book-1` 到 `book-10`,而 `next_cursor` 是字串 `"10"`。
|
||||
|
||||
用 `list_resources(cursor="10")` 把它交回去,第一個資源就是 `book-11`,新的 `next_cursor` 是 `"20"`。
|
||||
|
||||
第十頁回來時 `next_cursor` 是 `None`。結束。
|
||||
|
||||
## 用戶端迴圈 {#the-client-loop}
|
||||
|
||||
`Client` 上的每個 `list_*` 方法(`list_tools`、`list_resources`、`list_resource_templates`、`list_prompts`)都接受 `cursor=` 關鍵字引數。把分頁清單抓完只要一個 `while True`:
|
||||
|
||||
```python title="client.py" hl_lines="26-32"
|
||||
--8<-- "docs_src/pagination/tutorial002.py"
|
||||
```
|
||||
|
||||
* `cursor` 一開始是 `None`,所以第一個請求不帶游標。
|
||||
* 先 extend,**再**看 `next_cursor`:最後一頁也有資源。
|
||||
* `next_cursor is None` 就是出口。其他任何值都原封不動直接放回 `cursor=`。
|
||||
|
||||
執行它的 `main()`,會印出 `100 resources`:十頁、每頁十個,由一個從頭到尾不知道有十頁的迴圈接起來。
|
||||
|
||||
這和 **[用戶端](../client/index.md)** 為每個 `list_*` 動詞示範的迴圈是同一個,而且對不分頁的伺服器也沒有任何代價:第一個回應的 `next_cursor` 就是 `None`,迴圈只執行一次。
|
||||
|
||||
## 三條規則 {#the-three-rules}
|
||||
|
||||
**游標是不透明的。** 用戶端絕不能解析、組裝或猜測游標。游標唯一合法的來源,是上一頁的 `next_cursor`,一字不改。
|
||||
|
||||
**頁面大小由伺服器決定。** 協定裡沒有 `limit=`。如果需要不同的頁面大小,改的是伺服器。
|
||||
|
||||
**忽略分頁的用戶端照樣能用。** 它呼叫一次 `list_resources()`,拿到前十個,永遠不會注意到被它丟掉的 `next_cursor`。什麼都沒壞,只是看到的比較少。
|
||||
|
||||
!!! check
|
||||
不透明就是不透明。自己發明一個游標(`list_resources(cursor="page-2")`),協定也幫不了你。這個伺服器會嘗試 `int("page-2")`,處理函式引發例外,回到用戶端的是:
|
||||
|
||||
```text
|
||||
MCPError(-32603, 'Internal server error', None)
|
||||
```
|
||||
|
||||
不是從伺服器拿到的游標是 bug,不是功能需求。
|
||||
|
||||
## 重點回顧 {#recap}
|
||||
|
||||
* `MCPServer` 一頁回傳全部。分頁是選擇性啟用的,而啟用的地方是低階 `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`。
|
||||
* 游標是不透明的,頁面大小歸伺服器管,不分頁的用戶端還是拿得到第一頁。
|
||||
|
||||
手寫 `Server` API 的其餘部分(`on_call_tool`、`input_schema` dict、`_meta`)在 **[低階 Server](low-level-server.md)**。
|
||||
Loading…
Reference in a new issue