mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-31 11:33:20 +02:00
* feat(client): add experimental independent client groups 🤖 Generated with Pi * refactor(client): narrow client group naming policy 🤖 Generated with Pi * fix(client): avoid repeated discovery for unknown tools 🤖 Generated with Pi * feat(client): expose client group tool routes 🤖 Generated with Pi * docs(client): compare client and session groups 🤖 Generated with Pi * docs(client): clarify modern session group gap 🤖 Generated with OpenAI Codex * refactor(client): align client group call signatures with Client Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrQJXPeMepQgJay5xF91yE * docs(client): cross-link client groups from multi-server config docs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrQJXPeMepQgJay5xF91yE * refactor(client): promote client group out of experimental Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrQJXPeMepQgJay5xF91yE * fix(client): harden client group invariants Immutable membership, race-free group entry, and per-route connection checks so one dead server does not fail calls routed to healthy servers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrQJXPeMepQgJay5xF91yE * test(client): satisfy ty on immutable membership test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrQJXPeMepQgJay5xF91yE * perf(client): connect group clients concurrently Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrQJXPeMepQgJay5xF91yE * fix(client): refresh group catalog past client response caches Explicit group.list_tools() now defaults to cache_mode='refresh' via a new cache_mode parameter on Client.list_tools, so a cache-hinted server cannot leave the documented refresh mechanism serving a stale catalog. Lazy cold-start discovery still allows cache hits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrQJXPeMepQgJay5xF91yE --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
115 lines
5.5 KiB
Text
115 lines
5.5 KiB
Text
---
|
|
title: Client Groups
|
|
sidebarTitle: Client Groups
|
|
icon: layer-group
|
|
---
|
|
|
|
import { VersionBadge } from '/snippets/version-badge.mdx'
|
|
|
|
<VersionBadge version="4.0.0" />
|
|
|
|
A `ClientGroup` coordinates several independent FastMCP clients without combining them behind a proxy server. Each client keeps its own connection, negotiated protocol version, capabilities, and handlers. The group adds namespaced tool discovery and routes each call back to the client that advertised the tool.
|
|
|
|
This differs from passing a multi-server configuration directly to `Client`. `Client(config)` presents one aggregate MCP endpoint and therefore selects one protocol era shared by its proxy chain. A `ClientGroup` retains one MCP connection per configured server, so legacy and modern servers can operate in their native eras at the same time.
|
|
|
|
## Create a group from clients
|
|
|
|
Construct the clients explicitly when each server needs its own handlers, authentication, or connection settings:
|
|
|
|
```python
|
|
import asyncio
|
|
|
|
from fastmcp import Client
|
|
from fastmcp.client.group import ClientGroup
|
|
|
|
legacy_client = Client("legacy_server.py", mode="legacy")
|
|
modern_client = Client("https://modern.example.com/mcp", mode="auto")
|
|
|
|
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())
|
|
```
|
|
|
|
Tool names are prefixed with the configured client name by default. For example, a `get_weather` tool exposed by the `modern` client becomes `modern_get_weather`.
|
|
|
|
A group with a single client is a supported way to get namespacing alone: the one server's tools are presented under its configured name, with no other behavior change.
|
|
|
|
The first routed call loads the tool catalog lazily. After a successful load, unknown tool names fail locally rather than repeating discovery against every server. Call `list_tools()` explicitly to refresh the routes when servers add or remove tools dynamically — the explicit call refreshes past any client-side response cache, so the catalog reflects what every server advertises now.
|
|
|
|
## Bind tools to their owning client
|
|
|
|
Tool adapters — code that turns MCP tools into callables for an agent framework — often need more than routed calls: session-driven input loops, handler context, and interceptors must run on the connection that owns the tool. `resolve_tool()` returns that route, so an adapter can discover through the group and still bind each generated tool to its real client:
|
|
|
|
```python
|
|
async with group:
|
|
for tool in await group.list_tools():
|
|
route = await group.resolve_tool(tool.name)
|
|
# route.client is the connected FastMCP client for this tool;
|
|
# route.upstream_name is the name the server itself advertises
|
|
register_agent_tool(tool, client=route.client, name=route.upstream_name)
|
|
```
|
|
|
|
The group aggregates names and detects collisions; it never stands between the adapter and the client, so everything a single `Client` supports keeps working per tool.
|
|
|
|
## Create a group from configuration
|
|
|
|
`ClientGroup.from_config` creates one client per server rather than passing the entire configuration through a proxy. A FastMCP-specific `mode` field can select the protocol behavior for each server:
|
|
|
|
```python
|
|
from 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)
|
|
```
|
|
|
|
Entries without a `mode` use `"auto"` by default.
|
|
|
|
## Manage connections explicitly
|
|
|
|
Using the group as a context manager is optional. Applications can own each client connection and use the group only for discovery and routing:
|
|
|
|
```python
|
|
async with legacy_client, modern_client:
|
|
tools = await group.list_tools()
|
|
```
|
|
|
|
It is also safe to enter the group inside a client context. FastMCP client contexts are reference counted, so leaving the group does not close a connection still owned by an outer context.
|
|
|
|
The group adds no session handling of its own. Each client owns its transport and session exactly as it does standalone, so a legacy stateful session (for example over SSE or stdio) is held open by its client for as long as that client's context is active, whether the group or the caller entered it.
|
|
|
|
## Relationship to SDK session groups
|
|
|
|
The upstream official MCP Python SDK provides `ClientSessionGroup` for aggregating raw `ClientSession` connections, including tools, resources, and prompts. Its `connect_to_server()` path uses the classic `initialize` handshake, so it does not negotiate the modern protocol. Use it when classic sessions, raw SDK results, and dynamic group membership fit the application.
|
|
|
|
`connect_with_session()` can register a modern session that was connected separately, but the caller must create and keep that client alive because the SDK group does not own sessions registered this way. `ClientGroup` instead manages fully configured FastMCP clients directly. Each client can negotiate its own modern or legacy protocol, and calls continue through that client, preserving its authentication, handlers, caching, tracing, result parsing, and multi-round tool behavior. The API is intentionally narrower and currently aggregates tools only.
|