diff --git a/.changeset/fix-sse-disconnect-noise.md b/.changeset/fix-sse-disconnect-noise.md new file mode 100644 index 000000000..bf7c46383 --- /dev/null +++ b/.changeset/fix-sse-disconnect-noise.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/client': patch +--- + +Suppress `onerror` when an SSE stream disconnects but reconnection will be scheduled. Previously `onerror` fired unconditionally on every stream disconnect, producing `"SSE stream disconnected: TypeError: terminated"` noise every few minutes on long-lived connections even though the transport recovered transparently. diff --git a/packages/client/src/client/streamableHttp.ts b/packages/client/src/client/streamableHttp.ts index 56cbb4d98..95eeac7f6 100644 --- a/packages/client/src/client/streamableHttp.ts +++ b/packages/client/src/client/streamableHttp.ts @@ -445,9 +445,6 @@ export class StreamableHTTPClientTransport implements Transport { ); } } catch (error) { - // Handle stream errors - likely a network disconnect - this.onerror?.(new Error(`SSE stream disconnected: ${error}`)); - // Attempt to reconnect if the stream disconnects unexpectedly and we aren't closing // Reconnect if: already reconnectable (GET stream) OR received a priming event (POST stream with event ID) // BUT don't reconnect if we already received a response - the request is complete @@ -467,6 +464,9 @@ export class StreamableHTTPClientTransport implements Transport { } catch (error) { this.onerror?.(new Error(`Failed to reconnect: ${error instanceof Error ? error.message : String(error)}`)); } + } else { + // Stream disconnected and reconnection will not happen; surface the error + this.onerror?.(new Error(`SSE stream disconnected: ${error}`)); } } }; diff --git a/packages/client/test/client/streamableHttp.test.ts b/packages/client/test/client/streamableHttp.test.ts index 4a23e6db4..3285c8ea0 100644 --- a/packages/client/test/client/streamableHttp.test.ts +++ b/packages/client/test/client/streamableHttp.test.ts @@ -832,12 +832,9 @@ describe('StreamableHTTPClientTransport', () => { await vi.advanceTimersByTimeAsync(20); // Trigger reconnection timeout // ASSERT - expect(errorSpy).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringContaining('SSE stream disconnected: Error: Network failure') - }) - ); - // THE KEY ASSERTION: A second fetch call proves reconnection was attempted. + // onerror is NOT called: reconnection will handle the disconnect transparently + expect(errorSpy).not.toHaveBeenCalled(); + // A second fetch call proves reconnection was attempted. expect(fetchMock).toHaveBeenCalledTimes(2); expect(fetchMock.mock.calls[0]![1]?.method).toBe('GET'); expect(fetchMock.mock.calls[1]![1]?.method).toBe('GET');