-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
Bug Description
StreamableHTTPClientTransport.start() throws "StreamableHTTPClientTransport already started!" if called after close(), because close() aborts the _abortController but never resets it to undefined.
This breaks any flow that needs to reconnect after OAuth authentication, because:
transport.start()is called → setsthis._abortController- Server returns 401 → OAuth flow begins
transport.close()is called → aborts_abortControllerbut does NOT set it toundefined- After OAuth completes,
transport.start()is called again - The guard at line 257 checks
if (this._abortController)→ still truthy → throws
Reproduction
Any MCP client that uses StreamableHTTPClientTransport with an OAuth auth provider hitting a server that requires authentication will fail on the first connection attempt. The token is saved successfully, but the transport cannot be restarted in the same process.
This is observable with tools like mcporter when connecting to an OAuth-protected MCP server for the first time.
Root Cause
In src/client/streamableHttp.ts, the close() method aborts the controller but doesn't clear the reference:
async close(): Promise<void> {
// ...
this._abortController?.abort(); // aborts but keeps reference
this.onclose?.();
}While start() guards against re-entry by checking if _abortController exists:
async start(): Promise<void> {
if (this._abortController) {
throw new Error('StreamableHTTPClientTransport already started!...');
}
this._abortController = new AbortController();
}Suggested Fix
Reset _abortController to undefined in close():
async close(): Promise<void> {
// ...
this._abortController?.abort();
this._abortController = undefined;
this.onclose?.();
}This allows the transport to be restarted after being closed, which is the expected lifecycle for OAuth re-authentication flows.
Environment
@modelcontextprotocol/sdk: 1.27.1- Runtime: Node.js
- Transport: StreamableHTTPClientTransport