PrefectHQ/fastmcp · error · RuntimeError

Server session was closed unexpectedly

Error message

Server session was closed unexpectedly

What it means

While the client's session runner is active, a transport stream closed by the remote side (anyio.ClosedResourceError) is converted into this RuntimeError. It means the server terminated the connection mid-session rather than the client closing it. The original ClosedResourceError is chained as the cause.

Source

Thrown at fastmcp_slim/fastmcp/client/client.py:833

        # Only passed when this client actually wants non-default settings, so an
        # ordinary client never sends an argument a transport might not accept.
        if transport_options is not None:
            connection = self.transport.connect_session(
                transport_options=transport_options, **self._session_kwargs
            )
        else:
            connection = self.transport.connect_session(**self._session_kwargs)

        with catch(get_catch_handlers()):
            async with connection as session:
                self._session_state.session = session
                # Initialize the session if auto_initialize is enabled
                try:
                    if self.auto_initialize:
                        await self._negotiate()
                    yield
                except anyio.ClosedResourceError as e:
                    raise RuntimeError("Server session was closed unexpectedly") from e
                finally:
                    self._reset_session_state()

    async def _negotiate(
        self,
        timeout: datetime.timedelta | float | int | None = None,
    ) -> None:
        """Run the connect-time protocol negotiation dictated by ``self.mode``.

        - ``"legacy"``: today's initialize handshake (populates ``initialize_result``).
        - ``"auto"``: probe ``server/discover`` at the newest modern version and adopt it,
          denylist-falling-back to the initialize handshake for handshake-era servers.
        - a modern version string: adopt that version directly (from ``prior_discover`` if
          supplied, else a synthesized minimal ``DiscoverResult``).

        Idempotent: once the session has a negotiated protocol version, this is a no-op.
        """
        if self.session.protocol_version is not None:

View on GitHub (pinned to 1f02114297)

Solutions

  1. Check server logs for a crash or unhandled exception at disconnect time
  2. Verify the transport target: command path/env for stdio, URL/auth for remote servers
  3. Add retry/reconnect logic around the client session for long-lived connections
  4. Ensure server/client protocol versions are compatible

Example fix

// before
async with client:
    result = await client.call_tool("run", {})  # RuntimeError if server died

// after
for attempt in range(3):
    try:
        async with client:
            result = await client.call_tool("run", {})
        break
    except RuntimeError as e:
        if "closed unexpectedly" not in str(e) or attempt == 2:
            raise
Defensive patterns

Strategy: retry

Validate before calling

import shutil
assert shutil.which(server_command) is not None, "server executable missing"

Try / catch

try:
    async with client:
        result = await client.call_tool("run", {})
except RuntimeError as e:
    if "closed unexpectedly" in str(e):
        logger.error("server died; cause=%r", e.__cause__)
    else:
        raise

Prevention

When it happens

Trigger: The server process exits, crashes, or closes the transport (stdio pipe break, dropped HTTP/SSE stream, killed subprocess) while the client is inside `async with client:` and issues a request.

Common situations: Server crashes under load or on an unhandled exception; stdio server's stdout closes killing the subprocess; a proxy/gateway times out and terminates the stream; OS kills the server process.

Related errors


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