Skip to content

SSEClientTransport doesn't re-establish lifecycle state on disconnect/reconnect #510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
BobDickinson opened this issue May 18, 2025 · 1 comment
Labels
bug Something isn't working

Comments

@BobDickinson
Copy link

I am writing an SSE proxy (using this library, including the SSEServerTransport) and struggling with what I assess as some bad client behavior caused by the standard libraries (including SSEClientTransport).

A client will connect and initialize, and send some number of requests receiving responses (notifications, etc). All great so far. When that connection goes away (long timeout, server restart, etc), the EventSource used by the SSE client re-establishes a new session with the server via the event source fetch method (calling the SSE url endpoint, typically /sse, and getting a new /messages endpoint for a new session). The problem is that the client does not then re-initialize to establish proper lifecycle state - it assumes the server somehow understands that it has already initialized and just starts blasting requests, which fail (the server requires an initialize message exchange to initialize the new session before it can accept requests).

I'm going by the lifecycle documentation provided here: https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle

My reading of that and my understanding of how the MCP SSE protocol works indicate that if you make a new SSE endpoint request (/sse), that starts a new session, and you need to start the protocol again (initialize) with the /messages endpoint you get back (which is associated with that new session). I see no other way that multiple clients (or even multiple client connection instances from a single client) could use the same SSE server endpoint. In terms of trying to work around this on my server/proxy, I also see no reliable way to associate a client on a new connection with a previous session (especially while avoiding punishing a client who behaved correctly and re-initialized the new connection).

If my assessment is correct, and essentially all SSE clients in the wild do not correctly re-establish sessions on reconnect, then I'm not sure how a multi-user or multi-client SSE server can be made to work, at least if it implements the protocol lifecycle properly.

The fix to the client library for this is pretty straightforward, but it doesn't address the fact that all clients currently in the wild (many of which won't update anytime soon) aren't going to work in some common scenarios using SSE servers.

If anyone is curious, this is the basic new connection logic (more or less as advised):

app.get('/sse', async (req: Request, res: Response) => {
  const transport = new SSEServerTransport('/messages', res);
  activeSessions.set(transport.sessionId, transport);
  // This will  return the new endpoint with the new session id
  await transport.start(); 
}
@BobDickinson BobDickinson added the bug Something isn't working label May 18, 2025
@BobDickinson
Copy link
Author

After further investigation, I think if you are implementing the server side of SSE you are out of luck. However, if you want to fix your client using SSEClientTransport to behave correctly across disconnects, you have a couple of options.

Note: I understand that SSE is deprecated - this is more about what do do if you have an SSE server/proxy and want compatability with the large number of clients that support SSE, or if you have a client that wants to use an existing MCP server where SSE is the best or only option.

Option 1, which many are probably doing by convention, is just to not rely on long-lived transports. Structure you app so that on each logical operation you connect the transport, make your MCP calls (potentially several), then disconnect.

Option 2, for maintaining long-lived clients using SSE (across connections), is to provide an event source init handler fetch function and watch it. This fetch function is only called by the transport's event source to initialize the session. The first time it is called the session initializes correctly. On any subsequent call to fetch the session will not be correctly initialized and subsequent use of the transport will likely fail.

You can do something like this:

protected async createTransport(url: string): Promise<Transport> {
    let fetchCount: number = 0;
    return new SSEClientTransport(this.url, {
        eventSourceInit: {
            fetch: (url, init) => {
                fetchCount++;
                if (fetchCount > 1) {
                    this.transport?.close();
                    this.transport = null;
                    return new Response(null, { status: 400, statusText: 'SSE Connection terminated, will reconnect on next message' });
                } else {
                    return fetch(url, init);
                }
            }
        }
    });
}

And then on any client operation that depends on the transport, check to see if the transport is set, and if not, create another one. In this way, the client can be long-lived and will just recycle the transport when the transport is disconnected (it has the side benefit of doing this on demand so you're not establishing and re-establishing connections to the SSE server over long periods of time when you app isn't actually calling the server).

I implemented option 2 in my app (TeamsSpark AI Workbench) and the previous failures were handled gracefully (primarily that I could restart my SSE server and my client app reconnected properly).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

1 participant