MCP: initialized notification is not ordered before the first request, breaking stateful Streamable HTTP servers intermittently
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.7k
- PR merge metrics
- PR metrics pending
Description
Does this issue occur when all extensions are disabled?: Yes (MCP client is core)
- VS Code Version: 1.138.0 (also reproduced on 1.137.0)
- OS Version: macOS 15 / Windows 11 ARM64
What happens
Connecting to a stateful Streamable HTTP MCP server — one that issues Mcp-Session-Id on initialize and requires notifications/initialized before it will serve other requests — fails intermittently on the first real request:
Connection state: Error 400 status sending message to <server>/mcp:
{"jsonrpc":"2.0","id":2,"error":{"code":-32600,"message":"Session not initialized. Send notifications/initialized first."}}; will retry with new session ID
VS Code then retries with a new session and can fail the same way indefinitely, so the server appears permanently broken. Restarting it doesn't help; restarting repeatedly eventually succeeds. The failure rate scales with network latency — rare on a fast connection, near-constant for users on a higher-latency corporate network.
Why
notifications/initialized is sent fire-and-forget and is not ordered against the first real request, so the two are concurrent HTTP POSTs with no ordering guarantee.
mcpServerRequestHandler.ts:144— the handshake callssendNotification({ method: 'notifications/initialized' })without awaiting, thencreate()returns and the caller immediately issuestools/list(id:2).mcpServerRequestHandler.ts:281—sendNotificationisvoid;send()bottoms out inIMcpMessageTransport.send(), which is alsovoidby interface design, so there is nothing to await at this layer.extHostMcp.ts:385-392— inMcpHTTPHandle.send(), messages are only serialized through_requestSequencerwhile_mode.value === HttpMode.Unknown. Onceinitializereturns and the mode becomesHttpMode.Http, every subsequent send takes the unserializedelsebranch._sendStreamableHttpalso doesawait this._addAuthHeader(headers)before each fetch, adding enough jitter to flip the ordering.
So the sequencer that would have ordered these is switched off by the act of recognising the server as a session-based Streamable HTTP server — exactly the case where ordering matters most.
Verified server behaviour
Measured directly with raw HTTP against such a server:
| Request | Result |
|---|---|
initialize |
200, issues Mcp-Session-Id, < 1s |
request on a session before notifications/initialized |
400, -32600 Session not initialized |
notifications/initialized |
202 |
| request after the notification | 200 |
| request with an unknown session id | 404 (spec-correct) |
| request with no session header | 400 (spec-correct) |
Issuing the same calls sequentially always succeeds. Only the concurrent ordering fails.
Suggested fix
Keep serializing through the existing _requestSequencer until the initialized notification has actually been sent, instead of stopping as soon as the transport mode is known. This keeps the change local to McpHTTPHandle — no change to IMcpMessageTransport or the extension-host protocol — and leaves post-handshake traffic unserialized:
if (this._mode.value === HttpMode.Unknown || !this._didSendInitialized) {
await this._requestSequencer.queue(async () => {
await this._send(message);
if (isInitializedNotification(message)) {
this._didSendInitialized = true;
}
});
} else {
await this._send(message);
}
The flag is set only after the notification's POST resolves, so a request entering send() while it is still in flight queues behind it.
On the spec
To be upfront: I don't believe VS Code is violating the spec here. The only client-side ordering requirement in the lifecycle spec is "SHOULD NOT send requests other than pings before the server has responded to the initialize request", which VS Code honours; the rule referencing the initialized notification binds the server. The transports spec defines no server error for this state, and since every message is a separate POST, strict ordering isn't something the transport can guarantee — so this is being raised on the server side too.
Raising it here because servers that do enforce this ordering exist and are deployed, the failure is opaque from the user's side (it presents as an unrecoverable retry loop), and ordering one notification at startup is a cheap way to be robust against them.
Related: #330022 was a similar Streamable HTTP interop fix in the same client.
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 with mcpServerRequestHandler.ts around lines 144 and 281, then trace McpHTTPHandle.send() and _sendStreamableHttp in extHostMcp.ts around lines 385-392. Reproduce against a stateful Streamable HTTP server and verify that notifications/initialized completes before the first tools/list request, without the intermittent initialization error or retry loop.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, networking
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100