From 4ee8087eec3d73befa82d4f95083cd192b09db01 Mon Sep 17 00:00:00 2001 From: Matan Tsach Date: Mon, 9 Mar 2026 12:24:48 +0200 Subject: [PATCH 1/2] fix: allow transport restart after close() close() aborts _abortController but never resets it to undefined, so start() sees a truthy guard and throws "already started." Same issue in SSEClientTransport with _eventSource. Also reset _endpoint in SSE to avoid stale endpoint URLs after restart. Fixes #1641 Co-Authored-By: Claude Opus 4.6 --- packages/client/src/client/sse.ts | 3 ++ packages/client/src/client/streamableHttp.ts | 1 + packages/client/test/client/sse.test.ts | 28 ++++++++++++++ .../client/test/client/streamableHttp.test.ts | 37 +++++++++++++++++++ 4 files changed, 69 insertions(+) diff --git a/packages/client/src/client/sse.ts b/packages/client/src/client/sse.ts index 133aa0004..c01dd3afa 100644 --- a/packages/client/src/client/sse.ts +++ b/packages/client/src/client/sse.ts @@ -239,7 +239,10 @@ export class SSEClientTransport implements Transport { async close(): Promise { this._abortController?.abort(); + this._abortController = undefined; this._eventSource?.close(); + this._eventSource = undefined; + this._endpoint = undefined; this.onclose?.(); } diff --git a/packages/client/src/client/streamableHttp.ts b/packages/client/src/client/streamableHttp.ts index 79a20adfc..06e71be1c 100644 --- a/packages/client/src/client/streamableHttp.ts +++ b/packages/client/src/client/streamableHttp.ts @@ -452,6 +452,7 @@ export class StreamableHTTPClientTransport implements Transport { this._reconnectionTimeout = undefined; } this._abortController?.abort(); + this._abortController = undefined; this.onclose?.(); } diff --git a/packages/client/test/client/sse.test.ts b/packages/client/test/client/sse.test.ts index 0b0aff67b..18f253d13 100644 --- a/packages/client/test/client/sse.test.ts +++ b/packages/client/test/client/sse.test.ts @@ -1528,4 +1528,32 @@ describe('SSEClientTransport', () => { expect(globalFetchSpy).not.toHaveBeenCalled(); }); }); + + describe('Transport lifecycle', () => { + it('should allow start() after close() for transport reuse', async () => { + transport = new SSEClientTransport(resourceBaseUrl); + await transport.start(); + + // close() the transport + await transport.close(); + + // Second start() should succeed — not throw "already started" + await transport.start(); + + // Verify transport is functional by sending a message + const message: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'test', + params: {}, + id: 'test-1' + }; + + await transport.send(message); + + // Wait for request processing + await new Promise(resolve => setTimeout(resolve, 50)); + + expect(lastServerRequest.method).toBe('POST'); + }); + }); }); diff --git a/packages/client/test/client/streamableHttp.test.ts b/packages/client/test/client/streamableHttp.test.ts index 8a550feae..fe63fc709 100644 --- a/packages/client/test/client/streamableHttp.test.ts +++ b/packages/client/test/client/streamableHttp.test.ts @@ -1659,4 +1659,41 @@ describe('StreamableHTTPClientTransport', () => { }); }); }); + + describe('Transport lifecycle', () => { + it('should allow start() after close() for transport reuse', async () => { + const fetchMock = globalThis.fetch as Mock; + + // First start() + await transport.start(); + + // close() the transport + await transport.close(); + + // Second start() should succeed — not throw "already started" + await transport.start(); + + // Verify transport is functional by sending a message + fetchMock.mockResolvedValueOnce({ + ok: true, + status: 200, + headers: new Headers({ 'content-type': 'application/json' }), + json: async () => ({ + jsonrpc: '2.0', + result: {}, + id: 'test-1' + }) + }); + + const message: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'test', + params: {}, + id: 'test-1' + }; + + await transport.send(message); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + }); }); From c26226730448677a71b6798bde808cf9a04d46a0 Mon Sep 17 00:00:00 2001 From: Matan Tsach Date: Fri, 13 Mar 2026 12:42:37 +0200 Subject: [PATCH 2/2] chore: add changeset for transport restart fix --- .changeset/popular-moles-begin.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/popular-moles-begin.md diff --git a/.changeset/popular-moles-begin.md b/.changeset/popular-moles-begin.md new file mode 100644 index 000000000..35c685a28 --- /dev/null +++ b/.changeset/popular-moles-begin.md @@ -0,0 +1,5 @@ +--- +"@modelcontextprotocol/client": patch +--- + +fix: allow transport restart after close()