{"record":{"id":"b88a75ad1db08a08","repo":"PrefectHQ/fastmcp","slug":"clientgroup-is-already-connected","errorCode":null,"errorMessage":"ClientGroup is already connected","messagePattern":"ClientGroup is already connected","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/client/group.py","lineNumber":93,"sourceCode":"            config if isinstance(config, MCPConfig) else MCPConfig.from_dict(config)\n        )\n        clients: dict[str, Client[Any]] = {}\n\n        for name, server in parsed.mcpServers.items():\n            configured_mode = (server.model_extra or {}).get(\"mode\", default_mode)\n            if not isinstance(configured_mode, str):\n                raise TypeError(f\"Protocol mode for server {name!r} must be a string\")\n            clients[name] = Client(server.to_transport(), mode=configured_mode)\n\n        return cls(clients)\n\n    @property\n    def protocol_versions(self) -> dict[str, str | None]:\n        return {name: client.protocol_version for name, client in self._clients.items()}\n\n    async def __aenter__(self) -> ClientGroup:\n        if self._exit_stack is not None:\n            raise RuntimeError(\"ClientGroup is already connected\")\n\n        # Claim the stack before the first await so a concurrent entry hits the\n        # guard above instead of racing past it and overwriting this one.\n        stack = contextlib.AsyncExitStack()\n        self._exit_stack = stack\n        await stack.__aenter__()\n\n        # Connect concurrently: entry latency stays one handshake deep instead\n        # of growing linearly with the number of servers. With\n        # return_exceptions=True every connection attempt runs to completion,\n        # so on partial failure the successes are known and can be unwound.\n        clients = list(self._clients.values())\n        results = await gather(\n            (client.__aenter__() for client in clients), return_exceptions=True\n        )\n        errors = [result for result in results if isinstance(result, BaseException)]\n        if errors:\n            for client, result in zip(clients, results, strict=True):","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/client/group.py#L75-L111","documentation":"ClientGroup is a single-use async context manager owning one AsyncExitStack, set on entry and cleared on exit. Entering `async with group:` a second time before exit (or concurrently) raises this RuntimeError so the first session isn't silently overwritten. The stack is claimed before the first await so a concurrent entry hits the guard instead of racing past it.","triggerScenarios":"Re-entering an already-entered ClientGroup (e.g. nested `async with group:` blocks); multiple concurrent tasks entering one shared instance; re-entering in a retry loop without exiting first.","commonSituations":"A shared group stored in module/global state entered in multiple places; retry logic re-entering the same group instance inside a loop; concurrent coroutines entering simultaneously.","solutions":["Enter the group once and keep all work inside that single `async with` block","Create a new ClientGroup for each independent scope instead of reusing a connected one","For shared access, enter once in a parent task and pass the connected group down"],"exampleFix":"// before\nasync with group:\n    ...\nasync with group:  # RuntimeError: already connected\n    ...\n\n// after\nasync with group:\n    # do all work here\n    ...","handlingStrategy":"try-catch","validationCode":"def group_is_free(group) -> bool:\n    return group._exit_stack is None","typeGuard":null,"tryCatchPattern":"try:\n    async with group:\n        ...\nexcept RuntimeError as e:\n    if \"already connected\" in str(e):\n        ...  # reuse the active session instead of re-entering\n    else:\n        raise","preventionTips":["Enter each ClientGroup exactly once per scope","Never store a connected group in shared/global state for re-entry","Use per-task fresh ClientGroup instances or a single owning task"],"tags":["python","async","lifecycle","reentrancy"],"backgroundTag":"context-manager-reentry","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}