PrefectHQ/fastmcp · error · RuntimeError

ClientGroup clients are not connected: {names}

Error message

ClientGroup clients are not connected: {names}

What it means

ClientGroup raises this RuntimeError before any group operation (e.g. list_tools) when one or more of its managed clients have no active MCP session. The group never connects clients implicitly during operations; connections are established by entering the group's async context (`async with group:`) or by entering each client yourself. The error names every disconnected server so you can tell partial from total disconnect.

Source

Thrown at fastmcp_slim/fastmcp/client/group.py:144

        exc_type: type[BaseException] | None,
        exc_value: BaseException | None,
        traceback: TracebackType | None,
    ) -> bool | None:
        stack = self._exit_stack
        self._exit_stack = None
        self._tool_routes.clear()
        self._catalog_loaded = False
        if stack is not None:
            return await stack.__aexit__(exc_type, exc_value, traceback)
        return None

    def _require_connected(self) -> None:
        disconnected = [
            name for name, client in self._clients.items() if not client.is_connected()
        ]
        if disconnected:
            names = ", ".join(repr(name) for name in disconnected)
            raise RuntimeError(f"ClientGroup clients are not connected: {names}")

    def _require_route_connected(self, route: ToolRoute) -> None:
        if not route.client.is_connected():
            raise RuntimeError(
                f"ClientGroup client for server {route.server_name!r} is not connected"
            )

    async def list_tools(
        self, *, cache_mode: CacheMode = "refresh"
    ) -> list[mcp_types.Tool]:
        """List tools from every client with namespaced names.

        An explicit call is the group's catalog-refresh mechanism, so it
        defaults to `cache_mode="refresh"`: a client-side response cache
        (SEP-2549) is repopulated rather than served, and the routes reflect
        what every server advertises now. Pass `cache_mode="use"` to allow
        cache hits when staleness within the server's hint is acceptable.
        """

View on GitHub (pinned to 1f02114297)

Solutions

  1. Wrap usage in the group's async context manager: `async with ClientGroup.from_config(cfg) as group:` so every client connects on entry.
  2. If managing connections manually, enter each client's context (`async with client:`) — or the group context — before calling group methods; FastMCP client contexts are reference-counted so nesting is safe.
  3. Check connectivity up front with `[n for n, c in group.clients.items() if not c.is_connected()]` and reconnect the offenders (re-enter their contexts).
  4. If a client's session died mid-run (transport crash), recreate the ClientGroup or reconnect that client, then retry; routes/catalog reset on group exit so a fresh `async with` is the clean path.

Example fix

# before
group = ClientGroup.from_config(config)
tools = await group.list_tools()  # RuntimeError: clients are not connected

# after
async with ClientGroup.from_config(config) as group:
    tools = await group.list_tools()
Defensive patterns

Strategy: validation

Validate before calling

def ensure_group_connected(group) -> None:
    disconnected = [n for n, c in group.clients.items() if not c.is_connected()]
    if disconnected:
        raise RuntimeError(f"Connect these servers first: {disconnected}")

Type guard

def is_group_ready(group) -> bool:
    return all(c.is_connected() for c in group.clients.values())

Try / catch

try:
    tools = await group.list_tools()
except RuntimeError as e:
    if "not connected" in str(e):
        async with group_clients_recreated(config) as fresh_group:
            tools = await fresh_group.list_tools()
    else:
        raise

Prevention

When it happens

Trigger: Calling group.list_tools(), resolve_tool(), call_tool(), or call_tool_mcp() (which triggers lazy discovery) while at least one client in the group is not connected — e.g. you constructed ClientGroup(...) or ClientGroup.from_config(...) but never entered its async context, you used the clients outside the `async with` block, or a client's context was exited while the group object is still referenced.

Common situations: Forgetting `async with ClientGroup.from_config(cfg) as group:` and instead doing `group = ClientGroup.from_config(cfg); group.list_tools()`; managing client connections manually and one transport (stdio server that crashed, remote server that dropped) has died; reusing a group after its context manager exited; running list_tools inside a background task spawned after the `async with` block closed.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/7666edc59c6773f4. Report an issue: GitHub.