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); + }); + }); });