Skip to content

StreamableHTTPClientTransport cannot be restarted after close() — breaks OAuth re-authentication #1641

Open

Description

@semistrict

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:

  1. transport.start() is called → sets this._abortController
  2. Server returns 401 → OAuth flow begins
  3. transport.close() is called → aborts _abortController but does NOT set it to undefined
  4. After OAuth completes, transport.start() is called again
  5. 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

Activity

  1. matantsach commented on Mar 9, 2026

    @matantsach
    Contributor

    Hi, I'd like to work on this. The fix is straightforward: close() aborts _abortController but never resets it to undefined, so start() sees a truthy value and throws "already started." Same pattern in SSEClientTransport with _eventSource. PR incoming.

  2. added a commit that references this issue on Mar 9, 2026
    4ee8087
  3. added a commit that references this issue on Mar 31, 2026
    db137d9
  4. ghost added
    bugSomething isn't working
    on Apr 16, 2026
  5. ghost added
    ready for workEnough information for someone to start working on
    on Apr 16, 2026
  6. ghost added
    authIssues and PRs related to Authentication / OAuth
    on Apr 16, 2026
  7. ghost added
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    on Apr 16, 2026
  8. ghost added
    fix proposedBot has a verified fix diff in the comment
    on Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues affecting some users, edge cases, potentially valuable featureauthIssues and PRs related to Authentication / OAuthbugSomething isn't workingfix proposedBot has a verified fix diff in the commentready for workEnough information for someone to start working on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions