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

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

411
i18n/zh-hant/glossary.json Normal file
View file

@ -0,0 +1,411 @@
{
"keep": [
"MCP",
"Model Context Protocol",
"MCPServer",
"FastMCP",
"ClientSession",
"Context",
"ctx",
"stdio",
"Streamable HTTP",
"SSE",
"JSON-RPC",
"JSON",
"OAuth",
"PKCE",
"JWT",
"CIMD",
"HTTP",
"HTTPS",
"TLS",
"CORS",
"URI",
"URL",
"ASGI",
"WebSocket",
"API",
"SDK",
"CLI",
"IDE",
"LLM",
"SEP",
"RFC",
"Python",
"TypeScript",
"Node.js",
"PyPI",
"Pydantic",
"Starlette",
"FastAPI",
"uvicorn",
"httpx",
"anyio",
"asyncio",
"trio",
"pytest",
"OpenTelemetry",
"Inspector",
"Claude",
"GitHub",
"VS Code",
"Windows",
"macOS",
"Linux",
"llms.txt",
"2026-07-28",
"2025-11-25",
"2025-06-18",
"2025-03-26"
],
"terms": [
{
"source": "tool",
"target": "工具",
"note": "MCP protocol noun (a server exposes tools); \"tool call\" → 工具呼叫. Wire identifiers such as `tools/call` and `tools/list` are code and stay Latin. Provisional pending native review."
},
{
"source": "resource",
"target": "資源",
"note": "MCP protocol noun; \"resource template\" → 資源範本 (not 資源模板). `resources/read` stays Latin. Provisional pending native review."
},
{
"source": "prompt",
"target": "提示詞",
"note": "The MCP feature: a reusable prompt a server exposes (`prompts/get` stays Latin), and the everyday LLM word. Provisional pending native review: 提示詞 rather than the bare 提示, which reads as \"hint\" and is what a `tip` admonition title becomes; several Taiwan vendor docs use 提示 for prompt, so this is an open choice — but one rendering per page."
},
{
"source": "sampling",
"target": "取樣",
"note": "The (deprecated) client feature that lets a server borrow the client's model. Gloss the English on first use per page: 取樣(sampling). Taiwan signal-processing usage is 取樣; 抽樣 is statistical sampling and the wrong sense, 采样 is Simplified. The `sampling` capability key and `sampling/createMessage` stay Latin. Provisional pending native review.",
"avoid": ["采样", "抽樣"]
},
{
"source": "roots",
"target": "根目錄",
"note": "The (deprecated) client feature listing workspace folders; a `Root` object in code font stays Latin. Gloss the English on first use per page: 根目錄(roots). Never the bare 根 and never 根節點 (a tree node). Provisional pending native review.",
"avoid": ["根节点", "根節點"]
},
{
"source": "elicitation",
"target": "徵詢",
"note": "OPEN QUESTION for native review: there is no settled Taiwan term for the server asking the user a question mid-request; candidates seen are 徵詢, 引導, 詢問 and 誘導 (the last carries a manipulative overtone and is not wanted). Provisionally pinned to 徵詢, glossed with the English on first use per page: 徵詢(elicitation); the verb \"elicit\" is 向使用者徵詢. Write it with 徵, never the Simplified or mis-converted 征. `elicitation/create` and the `Elicit` class stay Latin.",
"avoid": ["征询", "征詢"]
},
{
"source": "capability",
"target": "能力",
"note": "A negotiated protocol capability (宣告了 `sampling` 能力; \"capability negotiation\" → 能力協商). The `capabilities` field and keys such as `sampling.tools` stay Latin. Not 功能, which means \"feature\" — the corpus uses \"feature\" as a separate word. Provisional pending native review."
},
{
"source": "transport",
"target": "傳輸",
"note": "As a countable noun use 傳輸方式 (\"three transports\" → 三種傳輸方式; the \"Transports\" page → 傳輸方式). The transport names stdio, Streamable HTTP and SSE stay in English. Provisional pending native review."
},
{
"source": "session",
"target": "工作階段",
"note": "An MCP session (the negotiated connection state). Provisional pending native review: 工作階段 is the established Taiwan localisation; gloss the English on first use per page — 工作階段(session). 會話 is the Mainland rendering and in Taiwan means a language-conversation class; `session` and `ctx.session` in code font stay Latin, as does the class name ClientSession. Open question: many Taiwanese developers simply write session in English.",
"avoid": ["会话", "會話"]
},
{
"source": "handler",
"target": "處理函式",
"note": "The tool, resource or prompt function you register (nav section \"Inside your handler\" → 在處理函式內部). Provisional pending native review: 處理函式 rather than the vendor form 處理常式 or the Mainland 處理器/處理函數; pick-one-and-never-mix applies within a page."
},
{
"source": "dependency",
"target": "相依性",
"note": "The SDK's parameter-injection feature (the \"Dependencies\" page → 相依性); \"dependency injection\" → 相依性注入; a package dependency in installation contexts → 相依套件. Provisional pending native review: 依賴/依賴注入 is the widespread community alternative and is not wrong in Taiwan, but do not mix the two families on one page. The `Resolve` marker class stays Latin."
},
{
"source": "client",
"target": "用戶端",
"note": "An MCP client, and the client side of a connection (\"client-side\" → 用戶端). The `Client` class name stays Latin in code font. Provisional pending native review: 用戶端 is the form Taiwan platform vendors use; 客戶端 is also heard in Taiwan but is pinned out here so a page never mixes the two, and 客户端 is Simplified.",
"avoid": ["客户端", "客戶端"]
},
{
"source": "server",
"target": "伺服器",
"note": "An MCP server (the program you build); \"server-side\" → 伺服器端. The low-level `Server` class and `MCPServer` stay Latin in code font. 服務器/服务器 is the Mainland word and never appears on a Taiwan page. Provisional pending native review.",
"avoid": ["服务器", "服務器"]
},
{
"source": "host",
"target": "主機",
"note": "The MCP host: the application that embeds the client and drives the model (Claude Desktop, an IDE). Gloss the English on first use per page when it is this protocol role — MCP 主機(host) — because 主機 alone first suggests a machine; a network host or hostname is also 主機/主機名稱. When \"the host app\" means the outer ASGI application an MCP server is mounted into, write 外層應用程式 (or 主應用程式), not 主機. Never 宿主, the Mainland docs convention. Provisional pending native review.",
"avoid": ["宿主"]
},
{
"source": "context",
"target": "上下文",
"note": "The generic lower-case word (\"provide context to LLMs\" → 為 LLM 提供上下文). Provisional pending native review: 上下文 is widely understood in Taiwan developer writing; 脈絡 and 情境 are the candidates a reviewer may prefer. The capitalised `Context` is the SDK object injected as `ctx`; both are on the keep list and stay Latin in prose (\"The Context\" → Context). The idiom \"in this context\" is not this term and may be recast (在這裡, 這種情況下)."
},
{
"source": "request",
"target": "請求",
"note": "A JSON-RPC or HTTP request (\"the initialize request\" → initialize 請求; \"Multi-round-trip requests\" → 多輪往返請求). Provisional pending native review: some Taiwan vendor docs write 要求 for an HTTP request; 請求 is pinned here. `Request` types in code font stay Latin."
},
{
"source": "response",
"target": "回應",
"note": "A JSON-RPC or HTTP response; the verb \"respond\" is also 回應. Never 響應 (Mainland; in Taiwan it means answering a call to action) — it is not on the avoid list only because the characters also occur inside 影響應用程式. `Response` types in code font stay Latin. Provisional pending native review.",
"avoid": ["响应"]
},
{
"source": "callback",
"target": "回呼",
"note": "Client callbacks and OAuth redirect callbacks alike (\"the elicitation callback\" → 徵詢回呼; \"Callbacks\" page → 回呼); 回呼函式 where a countable noun reads better. Never the Mainland 回調. Parameter names such as `sampling_callback` stay Latin. Provisional pending native review.",
"avoid": ["回调"]
},
{
"source": "decorator",
"target": "裝飾器",
"note": "The Python decorators the SDK is built on; `@mcp.tool()` and its siblings are code and stay untouched. Provisional pending native review."
},
{
"source": "type hint",
"target": "型別提示",
"note": "Python type hints (\"from your type hints\" → 從型別提示; \"type annotation\" → 型別註記). Taiwan usage is 型別 for a data type; 類型 is the general word for \"kind\" and stays available for that sense. Provisional pending native review.",
"avoid": ["类型提示", "類型提示"]
},
{
"source": "notification",
"target": "通知",
"note": "A JSON-RPC notification (a message that expects no response); \"change notifications\" → 變更通知. Method strings such as `notifications/tools/list_changed` stay Latin. Provisional pending native review."
},
{
"source": "multi-round-trip",
"target": "多輪往返",
"note": "The 2026-07-28 request pattern (\"Multi-round-trip requests\" → 多輪往返請求); a single \"round trip\" → 往返 (一次往返). Provisional coinage pending native review: gloss the English on first use per page — 多輪往返(multi-round-trip). The abbreviation MRTR stays Latin."
},
{
"source": "lifespan",
"target": "生命週期",
"note": "The server's startup/shutdown scope (the \"Lifespan\" page → 生命週期), written with Taiwan's 週, not 周. The neighbouring word \"lifecycle\" also renders 生命週期 and the overlap is accepted. The `lifespan` parameter name stays Latin in code font. 壽命 is the biological sense and wrong here. Provisional pending native review; keeping the English word lifespan in prose is the alternative a reviewer may prefer.",
"avoid": ["生命周期"]
},
{
"source": "back-channel",
"target": "反向通道",
"note": "The server-to-client request channel that exists only on 2025-era, non-stateless connections. Provisional coinage pending native review: gloss the English on first use per page — 反向通道(back-channel). A generic \"channel\" is 通道 or 管道, never the Mainland 渠道."
},
{
"source": "deprecated",
"target": "已棄用",
"note": "Advisory status: still works, scheduled for removal later. \"X is deprecated\" → X 已棄用; \"deprecation warning\" → 棄用警告; \"Deprecated features\" → 已棄用的功能; \"removed\" is a different word (已移除) and the corpus contrasts the two. Provisional pending native review: 已淘汰 is the rendering several Taiwan platform docs use and is the alternative to confirm against; do not mix the two families on one page. The `MCPDeprecationWarning` class stays Latin."
},
{
"source": "resolver",
"target": "解析器",
"note": "The function attached to a parameter with `Resolve(...)` that computes or asks for its value (\"an elicitation resolver\" → 徵詢解析器). The `Resolve` class stays Latin. Provisional pending native review, together with the handler entry."
},
{
"source": "wire",
"target": "線路",
"note": "The corpus's light metaphor for the byte stream between client and server (\"stdout is the wire\" → stdout 就是線路本身; \"invisible on the wire\" → 在線路上看不到; \"the JSON on the wire\" → 實際傳輸的 JSON). Never a literal 電線. Provisional pending native review.",
"avoid": ["电线", "電線"]
},
{
"source": "era",
"target": "世代",
"note": "\"Protocol era\" (\"a 2025-era client\", \"whatever era the client speaks\") → 協定世代, 2025 世代的用戶端. Provisional pending native review; not the literal 時代."
},
{
"source": "legacy",
"target": "舊版",
"note": "\"A legacy connection / client\" = one negotiated at spec version 2025-11-25 or earlier → 舊版連線, 舊版用戶端; \"Serving legacy clients\" → 服務舊版用戶端. Provisional pending native review."
},
{
"source": "handshake",
"target": "交握",
"note": "The initialization handshake (\"the classic handshake\" → 傳統的交握; \"three-way handshake\" is 三向交握 in Taiwan networking texts). Never the Mainland/literal 握手 — it is kept off the avoid list only because the characters also occur inside 掌握手動. Provisional pending native review."
},
{
"source": "protocol",
"target": "協定",
"note": "\"the protocol\" → 協定 (通訊協定 where the fuller form reads better); \"Protocol versions\" → 協定版本. The name Model Context Protocol is on the keep list and stays English. Never the Mainland 協議, which in Taiwan means an agreement. Provisional pending native review.",
"avoid": ["协议"]
},
{
"source": "extension",
"target": "擴充功能",
"note": "A protocol extension (the \"Extensions\" page, SEP-numbered extensions) → 擴充功能; \"declare an extension\" → 宣告一個擴充功能. A file extension is 副檔名; a pip extra such as `[cli]` is an extra, not this term. 擴展 stays available as the verb \"to scale / expand\" (\"Deploy & scale\" → 部署與擴展). Provisional pending native review.",
"avoid": ["扩展"]
},
{
"source": "completion",
"target": "自動完成",
"note": "The MCP feature that suggests values for prompt and resource-template arguments (the \"Completions\" page → 自動完成; `completion/complete` stays Latin). When \"completion\" means the text a model generates during sampling, write 生成結果 instead. Never the Mainland 補全. Provisional pending native review."
},
{
"source": "middleware",
"target": "中介軟體",
"note": "The \"Middleware\" page and `server.middleware` (code stays Latin). Provisional pending native review: 中介軟體 is the Taiwan form; 中間件 is Mainland; many Taiwanese developers keep middleware in English, which is the alternative to confirm against.",
"avoid": ["中间件"]
},
{
"source": "authorization",
"target": "授權",
"note": "OAuth authorization → 授權 (授權伺服器, 授權碼); authentication is a different word → 驗證/身分驗證, and identity is written 身分. The `Authorization` header and code identifiers stay Latin. Provisional pending native review."
},
{
"source": "exception",
"target": "例外",
"note": "A raised Python exception (\"raises an exception\" → 引發例外; \"catch\" → 攔截/捕捉). Never 異常, which in Taiwan means \"abnormal\". Exception class names stay Latin. Provisional pending native review."
},
{
"source": "argument",
"target": "引數",
"note": "A value passed in a call → 引數; the declared parameter → 參數 (\"tool arguments\" → 工具引數; \"a parameter with a default\" → 有預設值的參數). Provisional pending native review: everyday Taiwan writing often says 參數 for both; keep the distinction where the English makes it."
},
{
"source": "return value",
"target": "回傳值",
"note": "A function's return value; the verb \"return\" (a value) → 回傳. Use 回傳 consistently rather than alternating with 傳回. 返回 means \"go back\" (返回上一頁) and is never used for returning a value. The `return` keyword is code. Provisional pending native review."
},
{
"source": "async",
"target": "非同步",
"note": "The prose adjective (\"the async runtime\" → 非同步執行環境, \"an async callback\" → 非同步回呼); synchronous → 同步. Never the Mainland 異步. The `async` and `await` keywords in code font stay Latin. Provisional pending native review.",
"avoid": ["异步"]
},
{
"source": "function",
"target": "函式",
"note": "A Python function → 函式 (\"a plain Python function\" → 一個普通的 Python 函式). Provisional pending native review: 函數 is the mathematics word and common in older Taiwan programming texts; 函式 is pinned here and the two are never mixed on a page.",
"avoid": ["函数"]
},
{
"source": "object",
"target": "物件",
"note": "A Python or JSON object → 物件. Never 對象, which in Taiwan means a target audience or counterpart, not a programming object. Provisional pending native review.",
"avoid": ["对象"]
},
{
"source": "variable",
"target": "變數",
"note": "\"environment variable\" → 環境變數; a template variable → 變數. Never the Mainland 變量. Provisional pending native review.",
"avoid": ["变量"]
},
{
"source": "string",
"target": "字串",
"note": "A text string → 字串; a character → 字元. Never 字符串/字符. Provisional pending native review.",
"avoid": ["字符串"]
},
{
"source": "default",
"target": "預設",
"note": "\"the default\" → 預設值; \"by default\" / \"out of the box\" → 預設情況下; \"defaults to X\" → 預設為 X. Never 默認, which in Taiwan means \"to tacitly admit\". Provisional pending native review.",
"avoid": ["默认", "默認"]
},
{
"source": "data",
"target": "資料",
"note": "Computing data → 資料 (資料庫, 資料結構, 中繼資料 for metadata). 數據 in Taiwan means numeric figures or statistics and is not the general word; 数据 is Simplified. Provisional pending native review.",
"avoid": ["数据", "數據庫"]
},
{
"source": "file",
"target": "檔案",
"note": "A file on disk → 檔案 (\"Create a file `server.py`\" → 建立 `server.py` 檔案; \"config file\" → 設定檔). 文件 in Taiwan means a document or documentation (說明文件) and is used only in that sense. Provisional pending native review."
},
{
"source": "code",
"target": "程式碼",
"note": "Source code → 程式碼; a program → 程式; \"no validation code\" → 不用寫驗證程式碼. An error or status code is 錯誤碼/狀態碼. Never 代碼 for source code. Provisional pending native review.",
"avoid": ["代码"]
},
{
"source": "software",
"target": "軟體",
"note": "軟體, never the Mainland/Hong Kong 軟件; likewise 硬體 for hardware. Provisional pending native review.",
"avoid": ["软件", "軟件"]
},
{
"source": "network",
"target": "網路",
"note": "網路 (the Internet → 網際網路), never 網絡, which is the Mainland/Hong Kong form. Provisional pending native review.",
"avoid": ["网络", "網絡"]
},
{
"source": "memory",
"target": "記憶體",
"note": "RAM / process memory → 記憶體; \"in-memory\" (transport, client, token storage) → 記憶體內 (記憶體內傳輸, 存在記憶體內). Never the Mainland 內存. Provisional pending native review.",
"avoid": ["内存"]
},
{
"source": "message",
"target": "訊息",
"note": "A JSON-RPC, log or error message → 訊息; \"information\" → 資訊 (an `info` admonition title, when translated, is 資訊). Never 信息 (Mainland for both) and never 消息, which in Taiwan means news. Provisional pending native review.",
"avoid": ["信息"]
},
{
"source": "support",
"target": "支援",
"note": "Technical support of a feature or version (\"supports Python 3.10\" → 支援 Python 3.10; \"not supported\" → 不支援). In Taiwan 支持 means to endorse or back someone and is wrong for this sense; it is kept off the avoid list only because the characters also occur inside 分支持續. Provisional pending native review."
},
{
"source": "user",
"target": "使用者",
"note": "The person at the keyboard → 使用者 (\"ask the user\" → 詢問使用者). Not 用戶, which Taiwan reserves for an account holder or subscriber — except inside the fixed term 用戶端 (client). Provisional pending native review.",
"avoid": ["用户"]
},
{
"source": "cache",
"target": "快取",
"note": "Noun and verb (the \"Caching\" page → 快取; \"cached for 60 seconds\" → 快取 60 秒). Never the Mainland 緩存. Provisional pending native review.",
"avoid": ["缓存"]
},
{
"source": "logging",
"target": "記錄",
"note": "The activity and the \"Logging\" page → 記錄; \"the server log\" → 伺服器記錄; \"a log message\" → 記錄訊息; \"log level\" → 記錄層級. The `logging` module and logger names stay Latin. Provisional pending native review: 日誌 is common in Taiwan too and is the alternative to confirm against; do not mix the two on one page. 日志 is Simplified.",
"avoid": ["日志"]
},
{
"source": "schema",
"target": "schema",
"note": "Keep the English word in Chinese prose, singular and lower-case (輸入 schema, 從型別提示產生 schema; JSON Schema as a proper name keeps its capitals). Provisional pending native review: Taiwanese developers overwhelmingly say schema; 結構描述 is the formal vendor rendering a reviewer may prefer. Never 模式 (which means \"mode\", as in 無狀態模式) and never 架構 (architecture) for this word."
},
{
"source": "run",
"target": "執行",
"note": "\"Run the server\" → 執行伺服器; \"Running your server\" (nav section) → 執行伺服器; \"at runtime\" → 執行時. Never the Mainland 運行. A colloquial 跑 is acceptable once in a light aside, not as the standard verb. Provisional pending native review.",
"avoid": ["运行"]
},
{
"source": "call",
"target": "呼叫",
"note": "To call a tool or function → 呼叫 (\"call `add`\" → 呼叫 `add`; \"tool call\" → 工具呼叫; \"the caller\" → 呼叫端). Never the Mainland 調用 — it is kept off the avoid list only because the characters also occur inside 協調用戶端. Provisional pending native review.",
"avoid": ["调用"]
},
{
"source": "you",
"target": "你",
"note": "The register rule from instructions.md made machine-checkable: 您 must never appear on a page. Prefer dropping the pronoun; when one is needed it is 你.",
"avoid": ["您"]
},
{
"source": "Get started",
"target": "開始使用",
"note": "The nav section that opens the guide, and the title of its index page. \"First steps\" is a separate page inside that section (第一步), so the two need distinct renderings or the sidebar shows the same title twice. Provisional pending native review; 快速入門 is the alternative for the section."
},
{
"source": "First steps",
"target": "第一步",
"note": "The tutorial page inside the \"Get started\" section; never reuse this rendering for the section itself (see that entry). Provisional pending native review."
},
{
"source": "Recap",
"target": "重點回顧",
"note": "Recurring section heading that closes most pages; one rendering everywhere, not 重點回顧 on some pages and 小結 or 回顧 on others. Provisional pending native review."
},
{
"source": "Try it",
"target": "試試看",
"note": "Recurring section heading above a runnable example; one rendering everywhere, not 試一試 or 動手試試 on some pages. Provisional pending native review."
}
]
}

View file

@ -0,0 +1,173 @@
# Traditional Chinese, Taiwan (zh-hant) — translation instructions
Target language: Traditional Chinese as written for readers in Taiwan (繁體中文,台灣用語),
directory and URL code `zh-hant`, page language tag `zh-Hant`. This file is sent verbatim
with every translation request for this language, on top of the shared translation rules
in `../general-prompt.md`. The termbase in `glossary.json` is sent alongside it and wins
any terminology conflict with this file.
This is a language target in its own right, translated directly from the English — never
a character conversion of a Simplified Chinese text. Taiwan and Mainland usage differ in
hundreds of everyday computing words (伺服器/服务器, 程式碼/代码, 預設/默认, 物件/对象);
a converted page reads as foreign in Taiwan even when every character is Traditional.
## 1. Register
Write the casual-neutral register that developer documentation in Taiwan uses: plain,
even, close to how an engineer explains something aloud, without being chatty.
- Address the reader as 你. Never the honorific 您, never 您們, and never a mix. The rule
holds in body prose, headings, admonition titles, table cells and link text.
- Prefer no pronoun at all when the sentence stays clear: "You can pass a schema" →
可以傳入一個 schema. Reach for 你 only where the sentence would otherwise be ambiguous
about who acts. "Your server" is 伺服器, or 你的伺服器 only when ownership is the point.
- Steps and instructions are bare imperatives without a subject: "Run the server" →
執行伺服器, not 請您執行伺服器. A single 請 is fine where it reads naturally; a 請 in
front of every step is not, and neither is 若要……,請…… opening every paragraph.
- The register is uniform across a page. A page that drifts between 你 and 您, or between
plain sentences and stiff officialese, is wrong even when each sentence is fine alone.
## 2. Voice
Aim for the voice of an experienced Taiwanese engineer walking a colleague through a
library: warm, direct, professional, compact. The English is built on short declarative
payoff sentences ("That's a complete MCP server."); keep them short —
這就是一個完整的 MCP 伺服器。
Do:
- Follow Chinese word order and rhythm. Break one long English sentence into two Chinese
ones instead of mirroring its clause structure, and use concrete verbs (執行, 傳入,
回傳, 宣告, 阻塞) rather than nominal chains: 進行設定 → 設定; 對……進行處理 → 處理…….
- Keep the source's directness. Where the English says "don't", the Chinese says 不要,
not a hedge like 或許可以考慮避免.
- Use Taiwan function words: 透過 for "via / through" (通過 means "to pass" a check),
和/與 for "and", 如果/若 for "if", 即可/就好 to close an instruction lightly.
Avoid — these are the marks of a machine, converted or customer-service translation:
- 您 and its whole register: 溫馨提示, 敬請, 感謝您的耐心, 親愛的使用者.
- Mainland computing vocabulary and colloquialisms, even in Traditional characters:
服務器, 數據庫, 默認, 信息, 視頻, 質量 (for quality), 反饋, 渠道, 激活, 立馬, 挺好.
- Formal padding (進行……操作, 對……進行……) and document-speak (本文件旨在, 使用者應, 茲).
- English-shaped Chinese: possessive chains (你的伺服器的工具的 schema), 被 passives
where a topic–comment sentence is natural ("The tool is called by the model" →
模型會呼叫這個工具), a translated connective (然而, 因此, 此外) at the start of every
sentence, and 它 standing in for every "it".
- Internet slang from either side of the strait (神器, 保姆級, 給力, 超好用, 就醬), and
sentence-final particles 喔/囉/啦/耶.
Example — English: "You don't construct it and you don't configure it. You ask for it."
- Not this (您 + officialese): 您無需對其進行建構及配置,僅需提出請求即可。
- Not this either (Mainland casual): 你不用创建它也不用配置它,直接要就完事了。
- This: 不需要自己建立,也不需要設定,只要開口要就好。
## 3. Humour and idioms
- The English is friendly and dry rather than jokey; carry the friendliness, recast the
idioms. Never translate a pun, idiom or aside literally: say what it means as a short,
natural sentence in the same register. If an aside carries no information you may drop
it — but never drop a technical caveat that happens to be phrased lightly.
- Recurring English tags get fixed renderings: "**[X](…)** has the whole story" / "The
whole story is in **[X](…)**" → 完整說明請見 **[X](…)**; "That's the whole API." /
"That's the whole protocol." → 整個 API 就這樣。/ 整個協定就這樣。; "That's it. It's
just Python." → 就這樣,就只是 Python 而已。; "That's a complete MCP server." →
這就是一個完整的 MCP 伺服器。
- Idioms take the plain meaning, not the picture: "Out of the box the app answers
**only** requests addressed to localhost." → 預設情況下,這個應用程式**只**回應送往
localhost 的請求。— not the literal 開箱即用.
- Emoji and exclamation marks: keep the source's rare, deliberately placed emoji exactly
where they are — two payoff lines end in ✨ ("You get `3` back. ✨") — and never add
new ones. Do not add an exclamation mark to a plain payoff sentence; where one is kept
it is the full-width !, never doubled, never in a heading.
- Worked examples (source → good / bad): "You get `3` back. ✨" → 得到的結果是 `3`。✨
/ 您將獲得3!✨ (您, stiff 獲得, missing Han–Latin spacing, added exclamation). "Give a
parameter a default value and it stops being required. That's it. It's just Python."
→ 替參數加上預設值,它就不再是必填。就這樣,就只是 Python 而已。/
給一個參數一個默認值,然後它就停止是必需的了。就是它。它只是Python而已!(Mainland 默認,
English-shaped 它 chain, missing spacing, added exclamation).
## 4. Typography
- Chinese prose takes full-width punctuation: ,。:;!?、() with the dash —— and
the ellipsis ……; enumerations use 、 ("a, b, and c" → a、b 和 c). Parentheses are
full-width () even around Latin text, as in the first-use gloss 取樣(sampling).
Punctuation inside code spans, code blocks, commands, URLs and quoted English text
stays half-width and untouched. An English em-dash aside is usually better recast with
,, () or a second sentence than kept as ——; a colon introducing a code block, list
or example becomes : or a full sentence ending in 。.
- Quotation marks are the corner brackets 「」, with 『』 nested inside; never “ ” or
‘ ’ in Chinese text, and never 「」 around a code span. Titles of works take 《》.
- Put one half-width space between Han characters and any run of Latin letters or
digits — an English word, a number, an inline code span, a link whose text is Latin
(使用 Streamable HTTP 傳輸; 需要 Python 3.10+; 會收到 `Context`); put no space between
a full-width punctuation mark and adjacent Latin text (設定好 stdio。). Keep the spaces
around Markdown markers (`**…**`, links) exactly as the source has them.
- No italics in Chinese text. Where the source italicises a word for emphasis, use
**bold**; where it italicises an example utterance or a hypothetical question the user
might see, wrap it in 「」 instead. Keep bold on the same words the source bolds — a
bolded negation ("**not**" → **不是** / **不會**) stays bold. Emphasis markers around
text that stays in English are copied as-is.
- Digits stay half-width Arabic numerals (3 個工具, not 三個). Protocol revision strings
such as `2026-07-28` and `2025-11-25` are identifiers, copied byte-for-byte — never
2026 年 7 月 28 日, never 2026/07/28. Version numbers, HTTP status codes, ports, error
codes, and RFC and SEP numbers are copied exactly. A Latin unit takes a space (10 MB,
30 s); % attaches with none; a Chinese unit or measure word needs none (5 秒, 3 個).
- Line breaks: never put a newline between two Chinese characters (Han or full-width
punctuation), not even after 。 — the renderer turns it into a stray space. Where the
English wraps a paragraph, list item or admonition body over several lines, or gives
each sentence its own line, write the Chinese on one line, sentence after sentence;
block structure and indentation otherwise stay as in the source. The home page's
opening note puts "New to v2…", "Still on v1.x?…" and "Something rough or confusing?…"
on three lines; in Chinese that body is the single indented line
剛接觸 v2……破壞性變更。還在用 v1.x?……。哪裡卡住或看不懂?…… — three indented lines
there are wrong. A prose line ending in a Chinese character followed by a line of the
same block starting with one is always a defect: join the two.
## 5. Terminology pointer
The termbase `glossary.json` is injected separately and overrides anything written here.
This section fixes the conventions it assumes and the Taiwan forms it does not pin:
- Terms in the glossary's keep list, and any other English word left in Latin script,
are copied exactly as spelled, always singular, with no article and no plural "s":
"the URIs" → URI, "schemas" → schema. Everything in code font, plus API names, class,
function and parameter names, protocol method and message strings (`tools/call`,
`notifications/...`), header names, error codes, SEP numbers and product names, stays
in Latin script inline. A glossary term used as a code-font identifier stays Latin
even though its prose noun is translated: "the `sampling` capability" → `sampling` 能力.
- Text quoted from what the example code prints or displays — an output line, a log
message, an error string, a UI label such as the Inspector's **Tools** tab — stays
exactly as the code emits it (usually English), in or out of code font; do not
translate it or add a Chinese reading.
- First-use gloss: a translated MCP concept the reader may need to map back to the
English specification carries the English in full-width parentheses on its first
occurrence on a page — 取樣(sampling), 徵詢(elicitation) — and appears alone after
that. Each glossary entry's note says whether the term takes the gloss.
- One rendering per term per page: the glossary target, every time — also where an
entry's note marks the choice as open or provisional; never pick per sentence.
- Taiwan forms for words the glossary leaves unpinned (the bracketed Mainland form is
not used): 程式 program〔程序, which means "procedure" in Taiwan〕, 應用程式
application〔never bare 應用 as a noun〕, 資料庫 database〔數據庫〕, 欄位 field〔字段〕,
範例 example〔示例〕, 範本 template〔模板〕, 文件/說明文件 documentation〔文檔〕, 設定
configure, settings〔配置, which means "allocate"〕, 建立 create〔創建〕, 啟用/停用
enable, disable〔激活, 禁用〕, 介面 interface〔接口〕, 連結 link〔鏈接〕, 登入 log
in〔登錄〕, 標頭 header〔請求頭〕, 逾時 timeout〔超時〕, 連接埠 port〔端口〕, 執行緒
thread〔線程〕, 處理程序 process〔進程〕, 權杖 token in the OAuth sense〔令牌〕, 身分
identity, 疑難排解 troubleshooting〔故障排除, 排查〕, 偵錯 debug〔調試〕, 印出
print〔打印〕, 圖示 icon〔圖標〕, 音訊 audio〔音頻〕, 型別 type in the data-type
sense〔類型 is fine for "kind of"〕, 套件 package〔包〕, 相容 compatible〔兼容〕, 串流
stream〔流〕, 對話 conversation〔會話〕. In a table, a row is 列 and a column is 欄 —
the reverse of Mainland usage.
## 6. Provisional note
The register, voice and terminology decisions above, and every entry in `glossary.json`,
are provisional pending review by native readers in Taiwan. To propose a change — a
better rendering, a rule that produces awkward Chinese, a missing or wrong Taiwan form —
edit this file or `glossary.json` in a pull request, ideally with a short good/bad
example; never edit the generated `pages/` or `notices.md` next to this file. The tool
cannot tell a hand edit from its own output, so one would persist unchecked and be
carried forward into later runs; a correction made here reaches the pages when they are
regenerated with `translate --lang zh-hant --pages …`.

20
i18n/zh-hant/notices.md Normal file
View file

@ -0,0 +1,20 @@
---
translation:
sections: [aff1b3e872b7876a, 4d80558ad052d586, 0bb81f1e62062d26, d5c35dcec50156bc]
tool: 1
---
# 翻譯說明 {#translation-notices}
翻譯版說明文件網站的每一頁頂端都會出現以下其中一則說明。
## 機器翻譯 {#translated}
本頁是從英文說明文件自動翻譯而來,以[英文頁面](ENGLISH_PAGE)為準。如果哪裡讀起來不對勁,[翻譯](TRANSLATIONS_PAGE)有說明如何回報。
## 翻譯落後於英文頁面 {#outdated}
這份翻譯完成之後,英文頁面又有更動,因此部分內容可能已經過時。有疑問時請讀[英文頁面](ENGLISH_PAGE);[翻譯](TRANSLATIONS_PAGE)有說明翻譯版說明文件的運作方式。
## 以英文顯示 {#english}
本頁目前沒有翻譯,所以你讀到的是英文版。[翻譯](TRANSLATIONS_PAGE)有說明翻譯版說明文件的運作方式。

View 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
```

View 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()]` 的使用者,就知道這兩個東西**所有**可能碰過的地方。

View 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)** 則各有自己的頂層頁面。
如果不確定自己需不需要這一節,那就是不需要。

View file

@ -0,0 +1,206 @@
---
translation:
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
tool: 1
---
# 低階 Server {#the-low-level-server}
`@mcp.tool()` 是一層包裝。底下還有第二個伺服器類別 `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)**。

View 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)** 決定。

View 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)**。

View 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` 回傳的每個結果都帶有兩個欄位:`ttlMs`,表示用戶端可以把這個結果視為新鮮的毫秒數;`cacheScope`,表示快取的結果可以跨使用者共用(`"public"`),還是只屬於某一個授權上下文(`"private"`)。
伺服器本身什麼都不快取。這兩個欄位是一種**宣告**:「這份工具清單對所有人都一樣,而且一分鐘內不會變。」用戶端(或擋在你前面的閘道)就可以省掉這次往返。要不要遵守這些提示,由用戶端決定;送出這些提示則是伺服器的工作,而 SDK 會替你處理。
預設情況下,每個結果都是 `ttlMs: 0, cacheScope: "private"`:立刻過期、永不共用。這永遠安全,也永遠符合規範。如果你的清單確實穩定,而且對所有呼叫端都相同,就在建構時說清楚:
```python title="server.py" hl_lines="5-8"
--8<-- "docs_src/caching/tutorial001.py"
```
* 這個對應表以**方法名稱**為鍵,而且只有這六個可快取的方法是合法的鍵。參數的型別是 `Mapping[CacheableMethod, CacheHint]`,所以編輯器會自動完成這些鍵,並在執行前標出拼字錯誤;任何躲過型別檢查器的錯誤,都會在建構時引發例外。
* 沒提到的方法就維持預設值。這個對應表是一組覆寫,不是完整清單。
* `CacheHint(ttl_ms=5_000)` 沒有設定 `scope`,所以維持 `"private"`:每個呼叫端各自享有五秒的新鮮期。範圍和 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` 的處理函式,可以在其他部分都是 public 的伺服器上,針對某一個 URI 回傳 `cache_scope="private"`。
分頁清單有一點要注意:協定要求同一份清單的**每一頁都要有相同的 `cacheScope`**。建構子的對應表天生就滿足這一點,因為它以方法為鍵,而不是以頁為鍵。但自行覆寫範圍的處理函式,就得自己負責這份一致性:要在**每一**頁都覆寫,絕不能只在有 cursor 時才覆寫,否則第一頁和第二頁會對不上。
## 用戶端看到什麼 {#what-the-client-sees}
在 2026-07-28 的工作階段(session)上,`Client` 會替你遵守這些提示:它內建一個回應快取,預設開啟。帶著 `ttlMs` 抵達的結果會被存起來,在 TTL 內完全相同的呼叫會直接由快取提供,不需要往返。**沒有**帶提示的結果不會被快取:沒有提示的結果會套用 `CacheConfig.default_ttl_ms`,它預設為 `0`(立刻過期),所以什麼都沒宣告的伺服器,看到的流量和以往一模一樣,一次呼叫就一次請求。
```python title="client.py" hl_lines="33 35 38"
--8<-- "docs_src/caching/tutorial003.py"
```
四次呼叫,三次抓取。第二次呼叫找到新鮮的項目,根本沒送到伺服器;把(注入的)時鐘撥過 TTL 之後,第三次又重新抓取;第四次則指定了 `cache_mode="refresh"`。這個關鍵字引數存在於五個會快取的動詞上(`list_tools`、`list_prompts`、`list_resources`、`list_resource_templates`、`read_resource`):
* `"use"`(預設)如果有新鮮的項目就直接提供,沒有的話就抓取並存起來。
* `"refresh"` 從不由快取提供:它會抓取並儲存結果,取代原本快取的內容。
* `"bypass"` 直接往返,完全不碰快取:不讀、也不寫。
有一條規則凌駕於 `"use"` 之上:**帶有 `meta` 的呼叫一定會送到伺服器。**設定了 `meta` 的請求(進度 token、追蹤欄位)期待的是一個實際送上線路的請求,所以在 `cache_mode="use"` 下會被當成 `"refresh"` 處理:跳過快取讀取,而抓取回來的結果仍然會取代快取中的項目。`"bypass"` 和明確指定的 `"refresh"` 行為照舊。
要完全關掉快取,就用 `Client(server, cache=None)` 建構:每次呼叫又都變回一次往返,而 `cache_mode` 雖然仍可接受,但不會有任何作用。
範圍也會自動遵守:`"private"` 項目綁定在快取的**分區(partition)**上(見下文),而 `"public"` 項目則可以選擇更廣的共用。此外,對通知點名的那些項目來說,**通知勝過 TTL**:`list_changed` 通知會逐出對應的快取清單,`resources/updated` 則會逐出恰好存在該 URI 下的快取讀取結果,不管它們有多新鮮。在 2026-07-28 連線上,這些通知是透過你用 `client.listen(...)` 開啟的 `subscriptions/listen` 串流送達的,而且逐出會在你的監看程式看到事件之前完成;詳情請見 **[訂閱](subscriptions.md)**。
`resources/updated` 有一點要注意:逐出只比對完全相同的 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` 匯入。一次查詢最多可能對存放區連續發出兩次 `get`(先查 private 分支,再查 public 分支),所以遠端存放區的延遲預期要據此估算。自訂存放區**必須**搭配明確的 `partition`。
* `partition`:授權上下文的標籤,用來避免在共用存放區中把某個主體的 `"private"` 項目提供給另一個主體。
* `target_id`:明確的伺服器身分,用於自訂傳輸和同處理程序內的伺服器(見下文)。
* `default_ttl_ms`:套用在沒有帶 `ttlMs` 提示的結果上的 TTL。預設的 `0` 讓沒有提示的結果不被快取。
* `share_public`:跨分區提供伺服器宣稱為 `"public"` 的項目(見下文)。預設關閉。
* `clock`:牆上時鐘的來源,以 epoch 秒為單位。像上面的範例那樣注入一個,過期測試就不需要 sleep。
!!! warning "分區 = 經過驗證的主體"
`partition` 要從**經過驗證的憑證**推導出來,例如已驗證權杖的 subject。絕不要從請求提供的資料推導,也絕不要從伺服器 URL 推導(伺服器身分是另一條獨立的鍵軸)。SDK 是一個函式庫,本身沒有任何身分驗證:信任的錨點是建構 `CacheConfig` 的人,也就是部署方,而不是租戶。多租戶閘道要為每個已驗證的主體各建立一個 `CacheConfig`。
分區在 `Client` 的整個存活期間也是固定的。如果連線的授權上下文在工作階段中途改變(例如重新驗證成另一個主體),快取不會跟著變;請為新的主體建構一個新的 `Client`。
快取鍵也帶有**伺服器的身分**:你連線的 URL 字串,去掉任何 `user:pass@` 使用者資訊,其餘逐位元組保留。不做大小寫摺疊、不重排查詢參數、不清理結尾斜線。正規化不足只會損失共用的機會,過度正規化卻可能把兩個租戶合併在一起(`?tenant=a` 對 `?tenant=b`),所以表面上不同的 URL 就是不共用項目。沒有 URL 的時候(同處理程序內的伺服器,或 `Transport` 實例),用戶端會改拿到一個每個實例隨機產生的身分;設定 `CacheConfig.target_id` 來替伺服器命名(使用自訂存放區時這是必要的,建構時也會這麼告訴你)。身分在進入鍵的材料之前會先經過 sha256 雜湊,所以查詢字串裡帶著機密的 URL 永遠不會出現在存放區的鍵中。你自己也不要把雜湊前的形式記錄下來。
!!! warning "`share_public` 代表信任伺服器,而且是整個機群一起信任"
預設情況下,即使是 `"public"` 項目也會留在自己的分區內。`share_public=True` 會把伺服器標成 `cacheScope: "public"` 的項目提供給使用該存放區的**每一個**分區,等於代替它們全體信任伺服器的分類。如果伺服器(因為 bug 或惡意)把 `"public"` 蓋在各租戶專屬的資料上,某個租戶的回應就會洩漏給其他租戶。這個旗標刻意只放在建構子層級:每次呼叫的 `cache_mode` 可以縮小快取範圍,但沒有任何每次呼叫層級的東西可以擴大共用。
### 快取絕不會做的事 {#what-the-cache-never-does}
* **工作階段層級的呼叫會繞過它。** `client.session.list_tools()` 這一類的呼叫一定會往返;快取是掛在 `Client` 的動詞上。
* **`server/discover` 不參與。** discover 結果只在連線時送達一次,永遠不會進入回應快取,即使它帶著 `ttlMs` 也一樣。如果你自己把它保存下來以跳過重新連線時的探測([`prior_discover`](../protocol-versions.md#reconnecting-with-prior_discover)),它的新鮮度就由你自己記帳:`DiscoverResult` 帶有已解析好的 `ttl_ms` 和 `cache_scope`,正是為了這個用途。
* **後續分頁永遠不會被快取。** 只有不帶 cursor 的呼叫會參與。因為 cursor 過期而被拒絕的後續分頁倒是會**逐出**快取的清單,因為清單已經在底下變了。
* **多輪往返(multi-round-trip)的讀取永遠不會被快取。** 以 `input_responses`/`request_state` 起頭的 `read_resource`,或是經過輸入回合才解析出結果的讀取,都永遠不會進入快取(這是規範的 MUST)。
* **靠通知逐出,就得有通知。** 逐出的效果取決於傳輸能不能把通知送到,而現代的同處理程序內路徑(`Client(server)` 搭配預設的 `mode="auto"`)目前不會遞送獨立的通知。
* **逐出是最終發生,不是立即發生。** 走線路的通知是從衍生出來的 task 分派的,所以和通知抵達搶時間的呼叫,可能會再被提供一次逐出前的項目;這個空窗受分派延遲所限,而逐出終究會生效。
* **沒有 stale-if-error。** 過期的項目絕不會因為重新抓取失敗就被拿出來提供;錯誤會往上傳遞。
* **沒有提前重新抓取。** 已存的項目會一直提供到 TTL 過期為止,過期後的下一次呼叫要付出往返的代價;背景不會有任何東西在更新。
* **沒有合併。** 兩個同時發出的相同呼叫就是兩次抓取。
* **TTL 不會超過 24 小時。** 更大的 `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}
* 六個方法帶有 `ttlMs`/`cacheScope`;SDK 把它們預設為 `0`/`"private"`,過期且不共用,永遠安全。
* 建構時的 `cache_hints={method: CacheHint(...)}`(`MCPServer` 和 `Server` 都有)會為每個方法設定全伺服器的值。
* 在結果上設定這些欄位的處理函式會逐欄位覆寫對應表。
* `"public"` 是一個承諾:結果對每個呼叫端都完全相同。它不是存取控制。
* `Client` 會自動遵守提示:它的回應快取預設開啟,會提供新鮮的項目而不重新抓取,而對沒有提供提示的伺服器(或工作階段)則什麼都不快取。
* 每次呼叫可用 `cache_mode="refresh"` 重新抓取、用 `"bypass"` 跳過快取;建構時傳入 `cache=None` 則會完全關掉它。

View file

@ -0,0 +1,142 @@
---
translation:
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
tool: 1
---
# 用戶端回呼 {#client-callbacks}
MCP 裡幾乎每一個請求都是單向的:從用戶端到伺服器。
伺服器也可以反過來向**用戶端**要東西:向使用者提問、對使用者的模型取樣(sampling)、列出使用者的工作區資料夾。要回應這些請求,就把**回呼**傳給 `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` 是兩種徵詢模式的聯集。這裡的 `params.mode` 是 `"form"`;`"url"` 請求帶的是 `params.url` 而不是 schema。同一個回呼處理兩種模式,依 `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`,伺服器回送一個 `elicitation/create`,由你的函式回答,全都發生在同一次工具呼叫之內。
!!! info
`Client(...)` 呼叫上的 `mode="legacy"` 是真的有作用。預設情況下 `Client(...)` 會協商出現代的協定路徑,而這條路徑沒有讓伺服器向用戶端發請求的反向通道(back-channel):`ctx.elicit` 在你的回呼有機會執行之前就失敗了。決定這件事的不是傳輸方式,而是協商出來的協定,記憶體內和透過 URL 連線都一樣。只要用戶端必須回應這類請求,就固定用 `mode="legacy"`;這一頁背後的每個測試都是這樣做的。完整說明請見 **[協定版本](../protocol-versions.md)**。
在 2026-07-28 的工作階段(session)上,回呼並沒有失效,只是餵給它的方式不同:當工具回傳帶有 `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}` |
| 一個都不傳 | `{}` |
取樣的子能力是唯一需要細分的地方:如果你的取樣器會處理 `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']}
```
三個回呼都傳,會得到 `['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` 值得有的原因:行為良好的伺服器會先檢查再發問。
## 已棄用的那一對 {#the-deprecated-pair}
`sampling_callback` 回應 `sampling/createMessage`:伺服器請**你的**模型生成一些內容。`list_roots_callback` 回應 `roots/list`:伺服器詢問它可以在哪些目錄裡工作。
兩個都能用。兩個都遵守上面的規則。而兩個服務的 RPC 都是 **2026-07-28 規格移除的**:現代的伺服器不會在請求途中回頭呼叫你的用戶端,而是把請求當成工具結果的一部分交還給你(**[多輪往返請求](../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(...)` 來拒絕。
把它們傳給 `Client(...)` 的方式和 `elicitation_callback` 完全一樣。
## 通知回呼 {#the-notification-callbacks}
還有兩個。兩個都不宣告任何東西。
`logging_callback` 會收到伺服器送出的 `notifications/message`,型別是 `LoggingMessageNotificationParams`(`level`、`logger`、`data`)。協定記錄本身已被 2026-07-28 規格棄用(該怎麼改做請見 **[記錄](../handlers/logging.md)**),所以這個回呼是為了還在送出它的伺服器而存在。在 2026 世代的連線上,光有回呼什麼都收不到,因為 2026 的伺服器只會把記錄訊息送給主動選擇接收的請求:把 `log_level="info"`(或其他層級)傳給 `Client(...)`,就會在每個請求上蓋上這個選擇,並收到該層級以上的訊息。2026 之前的伺服器會忽略它,維持原本的 `logging/setLevel` 行為。
`message_handler` 是總攬一切的那個:工作階段浮現的每一個伺服器通知都會送到它(同時也送到各自專屬的回呼),在以串流為基礎的傳輸方式上,每一個傳輸層級的 `Exception` 也會。有兩種永遠不會:`notifications/cancelled` 由 SDK 直接套用而不浮現,而正在運作的 `listen()` 串流的訂閱確認則由那個串流自己消化。把這個參數註記為 `IncomingMessage`(`ServerNotification | Exception`,從 `mcp.client` 匯出)。唯一值得知道的寫法是 `if isinstance(message, Exception): raise message`,這樣連線斷掉時會大聲失敗,而不是悄悄消失。
## 重點回顧 {#recap}
* 伺服器可以向用戶端送出請求。用傳給 `Client(...)` 的回呼來回應它們。
* 徵詢回呼是現行的那一個:`async (context, params) -> ElicitResult`,一個函式同時處理 form 和 URL 模式。
* **註冊回呼就是宣告能力。**沒有它,SDK 會替你拒絕伺服器的請求,整個呼叫以 `MCPError` 失敗。
* 伺服器在發問之前用 `ctx.session.check_client_capability(...)` 先確認。
* `sampling_callback` 和 `list_roots_callback` 的運作方式相同,但服務的是已棄用的功能;現代的伺服器改用多輪往返請求。
* `logging_callback` 和 `message_handler` 接收通知。它們不宣告任何東西。
`Client(...)` 的第一個引數是一個傳輸物件。**[用戶端傳輸方式](transports.md)** 涵蓋了每一種。

View file

@ -0,0 +1,129 @@
---
translation:
sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0]
tool: 1
---
# 身分斷言 {#identity-assertion}
一般的 OAuth provider(**[OAuth 用戶端](oauth-clients.md)**)一開始會先問 MCP 伺服器一個問題:「你信任哪一個授權伺服器?」答案指向哪裡它就跟到哪裡,接著要嘛有人登入,要嘛用預先共享的密鑰代替。
企業兩者都不想交給每台伺服器各自決定。它早就有一個身分提供者(Okta、Microsoft Entra ID,或你自己的);使用者今天早上就已經登入過了;而且安全團隊希望只在這一個地方決定誰能存取什麼。[SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990),也就是 **Enterprise-Managed Authorization** 擴充功能,把決定權移到那裡。IdP 會簽署一個短效的 JWT,**Identity Assertion JWT Authorization Grant**,簡稱 **ID-JAG**:宣告**這位使用者**透過**這個用戶端**可以存取**這台 MCP 伺服器**。用戶端拿它換一個普通的存取權杖。沒有瀏覽器、沒有同意畫面、沒有動態註冊。
這一頁講的是這筆交換的兩端。MCP 伺服器本身完全不變:它仍然是 **[授權](../run/authorization.md)** 裡的資源伺服器,檢查送上門的任何權杖。
## 兩個權杖請求 {#two-token-requests}
這裡有兩個不同的權威機構在運作,把它們分開命名,幾乎就等於讀懂這一頁。**企業 IdP** 是你所屬組織的身分提供者:它知道員工是誰,政策放在它那裡,ID-JAG 也由它簽發。SDK 從不跟它對話。**MCP 授權伺服器**和 **[授權](../run/authorization.md)** 裡是同一個角色:MCP 伺服器中繼資料裡指名的簽發者,負責簽發該 MCP 伺服器接受的權杖。在一般的 OAuth 流程裡,這兩個角色通常是同一台機器。這裡它們是兩台,而整個授權流程就是後者同意信任前者。
用戶端對兩者各發一個權杖請求。
1. **對企業 IdP。** 用戶端拿使用者的登入結果(他們的 OpenID Connect ID 權杖)換 ID-JAG。這是一次 [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) 權杖交換,完全是你 IdP 的 API,而且 **SDK 不會發這個請求**。由你在一個非同步回呼裡完成。政策決定也發生在這裡:IdP 如果拒絕,就根本不會簽發 ID-JAG,也就沒有東西可以出示。
2. **對 MCP 授權伺服器。** 用戶端以 [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) 的 `jwt-bearer` 授權類型出示 ID-JAG(`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`,ID-JAG 放在 `assertion`),然後收到存取權杖。**這是 SDK 會發的請求**,而接受它,就是這一頁替授權伺服器加上的唯一一件事。
以下全部都是第二個請求:送出它的用戶端,以及回應它的授權伺服器。
## 用戶端 {#the-client}
**`IdentityAssertionOAuthProvider`** 位於 `mcp.client.auth.extensions.identity_assertion`。和 **[OAuth 用戶端](oauth-clients.md)** 裡的每個 provider 一樣,它是一個 `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)**),一行都沒改。重點正是這個:一旦 provider 存在,下游沒有任何東西知道權杖是哪一種授權類型產生的。
* 這個 provider 接收其他 provider 無法自行探索到的東西:有人事先向授權伺服器**預先註冊**好的 `client_id` 和 `client_secret`、該授權伺服器的 `issuer`,以及 `assertion_provider`,一個依需求回傳全新 ID-JAG 的非同步回呼。
* `storage` 是同一個 `TokenStorage` 協定。只會呼叫那兩個權杖方法;這裡沒有動態註冊,所以沒有 `client_info` 需要記住。
### 斷言提供者 {#the-assertion-provider}
`fetch_id_jag(audience, resource)` 是你唯一要寫的程式碼。每次權杖交換會 await 它一次,建立時從不呼叫,而且只在授權伺服器的中繼資料已取回並驗證**之後**才呼叫,所以設定錯誤的 issuer 永遠不會洩漏斷言。它的兩個引數是簽發 ID-JAG 時必須帶有的其中兩個 claim:`audience` 是授權伺服器的簽發者(ID-JAG 的 `aud`),`resource` 是 MCP 伺服器的正規識別碼(ID-JAG 的 `resource`)。第三個你手上已經有了:ID-JAG 的 `client_id` claim 必須指名你交給 provider 的那個 `client_id`,否則授權伺服器會拒絕交換。
上面的 `idp_issue_id_jag` **不是你的程式碼**。它代替身分提供者,在處理程序內簽署斷言,讓這個檔案能完整執行,你也能讀到 ID-JAG 帶有的每一個 claim。真正的 `fetch_id_jag` 會改發上一節的第一個權杖請求:對你的 IdP 做一次 [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) 權杖交換,定義在 Identity Assertion JWT Authorization Grant 草案裡,[SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) 則是該草案的 profile。已登入使用者的 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,而這正是重點:它是單次使用、只活幾分鐘的授權,而且這一頁的授權伺服器拒絕接受同一個兩次。不要快取它。會被重複使用的,是它幫你換來的存取權杖。
### issuer 是設定值 {#the-issuer-is-configuration}
反轉的地方在這裡。`OAuthClientProvider` 會問資源伺服器該用哪一個授權伺服器,答案指向哪裡就跟到哪裡。這個 provider 拒絕這麼做:`issuer` 是必填,[RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) 中繼資料從該 issuer 自己的 well-known 路徑取回,權杖端點必須位於該 issuer 的 origin 上,而且從不向資源伺服器詢問任何事。
擴充功能並沒有要求這樣做;這是刻意更嚴格的選擇。這個用戶端帶著兩樣值得偷的東西,一個預先註冊的密鑰和一個綁定 audience 的斷言,而如果用戶端任由遭入侵的 MCP 伺服器把它導向攻擊者的授權伺服器,這兩樣都會 POST 過去。在建立時就釘死 issuer,等於把這段對話整個刪掉。
!!! warning
設定的 `issuer` 會依 RFC 8414 §3.3 的簡單字串比對,和中繼資料文件的 `issuer` 欄位比較:逐字元比對,結尾斜線也算,沒有任何正規化。不要用猜的。從你的授權伺服器抓 `/.well-known/oauth-authorization-server`,把它回傳的 `issuer` 值複製過來。以這一頁的授權伺服器來說是 `https://auth.example.com/`,帶斜線,因為它的 issuer 是從 pydantic 的 URL 物件建出來的。不相符的話,流程會在送出任何一個憑證或斷言之前,就停在 `OAuthFlowError: Authorization server metadata issuer
mismatch`。
### 機密用戶端 {#a-confidential-client}
`client_secret` 是必填;沒有它,建構子會引發 `ValueError`。[SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) 底下的 IETF profile 把這種授權類型保留給機密用戶端,SEP-990 要求用戶端必須驗證身分,而這個 SDK 以堅持要有共享密鑰的方式同時強制這兩點。`token_endpoint_auth_method` 決定它走哪裡:`client_secret_post`(預設,放在表單主體)或 `client_secret_basic`(HTTP Basic 標頭)。profile 也允許 `private_key_jwt`;這個 provider 不支援。
!!! tip
從環境變數或密鑰管理服務讀取 `client_secret`,永遠不要從版本控制裡讀。
### provider 替你做的事 {#what-the-provider-does-for-you}
第一個請求不帶驗證就送出,伺服器的 `401` 啟動整個流程。
1. **探索。** 從設定的 issuer 的 [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) well-known 路徑取回授權伺服器中繼資料,檢查文件的 `issuer` 相符,並檢查權杖端點位於 issuer 的 origin 上。
2. **斷言。** await 你的 `assertion_provider`。
3. **交換。** 把 `jwt-bearer` 授權 POST 到權杖端點,儲存 `OAuthToken`,然後帶著 `Authorization: Bearer ...` 重送你原本的請求。
`WWW-Authenticate` 指名 `insufficient_scope` 的 `403`,會用你的 `scope` 和被質疑的 scope 的聯集,再跑一次步驟 2 和 3。(`scope` 永遠只是請求;這一頁的授權伺服器只核發 ID-JAG 上寫的,其他一概不給。)整個過程裡沒有任何更新權杖:存取權杖過期時,下一個 `401` 會簽發一個全新的 ID-JAG 再交換一次,而**那**正是 IdP 握在手上的控制桿。失敗和 **[OAuth 用戶端](oauth-clients.md)** 其他部分一樣是那兩個例外:探索和驗證用 `OAuthFlowError`,權杖端點拒絕時則是它的子類別 `OAuthTokenError`。
## 授權伺服器 {#the-authorization-server}
大多數時候你到這裡就停了。MCP 授權伺服器是別人的產品,接受 ID-JAG 是它要開啟的設定,而 SDK 在 [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) 裡負責的那一半,就是上面的用戶端。
SDK 也可以自己**當**授權伺服器:`create_auth_routes` 以任何 Starlette 應用程式都能掛載的清單形式回傳授權伺服器的路由,儲存庫裡的 `examples/servers/simple-auth/` 就是這樣跑起一個的。SEP-990 在這個介面上加了一個旗標和一個方法:
```python title="auth_server.py" hl_lines="48-50 105-107"
--8<-- "docs_src/identity_assertion/tutorial002.py"
```
* `identity_assertion_enabled=True` 管控一切。關閉時(這是預設),即使你實作了 hook,`/token` 也會以 `unsupported_grant_type` 回應這種授權類型,中繼資料也不會提到它。開啟時,中繼資料會多出 `jwt-bearer` 授權類型,並在 `authorization_grant_profiles_supported` 裡列出 `urn:ietf:params:oauth:grant-profile:id-jag`,也就是擴充功能用來宣傳支援的欄位。(這個 SDK 的用戶端從不讀它:它只為一個 issuer 佈建,直接開口問就是了。)
* **`exchange_identity_assertion`** 就是那個 hook。在它執行之前,SDK 已經驗證了用戶端、拒絕了公開用戶端,也拒絕了註冊資料裡沒列出這種授權類型的用戶端。你會拿到一個 `IdentityAssertionParams`(原始的 `assertion`、請求的 `scopes` 和 `resource`),回傳一個普通的 `OAuthToken`。
* 動態用戶端註冊無條件拒絕這種授權類型,所以這裡的 `get_client` 提供的是手動佈建的用戶端。ID-JAG 用戶端沒辦法靠自己註冊而存在。
* 這個類別有一半是拒絕。`OAuthAuthorizationServerProvider` 是**整個**授權伺服器,所以它也要求授權碼流程;同時讓使用者登入的伺服器會真的實作那些,而這一台只有一扇門。
!!! 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`,這是 profile 防止其他 JWT 被拿來重播成授權的防線。要求 `aud` 是你自己的 issuer。要求 ID-JAG 的 `client_id` claim 等於處理函式驗證過的用戶端,且它的 `resource` claim 指名的是你確實有提供的資源。追蹤 `jti` 直到斷言的 `exp`,讓它只被接受一次。還有,核發的 scope,以及最重要的、簽發權杖的 `resource`,都要從驗證過的 ID-JAG 取得,永遠不要從請求取得:`params.resource` 是用戶端隨便打的東西。完整的處理規則在 [Enterprise-Managed Authorization 規格](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization)裡。
用 `TokenError("invalid_grant", ...)` 拒絕不合格的斷言。這個流程裡另一個錯誤碼是 `invalid_target`:指名了你沒提供的資源的 ID-JAG 會用它拒絕,這正是阻止這台伺服器替別人的資源簽發權杖的機制。而核發的 scope 來自 ID-JAG 的 `scope` claim(沒有這個 claim 的斷言也會被拒絕);你的實作也許會改成對應使用者的群組。
再注意回傳的 `OAuthToken` 沒有帶的東西:更新權杖。IdP 透過決定是否簽發下一個 ID-JAG,來決定這位使用者能保有存取權多久。在這裡簽發更新權杖,等於悄悄把這個決定權交回去。
!!! info
仍以 `auth_server_provider=` 內嵌授權伺服器的伺服器,透過 `AuthSettings(identity_assertion_enabled=True)` 走到同一段程式碼。**[授權](../run/authorization.md)** 說明了為什麼新的伺服器不該從那裡開始。
!!! check
把這一頁的兩個檔案接在一起,整個授權流程就是一個 `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 中繼資料。線路上僅有的請求是引來 `401` 的那一個、well-known 的抓取、這次交換,然後就是帶著 bearer 的普通 MCP 流量。而你的驗證器從 ID-JAG 讀出的 `sub`,正是工具裡 `get_access_token().subject` 回報的值。
### 試試看 {#try-it}
SDK 儲存庫裡的 `examples/stories/identity_assertion/` 就是這一頁的實際執行版:同一個 `exchange_identity_assertion` 驗證器、一台以它的權杖把關的 MCP 伺服器、一個替身 IdP,和用戶端,全放在一個會自我檢查的程式裡。`uv run python -m stories.identity_assertion.client --http` 會跑完整個交換,並斷言 IdP 指名的使用者就是工具看到的使用者。
## 重點回顧 {#recap}
* [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) 讓企業身分提供者(而不是終端使用者)決定用戶端可以存取哪些 MCP 伺服器。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`:一個預先註冊的機密用戶端、一個釘死的 `issuer`,和一個 `assertion_provider(audience, resource)` 回呼。沒有瀏覽器、沒有註冊、沒有更新權杖。
* 授權伺服器從不透過資源伺服器探索。把 `issuer` 設定成和它中繼資料文件提供的字串一模一樣;比對是逐字元的。
* 伺服器端是 `identity_assertion_enabled=True` 加上 `exchange_identity_assertion`。SDK 驗證用戶端並管控授權類型;驗證 ID-JAG 完全是你的事,而簽發的權杖綁定的是 ID-JAG 的 `resource`,不是請求的。
這一頁唯一沒碰過的角色是 MCP 伺服器。它拿你剛簽發的權杖做什麼,早在 **[授權](../run/authorization.md)** 裡就已經在做了。

View file

@ -0,0 +1,207 @@
---
translation:
sections: [ebef1e7a0df854f4, a4c687d3d627d516, 8e79141fc2985342, b345dd05b9c3c7ab, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
tool: 1
---
# 用戶端 {#the-client}
Python 程式要和 MCP 伺服器對話,靠的就是 **`Client`**。
它是一個物件,只有一套生命週期:建立它、進入 `async with`、呼叫方法。每個協定動作(列出工具、呼叫其中一個、讀取資源、算繪提示詞)都是它上面的一個 `async` 方法,回傳有型別的結果。
## 你的第一個用戶端 {#your-first-client}
```python title="client.py" hl_lines="14-18"
--8<-- "docs_src/client/tutorial001.py"
```
最上面的伺服器只是讓你有東西可以連而已。用戶端就是標示出來的那五行。
* `Client(mcp)` 拿到的是**伺服器物件本身**。這就是記憶體內傳輸:沒有子處理程序、沒有連接埠、沒有 HTTP。這一頁的每個範例,以及你寫的每個測試,都是這樣連線的。
* `async with` 就是**生命週期**。進入時連線並協商;離開時斷線。沒有 `connect()` / `close()` 這種成對的方法,而且區塊結束後 `Client` 不能再重複使用。
* 在區塊內,連線的各項資訊已經以普通屬性的形式準備好了。
### 可以傳什麼給 `Client` {#what-you-can-pass-to-client}
`Client` 接受一個位置引數,並依它的型別決定傳輸方式:
* `MCPServer`(或低階的 `Server`)實例:在**同一個處理程序內**連線。
* URL 字串(`Client("http://localhost:8000/mcp")`):Streamable HTTP,也就是正式環境的路徑。
* 一個**傳輸**:任何可以 `async with ... as (read, write)` 的東西,例如包住子處理程序的 `stdio_client(...)`。
這一頁其餘的內容在這三種情況下完全相同。標頭、子處理程序、逾時,以及 `Transport` 協定另外有專屬的頁面:**[用戶端傳輸方式](transports.md)**。
### 連線後的用戶端上有什麼 {#whats-on-a-connected-client}
四個唯讀屬性,一進入區塊就填好了:
* `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` 會先探測伺服器,遇到較舊的伺服器就退回傳統的交握,所以同一個用戶端可以對應任何世代的伺服器。需要自己掌控時,完整說明請見 **[協定版本](../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` 裡。每一個都是 MCP 主機(host)會交給模型的完整定義:
```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"
}
```
這份 schema 就是 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
```
一個回傳值,三樣東西可讀。各自有不同的使用對象。
### `content`:給模型讀的 {#content-what-the-model-reads}
`content` 是一個**內容區塊**的 `list`,而內容區塊是一個聯集:`TextContent`、`ImageContent`、`AudioContent`、`ResourceLink` 或 `EmbeddedResource`。一個工具可以回傳好幾個,而且種類各異。
這就是為什麼 `main` 在碰 `block.text` 之前,先用 `isinstance(block, TextContent)` 縮窄型別。注意 `isinstance` 之外完全沒有出現 `.text`:型別檢查器不會放行,因為 `ImageContent` 有的是 `.data`,不是 `.text`。這個聯集誠實地表達了工具可以送什麼給你;你的程式碼也應該如此。
### `structured_content`:給應用程式讀的 {#structured_content-what-your-application-reads}
`structured_content` 是工具回傳值的 JSON 形式,符合工具宣告的 `output_schema`。不用剖析字串,不用猜。
兩者同時存在時,是刻意把同一件事講兩遍:`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`。只有在伺服器回的是 JSON-RPC **錯誤**而不是結果時,`Client` 的方法才會引發 `MCPError`;伺服器什麼時候產生哪一種,請見 **[處理錯誤](../servers/handling-errors.md)**。
## 資源 {#resources}
資源的動作是成組的:兩種列出的方式,一種讀取的方式。
```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}']`。它們分成兩個清單,因為範本在填好之前是不能讀的。
* `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` 會一頁回傳全部,所以 `next_cursor` 是 `None`,迴圈只跑一次,這也是為什麼大部分程式碼從來不寫它。真正會分頁的伺服器,以及游標遵守的規則,請見 **[分頁](../advanced/pagination.md)**。
## 在測試中 {#in-tests}
不需要處理程序、不需要連接埠的 `Client(mcp)`,本身就已經是伺服器的測試工具了。
有一個建構子旗標是專為此設計的:`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)**。

View file

@ -0,0 +1,143 @@
---
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)**。
## Provider {#the-provider}
```python title="client.py" hl_lines="44-54"
--8<-- "docs_src/oauth_clients/tutorial001.py"
```
要給它四樣東西:
* `server_url`:要連線的 MCP 端點。其餘的一切 provider 都會從這裡自行探索出來。
* `client_metadata`:就是你會在授權伺服器的「註冊應用程式」表單裡填的內容。
* `storage`:權杖在兩次執行之間存放的地方。
* `redirect_handler` 和 `callback_handler`:需要人介入的兩個時刻。
檔案裡其他地方都沒有提到 OAuth。`main()` 從頭到尾都看不到權杖。
### 用戶端中繼資料 {#client-metadata}
`OAuthClientMetadata` 就是貨真價實的 [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) 註冊文件,以 Pydantic 模型的形式呈現。
你設定三個欄位,其餘由預設值補上:`grant_types` 已經是 `["authorization_code", "refresh_token"]`,`response_types` 已經是 `["code"]`,正好就是這個 provider 執行的流程。
!!! check
因為它是 Pydantic 模型,所以**在任何一個位元組送上網路之前**就會先驗證。漏掉 `redirect_uris`,建構當場就會失敗,引發一個指名該欄位的 `ValidationError`:
```text
redirect_uris
Field required [type=missing, input_value={'client_name': 'Bookshop Agent'}, input_type=dict]
```
不會開啟瀏覽器,也不會在授權伺服器上留下做到一半的註冊。
### 權杖儲存 {#token-storage}
**`TokenStorage`** 是一個有四個非同步方法的 `Protocol`。不需要繼承任何東西;把這些方法寫出來,任何類別都能當權杖儲存庫:
* `get_tokens` / `set_tokens` 保存 `OAuthToken`:存取權杖、重新整理權杖、到期時間、範圍。
* `get_client_info` / `set_client_info` 保存授權伺服器在 provider 幫你註冊時發給你的 `OAuthClientInformationFull`,其中包含你的 `client_id`。
上面那個存在記憶體內的版本可以用。但處理程序結束時它就什麼都忘了,所以下次執行又得整套流程重來一遍。把它持久化到檔案或平台的鑰匙圈裡,下次執行就會安安靜靜。
!!! tip
要存 `client_info`,不要只存權杖。provider 第一次找不到已儲存的 `client_info` 時會動態註冊。把它丟掉,每次執行就會產生一筆全新的註冊。
### 兩個處理函式 {#the-two-handlers}
授權碼流程只需要人介入一次:得有人登入並按下「允許」。
* **`redirect_handler`** 會帶著組裝完整的授權 URL 被 await。`client_id`、`redirect_uri`、`state` 和 PKCE challenge 都已經在裡面。你唯一的工作是讓瀏覽器開到那裡。桌面應用程式會呼叫 `webbrowser.open`;這個檔案則把它印出來。
* **`callback_handler`** 接著被 await。它會等到使用者回到你的 `redirect_uri`,再把那次重新導向的查詢參數以 `AuthorizationCodeResult` 回傳。
真正的用戶端會在重新導向 URI 上跑一個小型本機 HTTP 伺服器,而不是呼叫 `input()`。形狀完全一樣:接收重新導向,交回 `code`、`state` 和 `iss`。
!!! warning
`state` 和 `iss` 要原封不動地傳回去。provider 會拿 `state` 與自己產生的那個比對,拿 `iss` 與探索到的 issuer 比對,不一致就拒絕。它們分別是 CSRF 與伺服器混淆攻擊的防線。
### 放進 `Client` {#into-the-client}
看看 `main()`。provider 掛在 **httpx2 用戶端**上,httpx2 用戶端放進 `streamable_http_client(url, http_client=...)`,那個傳輸再放進 `Client`。
`streamable_http_client` 沒有 `auth=` 關鍵字引數。凡是 HTTP 層級的東西(驗證、標頭、逾時、代理)都屬於你自備的 `httpx2.AsyncClient`。這種分層的說明請見 **[用戶端傳輸方式](transports.md)**。
## Provider 幫你做的事 {#what-the-provider-does-for-you}
`Client` 第一次送出請求時,伺服器回應 `401`。provider 接手:
1. **探索。** 讀取 `WWW-Authenticate` 標頭,從 `/.well-known/oauth-protected-resource` 抓取伺服器的 Protected Resource Metadata,得知是哪個授權伺服器在保護這個資源,再去抓取**那個**伺服器的中繼資料。
2. **註冊。** 儲存庫裡什麼都沒有?它會用你的 `OAuthClientMetadata` 動態註冊,並把結果存起來。
3. **授權。** 產生 PKCE 配對和一個 `state`,組出授權 URL,await 你的 `redirect_handler`,接著 await 你的 `callback_handler` 取得授權碼。
4. **交換。** 拿授權碼換得 `OAuthToken`,存起來,然後帶著 `Authorization: Bearer ...` 重送你原本的請求。
之後它就很安靜。權杖從儲存庫拿出來用,過期的存取權杖用重新整理權杖更新,只有這些都行不通時才會重跑整個流程。
這些你一行都沒寫。還剩兩個關鍵字引數(`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 有那兩個指令:啟動伺服器、對著它們執行用戶端,就能看著上面四個步驟依序發生。
## Client ID Metadata Documents {#client-id-metadata-documents}
規格的 2026-07-28 修訂版已棄用動態用戶端註冊,改用 **Client ID Metadata Documents**(CIMD)。用戶端不再對遇到的每個授權伺服器 POST 一筆新的註冊,而是在一個穩定的 HTTPS URL 上發布一份描述自己的 JSON 文件,而那個 URL **就是**它的 `client_id`。文件由授權伺服器去抓取;provider 完全不碰它。
SDK 已經支援:建構 provider 時把那個 URL 以 `client_metadata_url=` 傳入即可。當授權伺服器的中繼資料宣告 `client_id_metadata_document_supported: true` 時,provider 會完全跳過 `/register` 請求:URL 以 `client_id` 的身分進入流程,而且沒有 `client_secret`。當伺服器沒有宣告(目前多數都還沒有),或者你根本沒傳 URL,provider 會**默默地**退回動態註冊,上面的一切照原樣運作。已儲存的 `client_info` 仍然優先於這兩者。
URL 必須是 HTTPS 且路徑不能是根路徑;否則在建構時就會引發 `ValueError`,不會發生任何網路動作。隨附的 `examples/clients/simple-auth-client/` 透過 `MCP_CLIENT_METADATA_URL` 環境變數接收它。
## 機器對機器 {#machine-to-machine}
夜間排程、CI 步驟、另一個服務。沒有瀏覽器,也沒有人可以按「允許」。這就是 **client credentials** 授權類型:你手上已經有 `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`;provider 會圍繞它們建出一筆最精簡的 `client_credentials` 註冊,並完全跳過動態註冊。
* `scope` 是以空格分隔的字串,也就是 OAuth 的線路格式。
* 下游的一切完全相同:同樣的 `TokenStorage`、同樣的 `httpx2.AsyncClient(auth=...)`、同樣的 `streamable_http_client`。
預設情況下,secret 在權杖請求中以 HTTP Basic 驗證傳送(`client_secret_basic`)。傳入 `token_endpoint_auth_method="client_secret_post"` 可改放進表單主體。有些授權伺服器只接受兩者其中之一。
!!! tip
`client_secret` 要從環境變數或祕密管理工具讀取,絕對不要放進版本控制。
!!! info
`mcp.client.auth.extensions.client_credentials` 裡還有一個 provider:**`PrivateKeyJWTOAuthProvider`**,給用 JWT 而非共用 secret 來驗證的用戶端使用(`private_key_jwt`,也就是金鑰對與工作負載身分那一類)。它遵循同樣的模式:建構一個,放到 `auth=` 上。同一個模組還附了 `SignedJWTParameters` 和 `static_assertion_provider`,兩個用來建出其 assertion 的輔助工具。
還有一種無人介入的情境:用戶端屬於某個企業,由企業的身分提供者(而非使用者)決定它可以連到哪些 MCP 伺服器。那是另一種授權類型,有自己的信任模型,也有自己的頁面:**[身分斷言](identity-assertion.md)**。
## 失敗的時候 {#when-it-fails}
OAuth 流程出錯時,provider 會引發來自 `mcp.client.auth` 的 `OAuthFlowError`。它有兩個子類別。`OAuthRegistrationError` 表示註冊沒有產生可用的用戶端:授權伺服器拒絕替你註冊,或者有註冊,但給的憑證是這個流程用不了的(例如它沒有實作的驗證方法)。`OAuthTokenError` 表示無法取得權杖:權杖端點拒絕了,或者已儲存的用戶端紀錄帶著這個用戶端無法套用的驗證方法——這會在組裝權杖請求時就回報,而不是送出之後。一個 `except OAuthFlowError:` 就能涵蓋探索、註冊、授權與交換。
不是所有問題都是流程錯誤。網路還是可能出錯;那些是一般的 `httpx2` 例外,會原封不動地往外傳遞。
## 重點回顧 {#recap}
* `OAuthClientProvider` 是一個 `httpx2.Auth`。放到 `httpx2.AsyncClient` 上,再把它傳給 `streamable_http_client(url, http_client=...)`,`Client` 永遠不會知道發生過 OAuth。
* 你提供四樣東西:伺服器 URL、一個 `OAuthClientMetadata`、一個 `TokenStorage`,以及 redirect/callback 這一對處理函式。
* `TokenStorage` 是一個 `Protocol`:四個非同步方法,沒有基底類別。除了權杖,也要持久化 `client_info`。
* 探索、註冊(動態的,或透過 **Client ID Metadata Document**)、PKCE、`state` 與 `iss` 檢查,以及權杖重新整理,都是 provider 的工作,不是你的。
* `ClientCredentialsOAuthProvider` 是無人介入的版本:`client_id` + `client_secret`,沒有處理函式,沒有瀏覽器。
* 每一種 OAuth 失敗都是 `OAuthFlowError`;`OAuthRegistrationError` 和 `OAuthTokenError` 是它的子類別。
這次交握的另一半,也就是讓你的**伺服器**要求權杖,請見 **[授權](../run/authorization.md)**。

View file

@ -0,0 +1,85 @@
---
translation:
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73]
tool: 1
---
# 工作階段群組 {#session-groups}
一個 `Client` 只連到一台伺服器。實際的應用程式往往需要好幾台(搜尋伺服器、資料庫伺服器、內部 API),結果得替每一台各自管理一條連線和一份工具清單。
**`ClientSessionGroup`** 是單一物件,裡面握有多條連線,並把它們公開的所有東西合併成一個統一的檢視。
## 兩台伺服器 {#two-servers}
先從兩台普通的伺服器開始。它們彼此毫無關係,所以很自然地都把自己的工具取名為 `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"
```
## 一個群組 {#one-group}
建立一個 `ClientSessionGroup`,然後對每台伺服器各呼叫一次 **`connect_to_server`**:
```python title="client.py" hl_lines="10-12"
--8<-- "docs_src/session_groups/tutorial003.py"
```
* `connect_to_server` 接受的是傳輸參數,不是伺服器物件:用 `StdioServerParameters`(來自 `mcp`)啟動子處理程序,或用 `StreamableHttpParameters` / `SseServerParameters`(來自 `mcp.client.session_group`)連到已經在某個 URL 上監聽的伺服器。
* `group.tools` 是一個 `dict[str, Tool]`,收集所有已連線伺服器的工具。`group.resources` 和 `group.prompts` 的結構相同。
* `group.call_tool(name, arguments)` 會查詢名稱、找出擁有它的工作階段(session),再把呼叫轉送過去。你永遠不需要指明是哪台伺服器。
!!! check
把 `client.py` 放在兩台伺服器旁邊執行。第二次 `connect_to_server` 會拒絕:
```text
mcp.shared.exceptions.MCPError: {'search'} already exist in group tools.
```
這是一個 `MCPError`,在第二台伺服器的任何東西被登記之前就引發了。名稱在**整個**群組內必須唯一,而兩台你無法掌控的伺服器遲早會撞名。
## `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
這個 hook 會對**每台**伺服器的**每個**名稱執行,不只在衝突時才執行:沒有所謂「撞名才加前綴」的模式。選定一套命名規則,讓它套用到所有地方。
## 新增與移除伺服器 {#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` 建立在 `ClientSession` 之上,而不是 `Client`。每次 `connect_to_server` 都會執行傳統的 `initialize` 交握,從不送出 **[協定版本](../protocol-versions.md)** 裡描述的 `server/discover` 探測。每台 MCP 伺服器都懂這套交握,所以這不會讓你犧牲任何相容性;只是代表群組面對一台本來能做得更好的伺服器時,走的是比較舊、比較慢的路徑。
## 重點回顧 {#recap}
* `ClientSessionGroup` 握有多條伺服器連線,並把它們的工具、資源和提示詞各自合併成一個 `dict`。
* 每台伺服器呼叫一次 `connect_to_server(params)`。它接受傳輸參數,絕不是 `Client` 接受的伺服器物件或 URL。
* `group.call_tool(name, arguments)` 會替你轉送到擁有該工具的伺服器。
* 名稱在整個群組內必須唯一;兩台都有 `search` 工具的伺服器無法原樣共存。
* `component_name_hook=` 會改寫每個登記的名稱。dict 的鍵會變,線路上的名稱不變。
* `connect_with_session` 加入你已經握有的工作階段;`disconnect_from_server` 移除一個。
群組使用的交握(以及 `Client` 偏好的那套更快的交握)是 **[協定版本](../protocol-versions.md)** 的主題。

View file

@ -0,0 +1,88 @@
---
translation:
sections: [8f9558e57f29eee1, a88c587739e0465c, 46ebfd5b325ed041, 4d10b00b57ce4bd9, 2cdb0edd1f59b3e2]
tool: 1
---
# 訂閱 {#subscriptions}
伺服器的目錄不是固定的。工具會在執行時出現,資源 URI 背後的內容也會改變。用戶端透過 `client.listen(...)` 得知這些變化:一個 `subscriptions/listen` 請求,它的回應**就是**串流。這條串流會一直開著,承載用戶端所要求的變更通知。
這一頁講的是用戶端這一端:開啟串流、在主流程旁邊監看它,以及處理它的結束。發布變更、篩選和提供這個方法,則是伺服器那一邊的事,寫在「在處理函式內部」底下的 **[訂閱](../handlers/subscriptions.md)**。這裡的範例對接的是在那一頁建立的衝刺看板(sprint-board)伺服器。
## 監看串流 {#watching-the-stream}
一個訂閱就是一個上下文管理器。進入它會送出請求,把你的關鍵字引數當作訂閱的篩選條件,並等待伺服器的確認,所以區塊開始時串流已經是活的了。
```python title="client.py" hl_lines="15 18 28"
--8<-- "docs_src/subscriptions/tutorial003.py"
```
迭代會產生四種有型別的事件:`ToolsListChanged`、`PromptsListChanged`、`ResourcesListChanged` 和 `ResourceUpdated(uri=...)`。
事件只說**什麼**變了,從不說**怎麼**變的。這就是 `follow_board` 會呼叫 `read_resource` 和 `list_tools` 的原因:事件是重新擷取的信號。讀 `event.uri`,不要自己假設是哪個資源變動了:篩選條件可以列出好幾個 URI,伺服器也可能回報其中某個 URI 的子資源有變更。
等著被取用的重複事件會合併成一個,而重新擷取仍然能拿到目前的狀態。只有完全相同的事件才會合併:兩個 URI 不同的 `ResourceUpdated` 是兩個事件。
這個訂閱物件還有兩個屬性:
* `sub.honored` 是伺服器確認的篩選條件:一個 `SubscriptionFilter`,帶有你傳入的欄位,以屬性的方式讀取(`sub.honored.prompts_list_changed`)。`MCPServer` 會接受你要求的每一種,所以它會把你的請求原樣回傳。支援較少種類的伺服器確認的也較少,而且被接受的種類仍可能永遠不會觸發。伺服器也可能拒絕整個請求而不是確認它(見伺服器那一頁的[決定誰可以監看](../handlers/subscriptions.md#deciding-who-may-watch)),這會以該請求的錯誤呈現。
* `sub.subscription_id` 是 listen 請求的 id,也就是蓋在這條串流每個訊框上的那個 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`,這個 repo 把它存成 `tutorial003.py`。如果你把產生出來的檔案並排存成 `client.py` 和 `app.py`,就改寫成 `from client import BOARD, read_board`。更下面的 `watch.py` 範例也用同樣的方式匯入 `read_board`。
重點在於順序。沒有任何東西會重播,所以在你的串流存在之前發布的事件就錯過了。進入 `client.listen(...)` 會等待確認,所以從那一刻起的每個變更都會送到監看器,而你在區塊內取得的快照不會漏掉任何一個。
串流開著的時候,請求可以自由地在旁邊執行,不管來自監看器任務還是其他任務,都在同一個用戶端上。因為**重複**的未取用事件會合併,忙碌的主流程可能只產生一次重新擷取,而不是三次。不同的事件不會合併:列出許多 URI 的篩選條件會為每個 URI 各排一個待處理事件。
要停止監看,離開區塊就好:沒有 `unsubscribe` 呼叫。取消擁有該區塊的任務就會幫你做到這件事,SDK 會依傳輸方式預期的方法取消 listen 請求:在 Streamable HTTP 上,就是關閉該請求的串流。在應用程式整個存活期間執行的監看器永遠不會自己結束,所以在關閉時取消它,或取消它所屬任務群組的範圍。
## 串流會結束 {#streams-end}
串流的結束方式有兩種,兩種都是一般的控制流程。伺服器優雅地關閉會結束 `async for`;突然中斷則會引發 `SubscriptionLost`。
兩者的差別在於診斷,而不在於接下來該做什麼:串流沒了,沒有任何東西會重播,還在意的監看器就重新 listen 並重新擷取。
```python title="watch.py" hl_lines="16 20"
--8<-- "docs_src/subscriptions/tutorial005.py"
```
伺服器會因為自己的理由優雅地關閉串流,包括甩掉積壓太多的訂閱者,所以乾淨的結束並不是該停止監看的信號。重新 listen 之前先退避一下。
`SubscriptionLost` 也有一個本地端的成因。用戶端最多保留 1024 個未取用的事件,落後到這種程度的取用端會失去訂閱,而不是無限制地膨脹。讓 `async for` 的本體保持簡短,慢的工作放到別處做。
`keep_following` 只攔截 `SubscriptionLost`。進入 `listen()` 也可能引發 `MCPError`(連線失敗,或伺服器不提供這個方法)、`TimeoutError`(沒有收到確認)和 `ListenNotSupportedError`(2026 之前的連線)。決定其中哪些是監看器該重試的:最後一種永遠不會自己好。
## 重點回顧 {#recap}
* 進入 `async with client.listen(...)`;進入時會等待確認,所以之後發布的東西都不會漏掉。
* 用 `async for event in sub` 迭代。事件是重新擷取的信號,從來不是承載內容。
* 先開啟訂閱,再把監看器當成任務執行,工具呼叫就能在旁邊持續進行。
* 乾淨的結束會讓迴圈停下;中斷則引發 `SubscriptionLost`。不管哪一種:重新 listen、重新擷取,但先退避。
* 離開區塊就是取消訂閱。
發布這些事件、縮小篩選條件,以及擴展到超過一個處理程序,是伺服器那一邊的事:**[訂閱](../handlers/subscriptions.md)**。同樣這些事件也能讓用戶端快取保持正確,而 **[快取](caching.md)** 就是下一頁。

View file

@ -0,0 +1,114 @@
---
translation:
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, ff7401df479af877, 3d0832f39b0d7059, d4bf7e4479637768, 05e20c0a798860e7]
tool: 1
---
# 用戶端傳輸方式 {#client-transports}
每個 `Client` 都透過一種**傳輸**(transport)和伺服器溝通:也就是實際承載訊息的那個東西。
你從來不需要單獨設定它。`Client` 只接受一個位置引數,並依據它的型別判斷要用哪一種傳輸方式。
每種傳輸方式的**伺服器**端(`mcp.run()` 做的事,以及你部署的東西)請見 **[執行伺服器](../run/index.md)**。
## 記憶體內 {#in-memory}
直接傳入伺服器物件本身:
```python title="client.py" hl_lines="14"
--8<-- "docs_src/client_transports/tutorial001.py"
```
沒有子處理程序,沒有連接埠,線路上也沒有任何位元組。用戶端和伺服器是同一個處理程序裡的兩個物件,但呼叫仍然會經過真正的協定層:`search_books` 被列出、驗證、呼叫的方式,和透過 HTTP 時完全一樣。
所以它同時是兩樣東西:
* **測試工具。** 這份說明文件裡的每個範例都是這樣跑過的,而 **[測試](../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、proxy、mTLS,或不同的逾時,就自己建立 `httpx2.AsyncClient`,再交給 `streamable_http_client`:
```python title="client.py" hl_lines="8-14"
--8<-- "docs_src/client_transports/tutorial003.py"
```
有兩件事要注意:
* `httpx2.AsyncClient` 是你的,所以由**你**負責進入和離開它。SDK 永遠不會關閉不是它自己建立的用戶端。
* `streamable_http_client(url, http_client=...)` 回傳的是一個傳輸,而 `Client(transport)` 和接受其他東西一樣接受它。
關於 TLS 有一點要提:`httpx2` 是對照作業系統的信任存放區驗證憑證(透過 [`truststore`](https://pypi.org/project/truststore/)),而不是內建的 CA 清單。在沒有可用系統 CA 存放區的環境(某些精簡容器)裡,請設定標準的 `SSL_CERT_FILE`/`SSL_CERT_DIR` 環境變數,或明確傳入 `verify=ssl_context` 給你的 `httpx2.AsyncClient`(背景說明請見 [`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 有關的設定,現在都放在你傳入的那一個 `httpx2.AsyncClient` 上。
!!! info
`httpx2` 保留了熟悉的 `httpx` API,所以只要會用 `httpx`,就已經知道這裡的驗證、proxy、事件掛鉤、重試和連線數限制該怎麼做。SDK 沒有在上面加任何東西,也沒有拿掉任何東西。OAuth 也是從這裡接上的:`httpx2.AsyncClient(auth=OAuthClientProvider(...))`。整個流程請見 **[OAuth 用戶端](oauth-clients.md)**。
## stdio {#stdio}
**stdio** 伺服器是一個子處理程序。用戶端啟動它,把 JSON-RPC 寫進它的 stdin,再從它的 stdout 讀取 JSON-RPC。桌面版 MCP 主機(host)就是這樣在你的機器上執行伺服器的:主機**就是**這段程式碼加上一個 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)` 訊息串流的非同步 context manager:正式地說,就是 `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 連線,也就是正式環境用的傳輸方式。
* 標頭、驗證、proxy 和逾時都放在你傳給 `streamable_http_client(url, http_client=...)` 的 `httpx2.AsyncClient` 上。沒有 `headers=` 這個關鍵字引數。
* stdio 是 `Client(stdio_client(StdioServerParameters(...)))`,絕對不是單獨的參數物件。
* 子處理程序拿到的是允許清單上的環境,不是你的環境;`env=` 會往上加。
* 傳輸就是任何可以 `async with x as (read, write)` 的東西。只要不是伺服器物件或 URL,`Client` 就會直接交給那個協定處理。
* 建立 `Client` 是選定傳輸方式。`async with` 才是開啟它。
傳輸開啟之後,兩邊得對協定版本達成一致。平常根本不需要去想這件事;真的需要的時候,請看 **[協定版本](../protocol-versions.md)**。

View file

@ -0,0 +1,86 @@
---
translation:
sections: [20541a40dbdd5980, 01262a123ad9501d, 429db5b574a2ac08, 56b2d49da412cb28, 6a1717123fe4513c]
tool: 1
---
# 已棄用的功能 {#deprecated-features}
2026-07-28 規格讓五樣東西退場。SDK 仍然實作了其中每一項,而每一項現在都帶有**棄用警告**。
下表列出每一項已棄用的功能、它為什麼要退場,以及應該改用的替代做法。
## 哪些已棄用 {#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)**)。 |
| **伺服器發起的取樣(sampling)**:`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 讓這項能力退場。協定內沒有任何東西取代它。 | 用一般的 `import logging` 輸出到 stderr(請見 **[記錄](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)**)。 |
從這張表可以看出三件事:
* 根目錄、取樣和記錄是一起的。同一份提案 **SEP-2577** 一次棄用了這三項能力。
* 取樣和根目錄有個更深層的共同問題:它們都是**伺服器**向**用戶端**送出**請求**的地方。2026-07-28 用 **[多輪往返請求](handlers/multi-round-trip.md)** 取代的正是這整個方向。消失的是那些獨立的 RPC 方法(`sampling/createMessage`、`roots/list`,以及推送式的 `elicitation/create`);`CreateMessageRequest`/`ListRootsRequest`/`ElicitRequest` 這些酬載型別則保留下來,嵌在 `InputRequiredResult.input_requests` 裡,在用戶端會觸發同樣的回呼。
* `ping` 是特例。協定不是棄用它,而是移除它。SDK 的方法仍然會發出警告(訊息寫的是 *removed*,不是 *deprecated*),在現代連線上呼叫它,得到的回應是 *"Method not found"*。
## 棄用只是勸告性質 {#deprecated-is-advisory}
今天什麼都不會壞。
上面每個方法,在任何協商到 **2025-11-25 或更早版本**的工作階段(session)上都能繼續運作。在用戶端固定 `mode="legacy"`,就能得到和 2026 之前完全一樣的行為。線路上沒有任何變更,能力協商也維持不變。
改變的是,每個方法第一次執行時,你會看到一則明顯的警告:
```text
MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
```
`MCPDeprecationWarning` 繼承自 `UserWarning`,**不是** `DeprecationWarning`。這是刻意的:Python 的預設過濾器只會在直接以 `__main__` 執行的程式碼裡顯示 `DeprecationWarning`,這就是為什麼函式庫棄用了某樣東西,卻兩年都沒人注意到。這個警告到處都會出現,不需要 `-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.
```
兩個訊號,依這個順序出現。`MCPDeprecationWarning` 在呼叫方法的那一刻就會發出,任何連線都一樣。錯誤則是 SDK 接著嘗試傳送時回傳來的東西。這兩者只有在用戶端註冊了對應回呼的 `mode="legacy"` 連線上,才能從頭到尾正常運作。
## 讓警告靜音 {#silencing-the-warning}
新程式碼裡,不要這麼做。
但如果你維護的伺服器確實在服務 2026 之前的用戶端,它完全有權保持記錄乾淨。在第一個已棄用的呼叫執行之前,先過濾掉這個類別:
```python
import warnings
from mcp import MCPDeprecationWarning
warnings.filterwarnings("ignore", category=MCPDeprecationWarning)
```
整個 API 就這樣。沒有逐方法的開關,你也不會想要:只用一個類別的意義在於,一行就能關掉它,一行就能把它叫回來。
!!! check
把過濾器反過來用,就免費得到一個回歸測試。在 pytest 設定的 `filterwarnings` 裡加上 `"error::mcp.MCPDeprecationWarning"`,已棄用的呼叫就會**引發例外**而不是發出警告。一個名為 `old_log`、還在呼叫 `ctx.info()` 的工具會不再通過,開始回報:
```text
Error executing tool old_log: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
```
一行 pytest 設定,已棄用的呼叫就再也沒辦法在不讓測試失敗的情況下溜回程式碼庫。
## 重點回顧 {#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`,所以預設就會顯示)。
* 取樣和根目錄還額外需要一條反向通道(back-channel),而 2026-07-28 的工作階段沒有。在現代連線上,它們會先警告,再引發例外。
* `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` 會讓整個類別靜音;pytest 裡的 `"error::mcp.MCPDeprecationWarning"` 則把它變成測試失敗。
* 新程式碼不應該建立在這些東西之上。
這份說明文件的其他每一頁教的都是目前的 API。

View file

@ -0,0 +1,139 @@
---
translation:
sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686]
tool: 1
---
# 第一步 {#first-steps}
**[首頁](../index.md)** 的節奏很快:寫一個伺服器、執行它、呼叫一個工具。
這一頁放慢腳步,把伺服器能公開的三種東西都走過一遍,沿途每樣東西都給個名字。
## 主機、用戶端與伺服器 {#host-client-and-server}
接下來每一頁都會看到這三個詞:
* **主機**(host)是 LLM 應用程式:Claude、IDE、代理執行環境。使用者對話的就是它。
* **用戶端**位於主機內部,負責講 MCP。主機每連上一個伺服器,就執行一個用戶端。
* **伺服器**是你用這個 SDK 打造的東西。它把東西公開給用戶端,從不直接和模型溝通。
伺服器由你來寫,主機是別人的產品。SDK 也提供一個 `Client`,用來測試你的伺服器,這一頁稍後就會出現。
## 三種基本元件 {#the-three-primitives}
伺服器公開的東西正好只有三種。區分它們的關鍵是**誰決定要用**:
| 基本元件 | 由誰控制 | 是什麼 | 範例 |
|---------------|-----------------|-----------------------------------------------------|------------------------------------|
| **工具** | 模型 | 模型呼叫來執行動作的函式 | API 呼叫、寫入資料庫 |
| **資源** | 應用程式 | 主機載入到模型上下文的資料 | 檔案內容、API 回應 |
| **提示詞** | 使用者 | 使用者依名稱叫用的可重複使用訊息範本 | 斜線指令、選單項目 |
「由誰控制」正是這樣拆分的重點所在。工具會執行,是因為**模型**決定呼叫它。資源會被附上,是因為**應用程式**判斷模型需要它。提示詞會執行,是因為**使用者**選了它。
!!! info
如果做過 web API,大部分直覺你已經有了:**資源**是 `GET`(載入資料,不改變任何東西),**工具**是 `POST`(做事,可能有副作用)。**提示詞**沒有 HTTP 的對應物,比較像使用者依名稱執行的已儲存查詢。
## 一個伺服器,三種齊備 {#one-server-all-three}
```python title="server.py" hl_lines="6 12 18"
--8<-- "docs_src/first_steps/tutorial001.py"
```
三個普通函式,三個裝飾器。每個裝飾器就是完整的註冊動作:
* `@mcp.tool()` 讓 `add` 成為**工具**。
* `@mcp.resource("greeting://{name}")` 讓 `greeting` 成為**資源範本**:URI 裡的 `{name}` 就是函式的參數。
* `@mcp.prompt()` 讓 `summarize` 成為**提示詞**。它回傳的字串會變成一則使用者訊息。
其他一切(名稱、描述、引數 schema)SDK 都從函式本身讀取:函式名稱、docstring、型別提示。這些你從來沒有另外宣告過。
!!! tip
SDK 的兩半各有自己的匯入路徑:`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 每種基本元件各有一個分頁,依序走過一遍。
**Tools。**一個項目:`add`,描述為 *Add two numbers.*。表單有一個必填的整數欄位 `a`,另一個是 `b`。填好、呼叫,結果是 `3`。Inspector 是從 `a: int, b: int` 建出那張表單的,其他每個用戶端也一樣。
**Resources。**這裡的 *Resources* 清單是空的。`greeting` 在 **Resource Templates** 底下,因為 `greeting://{name}` 帶有參數:在有人提供 `name` 之前,沒有單一資源可以列出。給它 `World` 然後讀取:
```text
Hello, World!
```
**Prompts。**一個項目:`summarize`,只有一個必填的 `text` 引數。帶一段文字去取得它,會收到一則 `role: user` 的訊息,內容就是你組好的字串。提示詞就只是這樣:一個組出訊息的函式。
Inspector 透過 **stdio** 執行你的伺服器,這是 MCP 伺服器能使用的傳輸方式之一。現在還不用選;那是 **[執行伺服器](../run/index.md)** 那一頁的事。
## 能力 {#capabilities}
在 Inspector 裡看到了三個分頁。它怎麼知道有三個?
用戶端連線時,伺服器會宣告自己的**能力**:它會回應哪幾類請求。用戶端據此決定究竟該要求什麼。這份宣告不是你寫的,`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` 三種基本元件都提供,所以三者永遠都會宣告。
注意少了什麼。`completions`(資源範本和提示詞的引數自動完成)需要你寫一個處理函式,這個伺服器沒有,所以這項能力不存在,守規矩的用戶端也不會去問。所有選用的東西都照這條規則:註冊了,能力就出現;**[自動完成](../servers/completions.md)** 會證明這一點。
!!! info
`Client(mcp)` 就是這份文件裡每個範例測試時用的同一個記憶體內用戶端,你也會用它來測試自己的。它有專屬的一整頁:**[測試](testing.md)**。
## 你沒寫的東西 {#what-you-did-not-write}
回頭看這一頁。你寫了三個小小的 Python 函式。你**沒有**寫:
* JSON Schema。`a: int, b: int` **就是** `add` 的 schema。
* 請求處理函式。`tools/list`、`resources/read`、`prompts/get`:全都替你處理好了。
* 能力宣告。`MCPServer` 替你做了。
* 任何一行協定。版本協商、JSON-RPC 訊框、能力交換:全都發生在 `mcp dev` 和 `Client(mcp)` 裡面,你完全沒看到。
這個比例正是 SDK 的意義所在。
## 重點回顧 {#recap}
* **主機**是 LLM 應用程式,**用戶端**是它講 MCP 的那一半,**伺服器**是你打造的東西。
* 工具由**模型**控制,資源由**應用程式**控制,提示詞由**使用者**控制。
* 每種基本元件一個裝飾器:`@mcp.tool()`、`@mcp.resource(uri)`、`@mcp.prompt()`。名稱、描述和 schema 都來自函式。
* 帶 `{param}` 的 URI 會產生資源**範本**,和具體資源分開列出。
* 伺服器的**能力**會替你宣告好,而用戶端只會要求伺服器宣告過的東西。
* `Client(mcp)` 在記憶體內連上伺服器物件:從第一天起就是你的測試工具。
接下來是 **[連接真正的主機](real-host.md)**:把這個伺服器真的放進 Claude Desktop 或 IDE 裡。然後是 **[測試](testing.md)**:一頁、一個記憶體內用戶端,從此不用猜它到底能不能動。再之後,每種基本元件各有自己的一頁,從模型主導的那個開始:**[工具](../servers/tools.md)**。

View file

@ -0,0 +1,53 @@
---
translation:
sections: [ed4a756b4c53c585, 97e2fb315b7fe398, 4d04f1c6f4bf6c1d, 577d73078fc62baf]
tool: 1
---
# 開始使用 {#get-started}
剛接觸 MCP,或剛接觸這個 SDK?從這裡開始。這幾頁會帶你從零開始,做出一個能運作、經過測試的伺服器:[安裝 SDK](installation.md)、建立[第一個伺服器](first-steps.md)、[把它接上真正的 MCP 主機(host)](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)**。

Some files were not shown because too many files have changed in this diff Show more