-
-
Notifications
You must be signed in to change notification settings - Fork 15
Description
Issue: Session Expiry Causes "Server not initialized" Errors
Hi - first, thank you for all the work on this library. It has been really helpful to us getting started with an elixir MCP server for our project.
We've run into an issue with inactive client sessions / timeouts, using Cursor, Claude or ChatGPT with our server, and wanted to post here to make sure I was understanding things correctly, or see if you had any feedback on approach.
Problem Description
When using anubis_mcp with the streamable_http transport, sessions that expire due to inactivity cause subsequent requests to fail with "Server not initialized" errors, even though the client and server are both active and the connection is re-established.
Observed behavior:
- Session expires after
session_idle_timeout(e.g., 30-60 minutes of inactivity) (as designed, in How to automatically close inactive sessions after a long period? cloudwalk/hermes-mcp#138) - Client sends a new request (e.g.,
tools/call) with the samemcp-session-id - Library creates a new session Agent for that session ID
- However, the new session is marked as
initialized: false - Request is rejected in
handle_single_requestwith "Server not initialized" error - Client is stuck - can't use the server without an explicit
initializehandshake
Root Cause
The library couples session lifecycle with initialization state. When a session expires:
- The session Agent is terminated
- A new session is created on the next request via
maybe_attach_session - But the new session defaults to
initialized: false - The library expects an explicit
initializerequest to mark it as initialized - Non-initialize requests are blocked by the guard in
handle_single_request:defguardp is_server_initialized(decoded, session) when Message.is_initialize_lifecycle(decoded) or Session.is_initialized(session)
This design assumes sessions should always go through the full MCP initialization handshake, even after expiry. However, in practice:
- The HTTP connection is persistent (SSE)
- Authentication happens at the transport/plug level (before MCP)
- Session expiry is an implementation detail, not a protocol event
- Clients don't expect to re-initialize after timeouts
Our Workaround
Since we handle authentication in a Plug before the MCP server (via JWT/API key), we implemented auto-initialization in our McpAuth plug:
defp ensure_mcp_session_initialized(conn) do
# Handle session expiry gracefully: when a session expires and a new request comes in,
# we create/reinitialize the session automatically since authentication already happened.
# This prevents "Server not initialized" errors after session timeout.
# gets from the `mcp-session-id` header passed on the request
mcp_session_id = get_mcp_session_id(conn)
session_name =
Anubis.Server.Registry.server_session(CustomerApi.AI.MCP.Server, mcp_session_id)
case Anubis.Server.Registry.whereis_server_session(
CustomerApi.AI.MCP.Server,
mcp_session_id
) do
nil ->
# Session doesn't exist - create it and mark as initialized
case Anubis.Server.Session.Supervisor.create_session(
Anubis.Server.Registry,
CustomerApi.AI.MCP.Server,
mcp_session_id
) do
{:ok, _pid} ->
Anubis.Server.Session.mark_initialized(session_name)
{:error, {:already_started, _pid}} ->
# Race condition - session was just created
Anubis.Server.Session.mark_initialized(session_name)
error ->
Logger.error("MCP session creation failed: #{inspect(error)}")
end
_pid ->
# Session exists - check if it needs initialization
session = Anubis.Server.Session.get(session_name)
if not session.initialized do
Anubis.Server.Session.mark_initialized(session_name)
end
end
endWe call this before the request reaches the MCP server in the auth plug, and after we've successfully authenticated the request. This seems to just work, because later on in the base server call to maybe_attach_session, it picks up the :already_started case and sets up process monitoring and expiry https://github.com/zoedsoupe/anubis-mcp/blob/v0.15.0/lib/anubis/server/base.ex#L630-L641
Library Options
Perhaps a couple options to consider:
Option 1: Auto-initialize on session recreation (simplest)
When maybe_attach_session creates a new session for an existing session_id, automatically mark it as initialized if the server has already seen an initialize request for that session ID in the past. This could be tracked in the server state. It seems like this would require persistent state using something like the proposal in #48
Option 2: Re-trigger init/2 on session recreation
When maybe_attach_session creates a new session for an existing session_id, if it is not initialized, it could call init/2 on the server to allow the implementation code to handle initialization if possible. In an app like ours where the only state we maintain is the auth layer on top of the server from the plug, this would be feasible.
Related Observations
- The SSE handler reconnects automatically (
sse_handler_down/sse_handler_registeredevents), which is great - But session expiry happens independently of connection state
- There's no way for servers to intercept session creation to customize initialization behavior
- The
init/2callback only runs on explicitinitializerequests, not on session creation
Hope this helps! Happy to discuss further or test any fixes.