戰(zhàn):用 `Resolve` 讓工具參數(shù)脫離模型幻覺)
MCP Python SDK 依賴注入實(shí)戰(zhàn)用Resolve讓工具參數(shù)脫離模型幻覺【免費(fèi)下載鏈接】python-sdkThe official Python SDK for Model Context Protocol servers and clients項(xiàng)目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk在 MCPModel Context Protocol服務(wù)端開發(fā)中工具的入?yún)⒛J(rèn)全部來自模型。但總有那么一類參數(shù)絕不應(yīng)該由模型決定從你業(yè)務(wù)庫存里查出的實(shí)時(shí)價(jià)格、只有真人才能給出的審批確認(rèn)、任何模型靠編造就可能出錯(cuò)的數(shù)值。本教程以當(dāng)前倉庫的官方文檔 docs/handlers/dependencies.md 為骨架結(jié)合 SDK 源碼 src/mcp/server/mcpserver/resolve.py 與配套測試 tests/docs_src/test_dependencies.py系統(tǒng)講解 Python SDK 的依賴注入機(jī)制如何用Annotated[...] Resolve(fn)聲明依賴、如何讓解析器resolver彼此嵌套成依賴圖、如何在必要時(shí)向用戶提問Elicit以及如何向客戶端請求 LLM 采樣Sample與根目錄列表ListRoots。讀完你將掌握一套模型無法偽造的服務(wù)端值注入的完整實(shí)戰(zhàn)方案。什么是依賴注入工具參數(shù)的另一個(gè)來源工具的參數(shù)來自模型這是 MCP 的默認(rèn)模型。docs/handlers/dependencies.md開篇點(diǎn)出核心問題有些值永遠(yuǎn)不該來自模型——從你的記錄中查出的價(jià)格、只有人能給出的確認(rèn)、任何模型靠編造就可能出錯(cuò)的數(shù)值。依賴Dependencies就是由你自己的函數(shù)填充的參數(shù)你為參數(shù)加上類型注解、指明函數(shù)SDK 會(huì)在工具執(zhí)行之前先調(diào)用它把返回值注入?yún)?shù)。這是 FastAPI 用戶非常熟悉的Depends模式在 MCP 世界的對應(yīng)物——函數(shù)聲明自己需要什么框架負(fù)責(zé)提供連接關(guān)系全部寫在類型注解里。從源碼看這一機(jī)制的核心標(biāo)記定義在 src/mcp/server/mcpserver/resolve.py#L102-L106class Resolve: Marker for Annotated[T, Resolve(fn)]: fill the parameter by running fn. def __init__(self, fn: Callable[..., Any]) - None: self.fn fnResolve本身只是一個(gè)攜帶函數(shù)的標(biāo)記真正的工作由 SDK 的解析管線完成注冊時(shí)靜態(tài)分析、調(diào)用時(shí)按依賴圖求值。聲明第一個(gè)依賴Annotated[T, Resolve(fn)]把參數(shù)的類型用Annotated[...]包起來并追加Resolve(fn)即可。完整的可運(yùn)行示例見 docs_src/dependencies/tutorial001.pyfrom typing import Annotated from pydantic import BaseModel from mcp.server import MCPServer from mcp.server.mcpserver import Resolve mcp MCPServer(Bookshop) INVENTORY {Dune: 7, Neuromancer: 0} class Stock(BaseModel): title: str copies: int async def check_stock(title: str) - Stock: return Stock(titletitle, copiesINVENTORY.get(title, 0)) mcp.tool() async def reserve_book(title: str, stock: Annotated[Stock, Resolve(check_stock)]) - str: Reserve a copy of a book. if stock.copies 0: return f{title!r} is out of stock. return fReserved {title!r} ({stock.copies - 1} copies left).這里的關(guān)鍵點(diǎn)check_stock是一個(gè)解析器resolver一個(gè)普通的函數(shù)SDK 在reserve_book之前運(yùn)行它其返回值變成stock參數(shù)。check_stock的title參數(shù)是工具自身的title參數(shù)按名字匹配。解析器看到的正是工具體稍后將會(huì)看到的那個(gè)已驗(yàn)證值——兩者完全一致。工具體從已經(jīng)存在的一個(gè)Stock開始工作工具里沒有查找?guī)齑娴拇a也沒有萬一沒有怎么辦的前置判斷。職責(zé)被干凈地拆給了解析器。值得注意的是解析器既可以是async def也可以是同步函數(shù)源碼 src/mcp/server/mcpserver/resolve.py#L553-L556 顯示同步解析器會(huì)被調(diào)度到線程池anyio.to_thread.run_sync中執(zhí)行異步解析器則直接await。對模型不可見tools/list為reserve_book報(bào)告的輸入 schema 是{ type: object, properties: { title: {title: Title, type: string} }, required: [title], title: reserve_bookArguments }只有一個(gè)屬性。和 Context 對象 一樣被解析的參數(shù)是你與 SDK 之間的契約stock不在 schema 中、模型永遠(yuǎn)不會(huì)被告知它的存在而且即使某個(gè)客戶端強(qiáng)行發(fā)送一個(gè)stock值也會(huì)被忽略。解析器的值是工具唯一能收到的值。最后這一點(diǎn)才是關(guān)鍵——模型無法提供的參數(shù)就是模型不可能搞錯(cuò)的參數(shù)。價(jià)格、身份、權(quán)限這類模型必須不能編造的值就屬于這里。這一點(diǎn)有測試直接背書。tests/docs_src/test_dependencies.py#L41-L46 的用例test_a_client_supplied_value_for_a_resolved_parameter_is_ignored故意向reserve_book傳入{title: Dune, stock: {title: Dune, copies: 999}}結(jié)果工具仍收到解析器算出的真實(shí)庫存6 本偽造的 999 被靜默丟棄。動(dòng)手嘗試用 MCP Inspector 驗(yàn)證用 MCP Inspector 啟動(dòng)服務(wù)器uv run mcp dev server.pyInspector 中reserve_book的表單只有一個(gè)title字段stock無處可見。用Dune調(diào)用它Reserved Dune (6 copies left).工具體什么都沒查check_stock先運(yùn)行返回的Stock作為參數(shù)抵達(dá)。再試Neuromancer同一個(gè)解析器交給工具一個(gè)零庫存對象工具返回缺貨提示。提示你當(dāng)然也可以在工具體內(nèi)直接調(diào)用check_stock(title)。但當(dāng)一個(gè)值值得被多個(gè)工具共享時(shí)就該把它聲明為依賴——每個(gè)需要庫存信息的工具聲明同一個(gè)參數(shù)無論多少個(gè)工具聲明它SDK 每次調(diào)用最多只運(yùn)行該解析器一次。依賴的依賴解析器組成的 DAG解析器可以用同樣的注解聲明自己的依賴。完整示例見 docs_src/dependencies/tutorial002.pyfrom typing import Annotated from pydantic import BaseModel from mcp.server import MCPServer from mcp.server.mcpserver import Resolve mcp MCPServer(Bookshop) INVENTORY {Dune: 7, Neuromancer: 0} class Stock(BaseModel): title: str copies: int async def check_stock(title: str) - Stock: return Stock(titletitle, copiesINVENTORY.get(title, 0)) async def estimate_delivery(stock: Annotated[Stock, Resolve(check_stock)]) - str: return tomorrow if stock.copies 0 else in 2-3 weeks mcp.tool() async def order_book( title: str, stock: Annotated[Stock, Resolve(check_stock)], delivery: Annotated[str, Resolve(estimate_delivery)], ) - str: Order a book from the shop. if stock.copies 0: return f{title!r} is on backorder; it would arrive {delivery}. return fOrdered {title!r}; it arrives {delivery}.estimate_delivery依賴check_stock。SDK 按圖序執(zhí)行先查庫存再估算配送最后運(yùn)行工具。stock和delivery最終都需要check_stock但它每次調(diào)用只運(yùn)行一次。一次庫存查詢兩個(gè)消費(fèi)者。沒有任何注冊表需要維護(hù)。依賴圖就是那些類型注解本身。每次調(diào)用一次不是口頭承諾。源碼中_Resolution持有按解析器身份去重的cachesrc/mcp/server/mcpserver/resolve.py#L456而 tests/docs_src/test_dependencies.py#L61-L81 用帶計(jì)數(shù)的CountingInventory驗(yàn)證一次order_book調(diào)用只觸發(fā)一次INVENTORY.get下一次tools/call會(huì)再次觸發(fā)——記憶化按調(diào)用生效而不是按服務(wù)器進(jìn)程生效。圖在注冊時(shí)分析壞圖直接InvalidSignatureSDK 在工具注冊時(shí)分析依賴圖而不是在工具被調(diào)用時(shí)。一個(gè)無法歸類的參數(shù)——既不是Context不是Resolve(...)也不是某個(gè)工具參數(shù)的名字——以及解析器之間的循環(huán)依賴都會(huì)在啟動(dòng)階段拋出InvalidSignature。服務(wù)器在任何一個(gè)客戶端連接之前就啟動(dòng)失敗報(bào)錯(cuò)信息會(huì)點(diǎn)名出問題的參數(shù)或解析器。這一點(diǎn)在源碼中有完整實(shí)現(xiàn)build_resolver_planssrc/mcp/server/mcpserver/resolve.py#L347-L404遞歸分析每個(gè)解析器的參數(shù)用調(diào)用棧檢測循環(huán)Resolver fn has a cyclic dependency對無法分類的參數(shù)拋出InvalidSignature。此外find_resolved_parameters還會(huì)拒絕把Resolve(...)埋在 union 里的寫法如Annotated[T, Resolve(f)] | None要求直接注解為Annotated[T, Resolve(...)]。解析器的參數(shù)與工具參數(shù)遵循同一套解析規(guī)則另一個(gè)Resolve(...)、按名字匹配的工具參數(shù)、或完整的Contextctx.headers、lifespan 對象全部可用。一個(gè)安全警告ctx.headers是客戶端輸入在 HTTP 傳輸上Context包含ctx.headers。請求頭是客戶端提供的輸入和任何工具參數(shù)一樣適合用來傳遞語言區(qū)域或特性開關(guān)但永遠(yuǎn)不能用來做身份認(rèn)證。調(diào)用者是誰應(yīng)該來自你的授權(quán)層見 授權(quán)而不是任何人都能設(shè)置的請求頭。關(guān)于每次調(diào)用一次的邊界每次調(diào)用一次意味著下一個(gè)tools/call會(huì)重新運(yùn)行check_stock。需要跨請求存活、由服務(wù)器在啟動(dòng)時(shí)構(gòu)建一次的資源——數(shù)據(jù)庫連接池、HTTP 客戶端——屬于 Lifespan 生命周期 的職責(zé)范圍解析器可以通過ctx.request_context.lifespan_context訪問它。只在必要時(shí)詢問用戶Elicit解析器并不非得知道答案。它可以返回Elicit(message, Model)由 SDK 去問用戶——這正是為你代跑的 Elicitation 信息征詢機(jī)制。完整示例見 docs_src/dependencies/tutorial003.pyfrom typing import Annotated from pydantic import BaseModel, Field from mcp.server import MCPServer from mcp.server.mcpserver import Elicit, Resolve mcp MCPServer(Bookshop) INVENTORY {Dune: 7, Neuromancer: 0} class Stock(BaseModel): title: str copies: int class Backorder(BaseModel): confirm: bool Field(descriptionOrder anyway and wait?) async def check_stock(title: str) - Stock: return Stock(titletitle, copiesINVENTORY.get(title, 0)) async def confirm_backorder( title: str, stock: Annotated[Stock, Resolve(check_stock)], ) - Backorder | Elicit[Backorder]: if stock.copies 0: return Backorder(confirmTrue) # in stock: nothing to ask return Elicit(f{title!r} is out of stock (2-3 weeks). Order anyway?, Backorder) mcp.tool() async def order_book( title: str, stock: Annotated[Stock, Resolve(check_stock)], backorder: Annotated[Backorder, Resolve(confirm_backorder)], ) - str: Order a book from the shop. if not backorder.confirm: return No order placed. if stock.copies 0: return fBackordered {title!r}; it ships in 2-3 weeks. return fOrdered {title!r}.三個(gè)關(guān)鍵行為有貨時(shí)confirm_backorder直接返回Backorder。沒有問題、沒有額外往返。用戶只在自己的回答真正重要時(shí)才被打擾。缺貨時(shí)SDK 發(fā)送 elicitation 請求把用戶回答按Backorder模型校驗(yàn)后注入。你的解析器全程不接觸協(xié)議細(xì)節(jié)。工具像讀取其他參數(shù)一樣讀取backorder.confirm?;卮餹o也是一種回答elicitation 以confirmFalse被接受工具繼續(xù)運(yùn)行訂單不生成。詢問成為前置條件而不是工具體里的管道設(shè)施。Elicit在源碼中定義于 src/mcp/server/mcpserver/resolve.py#L109-L118它攜帶message展示給用戶的問題文本和schema回答必須符合的模型。用戶不回答怎么辦如果用戶拒絕或取消問題呢當(dāng)注解寫成Annotated[Backorder, Resolve(...)]未解包形式時(shí)工具體根本不會(huì)運(yùn)行調(diào)用以模型可讀的錯(cuò)誤結(jié)果失敗Error executing tool order_book: Resolver for parameter backorder could not resolve: elicitation was decline這正是前置條件的正確默認(rèn)沒有回答就沒有訂單。源碼中_unwrapsrc/mcp/server/mcpserver/resolve.py#L645-L648在 outcome 不是AcceptedElicitation時(shí)拋出ToolError措辭與文檔完全一致。配套測試 tests/docs_src/test_dependencies.py#L126-L140 在 legacy 與 auto 兩種模式對應(yīng)新舊協(xié)議版本下都驗(yàn)證了這條錯(cuò)誤文本。如果拒絕是工具想要自己處理的結(jié)果——比如跳過缺貨預(yù)訂但仍推薦另一本書——就把注解改為ElicitationResult[Backorder]工具將收到完整的 accept/decline/cancel 三態(tài)結(jié)果自行分支。關(guān)于該形式、schema 規(guī)則、三種回答以及對話的客戶端一側(cè)Elicitation 頁面 有完整說明。協(xié)議版本雙軌多輪tools/call與同步請求框架根據(jù)協(xié)商出的協(xié)議版本選擇問題的傳輸方式上面的代碼在兩個(gè)時(shí)代完全相同2026-07-28 及之后問題搭載在多輪往返multi-round-trip的tools/call中——服務(wù)器返回問題客戶端的elicitation_callback作答Client替你重試調(diào)用見 多輪請求。2025-11-25 及之前調(diào)用中途發(fā)出一條同步的 elicitation 請求。源碼中_uses_input_requiredsrc/mcp/server/mcpserver/resolve.py#L664-L670依據(jù)協(xié)商版本決定走InputRequiredResult批量傳輸還是老式同步回信道。關(guān)于每個(gè)問題每次調(diào)用只問一次有幾點(diǎn)精細(xì)的保證這個(gè)保證是關(guān)于問題的不是關(guān)于解析器的。多輪形式下調(diào)用在問題之后每次恢復(fù)時(shí)任何解析器都可能重新運(yùn)行——因此return Elicit(...)之前的代碼會(huì)在每一輪都執(zhí)行。已記錄的回答會(huì)滿足重復(fù)的問題而不再向用戶追問。已記錄的回答只在解析器真正提問時(shí)才會(huì)被參考像check_stock這樣不問就答的解析器永遠(yuǎn)提供自己算出的值。因?yàn)槊總€(gè)回答都要匹配回它對應(yīng)的問題發(fā)起 elicitation 的解析器必須從工具參數(shù)和之前的回答中確定性地推導(dǎo)問題。每次調(diào)用生成的值一個(gè)default_factory生成的 id、一個(gè)時(shí)間戳?xí)诿恳惠喼匦律山^不能出現(xiàn)在需要綁定回答的問題里。用這類易變數(shù)據(jù)構(gòu)造的問題會(huì)讓每個(gè)已記錄回答都顯得過期于是服務(wù)器每輪都會(huì)重新提問直到客戶端的輪次上限終止調(diào)用。詢問客戶端而不是用戶Sample與ListRootsElicitation 只是解析器能提的三種問題之一而多輪流程不允許其他形式。另外兩種面向的是客戶端而非用戶返回Sample(...)通過客戶端運(yùn)行一次 LLM 調(diào)用一次sampling/createMessage請求返回ListRoots()獲取客戶端當(dāng)前的根目錄roots列表。兩者都沒有 accept/decline 結(jié)果消費(fèi)者直接在類型注解中寫結(jié)果類型CreateMessageResult當(dāng)請求攜帶tools或tool_choice時(shí)用CreateMessageResultWithTools或ListRootsResult。完整示例見 docs_src/dependencies/tutorial004.pyfrom typing import Annotated from mcp.server import MCPServer from mcp.server.mcpserver import Resolve, Sample from mcp.types import CreateMessageResult, SamplingMessage, TextContent mcp MCPServer(Bookshop) def suggest_title(genre: str) - Sample: prompt fSuggest one {genre} book title. Answer with the title only. return Sample( [SamplingMessage(roleuser, contentTextContent(typetext, textprompt))], max_tokens50, ) mcp.tool() async def recommend_book( genre: str, suggestion: Annotated[CreateMessageResult, Resolve(suggest_title)], ) - str: Recommend a book in the given genre. title suggestion.content.text if suggestion.content.type text else the classics return fTodays {genre} pick: {title}行為要點(diǎn)框架像路由Elicit一樣路由這兩種請求2026-07-28版本走多輪tools/call2025-11-25版本走獨(dú)立的服務(wù)器到客戶端請求。如果客戶端沒有聲明相應(yīng)能力調(diào)用會(huì)被-32021協(xié)議錯(cuò)誤拒絕——源碼_require_capabilitysrc/mcp/server/mcpserver/resolve.py#L673-L708會(huì)校驗(yàn)sampling、roots、表單模式的elicitation能力請求攜帶tools或tool_choice時(shí)還需sampling.tools并以MISSING_REQUIRED_CLIENT_CAPABILITY錯(cuò)誤碼拒絕。關(guān)于問題的一切規(guī)則原樣適用Sample請求按其精確渲染匹配已記錄結(jié)果所以要由工具參數(shù)和之前的回答確定性地構(gòu)造它。這樣客戶端為一次工具調(diào)用支付一次 LLM 調(diào)用費(fèi)用而不是每輪一次。已記錄結(jié)果在調(diào)用剩余部分隨request_state攜帶——因此一個(gè)極大的補(bǔ)全結(jié)果會(huì)讓后續(xù)每一輪往返都更重。Sample的構(gòu)造參數(shù)max_tokens、system_prompt、temperature、stop_sequences、metadata、model_preferences、tools、tool_choice等定義在 src/mcp/server/mcpserver/resolve.py#L131-L157并會(huì)校驗(yàn)帶工具使用的消息合法性。獨(dú)立的 sampling 與 roots特性自 2026-07-28 起被棄用SEP-2577。需要客戶端模型的新服務(wù)器應(yīng)通過這一載體提問不需要的應(yīng)直接集成 LLM 提供商。除none之外的include_context值本身也已棄用應(yīng)避免使用??偨Y(jié)工具參數(shù)上的Annotated[T, Resolve(fn)]SDK 運(yùn)行fn并注入其返回值。被解析的參數(shù)對模型不可見客戶端也無法提供它。模型絕不能編造的值——價(jià)格、身份、權(quán)限——都屬于這里。解析器的參數(shù)以同樣方式解析Context、另一個(gè)Resolve(...)、或按名字匹配的工具參數(shù)。無論有多少消費(fèi)者依賴圖每輪最多運(yùn)行每個(gè)解析器一次每個(gè)問題恰好被問一次調(diào)用在問題之后恢復(fù)時(shí)任何解析器都可能再次運(yùn)行。壞圖在注冊時(shí)以InvalidSignature失敗而不是在調(diào)用中途失敗。需要詢問用戶時(shí)且僅在你必須問時(shí)返回Elicit(message, Model)。未解包的注解在拒絕時(shí)中止調(diào)用ElicitationResult[T]則允許工具自行分支。需要向客戶端索要 LLM 補(bǔ)全或根目錄列表時(shí)返回Sample(...)或ListRoots()純結(jié)果被直接注入。服務(wù)器在啟動(dòng)時(shí)一次性構(gòu)建的狀態(tài)、以及處理器如何訪問它是 Lifespan 生命周期 頁面的主題?!久赓M(fèi)下載鏈接】python-sdkThe official Python SDK for Model Context Protocol servers and clients項(xiàng)目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考