
FastMCP ClientGroup 詳解多服務器客戶端編排、工具命名空間與獨立協議協商【免費下載鏈接】fastmcp The fast, Pythonic way to build MCP servers and clients.項目地址: https://gitcode.com/GitHub_Trending/fa/fastmcp導讀ClientGroup是 FastMCP v4.0 引入的客戶端編排原語用于協調多個彼此獨立的 FastMCP 客戶端每個客戶端保留自己獨立的連接、協商出的協議版本與能力工具以{server}_{tool}的命名空間形式統一發布調用則路由回最初發布該工具的那個客戶端。讀完本文你將掌握如何把多個協議時代如legacy與auto的 MCP 服務器喂給同一個 Agent、如何為單臺服務器單獨實現工具前綴命名空間、如何通過resolve_tool()將工具綁定到其所屬連接以及ClientGroup在并發、故障隔離與緩存刷新上的行為邊界。本文以 dev-docs/v4-notes/client-groups.md 這份 v4 設計筆記為主體結合 docs/clients/client-groups.mdx 用戶文檔、group.py 實現與 test_client_group.py 測試展開。背景為什么需要一個介于兩者之間的編排對象在ClientGroup出現之前把來自多個服務器的工具交給同一個 Agent只能在兩種都有明顯缺陷的形態中二選一見 dev-docs/v4-notes/client-groups.md單一Client(config)聚合多服務器配置被組合在一個進程內代理后面所有后端共享同一個協商出的協議時代。一個走握手時代handshake era的后端會把所有現代后端都拉回握手時代重連并發調用則全部經由同一個前端會話串行化。每服務器一個Client每個連接都能保住自己協商出的協議時代但沒有任何對象負責組合——調用方必須手寫命名空間、碰撞檢測、生命周期和路由邏輯。ClientGroup正是填補這兩者之間空白的對象一個名稱 → 客戶端的映射其中每個連接獨立協商協議工具以{server}_{tool}發布調用路由回發布該工具的客戶端。它位于fastmcp.client.group被放在主命名空間下因為其對外表面積是純增量的。設計筆記點明了直接的驅動因素——按服務器釘定協議per-server protocol pinningAgent 集成需要在某臺服務器上用modelegacy在另一臺上用modeauto這是單一聚合連接無法表達的。與此同時命名空間訴求為單臺服務器的工具加前綴也在持續出現因為同一批集成要把多臺服務器的工具喂給同一個模型。測試 test_client_group.py 中的test_clients_negotiate_independently直觀印證了這一點同一組內舊客戶端協商出2025-11-25、新客戶端協商出2026-07-28兩個協議時代并存且各自獨立工作。快速上手從客戶端構建一個 ClientGroup當每臺服務器需要各自的 handler、認證或連接設置時顯式構造客戶端再組成 groupfrom pathlib import Path import asyncio from fastmcp import Client from fastmcp.client.group import ClientGroup legacy_client Client(Path(legacy_server.py), modelegacy) modern_client Client(https://modern.example.com/mcp, modeauto) group ClientGroup( { legacy: legacy_client, modern: modern_client, } ) async def main() - None: async with group: tools await group.list_tools() print([tool.name for tool in tools]) result await group.call_tool( modern_get_weather, {city: Chicago}, ) print(result) asyncio.run(main())工具名默認以配置的客戶端名作為前綴。例如modern客戶端暴露的get_weather工具在 group 層面變成modern_get_weather。單客戶端 group 是合法的命名空間方案只包含一個客戶端的 group會把該服務器的工具統一掛到其配置名稱下行為上沒有任何其他變化。測試 test_single_client_group_provides_namespacing_alone.py 驗證了solo_echo、solo_protocol_era這樣的命名空間產物。懶加載目錄首次路由調用才會懶加載工具目錄cold start。加載成功后未知工具名會在本地失敗而不會對每臺服務器重復做一次發現。當服務器動態增刪工具時顯式調用list_tools()可以刷新路由——這個顯式調用會繞過任何客戶端側響應緩存讓目錄反映每臺服務器現在實際發布的內容。工具命名空間與碰撞檢測的實現命名空間與碰撞檢測不是靠字符串拼接那么簡單。在 group.py 的list_tools()中并行地對每個客戶端調用client.list_tools()通過utilities.async_utils.gather每個工具生成公開名{server_name}_{tool.name}并用tool.model_copy(update{name: public_name})產生重命名后的副本若同一公開名被兩臺服務器同時命中例如服務器名a的工具b_echo與服務器名a_b的工具echo都變成a_b_echo直接拋出ValueError: Tool name collision: a_b_echo同時構建并緩存一張public_name - ToolRoute路由表。ToolRoute是一個 frozen dataclassgroup.py攜帶三個字段server_name組內的服務器名、client發布該工具的客戶端實例、upstream_name服務器自己聲明的原始工具名。碰撞檢測之所以放在組合層而非Client層是因為前綴本質上是碰撞策略而碰撞策略理應屬于負責檢測碰撞的組合對象。resolve_tool()把工具綁定回它的所屬連接工具適配器把 MCP 工具轉成 Agent 框架可調用對象的那層代碼往往需要的不僅僅是路由調用——會話驅動的輸入循環、handler 上下文、攔截器都必須跑在真正擁有該工具的連接上。resolve_tool()返回的就是這條完整路由因此適配器可以經由 group 做發現再把每個生成的工具綁定到其真實客戶端async with group: for tool in await group.list_tools(): route await group.resolve_tool(tool.name) # route.client 是持有該工具的已連接 FastMCP 客戶端 # route.upstream_name 是服務器自己聲明的原始工具名 register_agent_tool(tool, clientroute.client, nameroute.upstream_name)這正是無合成前端No synthetic frontend設計決策的落點group 只負責聚合名稱與檢測碰撞從不站在適配器與客戶端之間。沒有聚合會話、沒有協議翻譯因此單個Client支持的一切認證、tracing、緩存、結果解析、進度、多輪工具對每個工具都繼續有效。resolve_tool()的路由解析語義group.py值得注意已知路由只要求自己的客戶端已連接——一臺死掉的服務器不會把故障耦合到路由向健康服務器的調用上。測試 test_known_route_survives_an_unrelated_dead_client 展示了doomed客戶端斷開后healthy_echo照常可調而doomed_echo拋出RuntimeError。目錄加載仍要求全員在線——首次解析或刷新后要查詢所有客戶端。懶加載在anyio.Lock保護下進行多路并發冷啟動調用共享同一次發現測試 test_concurrent_cold_calls_share_tool_discovery 斷言list_tools每臺服務器只被調用一次未知工具也不會重復觸發發現見 test_unknown_tools_do_not_repeat_discovery。from_config每服務器獨立協商協議的配置入口ClientGroup.from_config為配置中的每臺服務器各創建一個客戶端而不是把整個配置塞進一個代理。FastMCP 特有的mode字段可以為每臺服務器單獨選擇協議行為group.pyfrom fastmcp.client.group import ClientGroup config { mcpServers: { legacy: { command: python, args: [legacy_server.py], mode: legacy, }, modern: { url: https://modern.example.com/mcp, mode: auto, }, } } group ClientGroup.from_config(config)未帶mode的條目默認使用autodefault_mode參數類型為ConnectMode Literal[legacy, auto] | str定義見 client.py。從實現看mode通過server.model_extra讀取——它是 MCP 配置 schema 之外的 FastMCP 擴展字段若值不是字符串from_config會拋出TypeError。每個客戶端以Client(server.to_transport(), modeconfigured_mode)構造mode只作用于該服務器。測試 test_from_config_applies_mode_per_server 驗證了同一配置里legacy與auto各自生效protocol_versions分別為2025-11-25與2026-07-28。連接生命周期上下文管理器、并發進入與引用計數兩種使用方式用 group 作為上下文管理器是可選的。應用可以自行持有每個客戶端連接只把 group 當作發現與路由工具async with legacy_client, modern_client: tools await group.list_tools()也可以在客戶端上下文內部進入 group。FastMCP 客戶端上下文是引用計數的client.py 注釋明確說明首次調用創建后臺會話任務并等待就緒后續調用遞增引用計數并復用既有會話所以退出 group 不會關閉仍由外層上下文持有的連接。測試 test_group_context_does_not_close_caller_owned_client 驗證了這一點。并發進入與部分失敗回滾__aenter__group.py的實現體現了幾條關鍵不變式進入守衛在第一個await之前就先把AsyncExitStack掛到self._exit_stack上讓并發的第二次進入命中RuntimeError(ClientGroup is already connected)守衛而不是競態覆蓋測試 test_concurrent_group_entry_does_not_double_connect。并發連接所有客戶端同時__aenter__因此進入延遲大致保持一次握手深度而不是隨服務器數量線性增長。return_exceptionsTrue保證每個連接嘗試都跑完從而知道哪些成功、可以精確回滾。部分失敗回滾任一客戶端連接失敗時已成功連接的客戶端會被依次__aexit__清理棧被關閉然后拋出第一個錯誤測試 test_partial_connect_failure_unwinds_connected_clients。每服務器一條連接組內成員共享客戶端實例一臺服務器的 lifespan 只進入一次測試 test_group_keeps_one_connection_per_server 斷言entered 1。group 自身不新增任何會話處理。每個客戶端像獨立使用時一樣持有自己的傳輸與會話因此一個 legacy 有狀態會話例如經 SSE 或 stdio只要其客戶端上下文存活就一直保持打開無論進入它的是 group 還是調用方。成員不可變clients屬性返回MappingProxyType只讀映射構造后成員不可變group.py——因為路由表持有發布工具的客戶端引用若映射被替換路由會悄悄失效。測試 test_client_membership_is_immutable 斷言向group.clients賦值會拋TypeError。目錄刷新與緩存語義list_tools()默認cache_moderefresh顯式調用就是 group 的目錄刷新機制客戶端側響應緩存SEP-2549會被重新填充而非命中路由反映的是每臺服務器現在發布的內容。測試 test_explicit_list_tools_refreshes_past_client_response_cache 展示在帶cache_ttl提示的服務器上顯式list_tools()能發現動態新增的hinted_added工具并可立即調用。相對地懶加載的冷啟動發現resolve_tool首次解析會以cache_modeuse執行允許命中緩存——只有顯式list_tools()才承諾刷新后的目錄。若需要容忍服務器提示范圍內的短暫陳舊也可以顯式傳cache_modeuse。設計決策四個關鍵取舍設計筆記用四條決策劃定了ClientGroup的邊界用 Group 而不是 kwarg。一個tool_name_prefixkwarg 曾被完整實現作為對照方案#4932已關閉并最終被拒絕前綴本質上是碰撞策略而碰撞策略應歸屬負責檢測碰撞的組合對象同時它會讓穩定的單服務器客戶端對外暴露服務器從未聲明的名字。group 的字典鍵本身就是命名空間——單客戶端 group 是單獨獲得命名空間的受支持方式正是這一決策的推論。無合成前端。沒有聚合會話、沒有協議翻譯。resolve_tool()返回服務器名、所屬Client與上游工具名因為工具適配器需要把按工具綁定的行為會話驅動的輸入循環、handler、攔截器掛到真實連接上group 從不站在適配器與客戶端之間。不變式優先于靈活性。成員構造后不可變路由持有廣告客戶端突變會使路由失效組進入有競態保護且并發連接部分失敗時回滾已成功的連接已知路由只要求自己的客戶端在線一臺死服務器不會拖垮路由向健康服務器的調用只有目錄發現這種查詢所有人的操作才要求全艦隊在線。當前僅限工具Tool-only。資源resource采用基于 URI 的身份碰撞規則不同提示prompt調用面也不同。擴展到工具之外應遵循具體使用需求而不是按類比提前宣告策略。性能特征設計筆記給出了對兩臺真實本地 streamable-HTTP 服務器實測的結論比值比絕對值更可信路由調用與直連調用不可區分p50 均為 1.89ms而Client(config)代理每次調用額外增加約 65% 開銷。并發下差距被放大跨兩臺服務器 200 個并發調用經由 group 約 145ms經由代理約 875ms快約 6 倍——因為 group 在獨立會話間扇出而代理通過單一會話串行化。組進入延遲客戶端并發連接進入延遲大致保持一次握手深度不隨服務器數量線性增長。需要說明的是這些是設計筆記作者針對特定環境的測量結果用于說明比值這一結論實際數字會隨服務器實現與運行環境變化。與 MCP Python SDK 的 ClientSessionGroup 的關系MCP Python SDK 的ClientSessionGroup是同類先例prior art兩者的分工值得分清用戶文檔 docs/clients/client-groups.mdx 的Related: SDK session groups一節亦有說明SDK 的connect_to_server()擁有生命周期但只講經典握手協議connect_with_session()可以注冊一個外部協商好的現代會話但不擁有它。ClientGroup則把獨立的現代協議協商與組擁有的生命周期結合起來同時把調用保持在 FastMCPClient的完整表面上認證、tracing、緩存、結果解析、進度、多輪工具。使用場景分工直接操作原始 session 時選 SDK 的 group服務器已經以 FastMCP 客戶端形態存在時選ClientGroup。源碼與測試導航設計筆記dev-docs/v4-notes/client-groups.md本文主體狀態為 In review #4904用戶文檔docs/clients/client-groups.mdx核心實現fastmcp_slim/fastmcp/client/group.pyToolRoute在 L22-L28ClientGroup在 L31from_config在 L62-L85list_tools在 L152-L187resolve_tool在 L189-L222call_tool/call_tool_mcp在 L224-L264測試套件tests/client/group/test_client_group.py覆蓋獨立協商、碰撞拒絕、懶加載共享發現、引用計數、并發進入守衛、部分失敗回滾、死服務器故障隔離、緩存刷新等 16 個場景客戶端引用計數與mode參數fastmcp_slim/fastmcp/client/client.py總結ClientGroup用最小的對外表面積解決了多服務器客戶端編排的三個實際問題為不同協議時代的服務器各自保住獨立協商、用{server}_{tool}命名空間消除工具名沖突、把每次調用精確路由回發布它的客戶端及其完整能力面。它在單一聚合連接與裸客戶端集合之間提供了一個既有明確歸屬、又不過度設計的中間形態——無合成會話、成員不可變、按需刷新目錄、已知路由故障隔離這些不變式讓它在 Agent 集成這類多服務器場景下既可預測又可組合。【免費下載鏈接】fastmcp The fast, Pythonic way to build MCP servers and clients.項目地址: https://gitcode.com/GitHub_Trending/fa/fastmcp創作聲明:本文部分內容由AI輔助生成(AIGC),僅供參考