ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FastMCP ClientGroup 详解:多服务器客户端编排、工具命名空间与独立协议协商

FastMCP ClientGroup 详解:多服务器客户端编排、工具命名空间与独立协议协商 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),仅供参考
返回列表