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
- Check server logs for a crash or unhandled exception at disconnect time
- Verify the transport target: command path/env for stdio, URL/auth for remote servers
- Add retry/reconnect logic around the client session for long-lived connections
- 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
- Monitor server process health and logs
- Add reconnect/backoff around long-lived client sessions
- Verify transport URLs, commands, and auth before connecting
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
- Session task completed without exception but connection fail
- Both 'httpx_client_factory' and 'verify' were provided. The
- Both 'httpx_client_factory' and 'verify' were provided. The
- module {__name__!r} has no attribute {name!r}
- Cannot resolve tool reference: {fn!r}
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/f900a6182ec91f11.
Report an issue: GitHub.