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