diff --git a/.changeset/streamable-http-restart-close.md b/.changeset/streamable-http-restart-close.md new file mode 100644 index 0000000000..2e85312304 --- /dev/null +++ b/.changeset/streamable-http-restart-close.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/client': patch +--- + +Allow StreamableHTTPClientTransport to be restarted after close by clearing its aborted controller. diff --git a/packages/client/src/client/streamableHttp.ts b/packages/client/src/client/streamableHttp.ts index 3b8ddafe5a..3cb4b9809d 100644 --- a/packages/client/src/client/streamableHttp.ts +++ b/packages/client/src/client/streamableHttp.ts @@ -345,7 +345,7 @@ export class StreamableHTTPClientTransport implements Transport { const reconnect = (): void => { this._cancelReconnection = undefined; - if (this._abortController?.signal.aborted) return; + if (!this._abortController || this._abortController.signal.aborted) return; this._startOrAuthSse(options).catch(error => { this.onerror?.(new Error(`Failed to reconnect SSE stream: ${error instanceof Error ? error.message : String(error)}`)); try { @@ -513,6 +513,7 @@ export class StreamableHTTPClientTransport implements Transport { } finally { this._cancelReconnection = undefined; this._abortController?.abort(); + this._abortController = undefined; this.onclose?.(); } } diff --git a/packages/client/test/client/streamableHttp.test.ts b/packages/client/test/client/streamableHttp.test.ts index 0edf8b75ac..d84f706a0f 100644 --- a/packages/client/test/client/streamableHttp.test.ts +++ b/packages/client/test/client/streamableHttp.test.ts @@ -149,6 +149,18 @@ describe('StreamableHTTPClientTransport', () => { await reconnectTransport.close().catch(() => {}); }); + it('can be started again after close()', async () => { + await transport.start(); + const firstAbortController = transport['_abortController']; + + await transport.close(); + + expect(firstAbortController?.signal.aborted).toBe(true); + expect(transport['_abortController']).toBeUndefined(); + await expect(transport.start()).resolves.toBeUndefined(); + expect(transport['_abortController']).toBeDefined(); + }); + it('should terminate session with DELETE request', async () => { // First, simulate getting a session ID const message: JSONRPCMessage = {