modelcontextprotocol / modelcontextprotocol/typescript-sdk
StreamableHTTPClientTransport cannot be restarted after close() — breaks OAuth re-authentication
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 13.4k
- Forks
- 2.2k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 4
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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/client/streamableHttp.ts at StreamableHTTPClientTransport.start() and close(), then trace the _abortController lifecycle. Done when closing leaves the transport restartable and the OAuth re-authentication sequence can call start() without the already-started error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, authentication
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100