Manage MCP Sessions over Streamable HTTP
Capture any MCP-Session-Id returned with InitializeResult, send it on every later HTTP request, and treat 404 as an instruction to initialize again. Authenticate every request, bind an unpredictable session handle to that client, and send DELETE when finished; accept 405 when the server does not support client-initiated termination.
Prerequisites
Use these steps for an MCP client or server implementing the 2025-11-25 Streamable HTTP transport. You need one MCP endpoint, such as https://example.com/mcp, and an implementation of the initialization exchange. If the server authenticates clients, the request handler must also have access to the authenticated user or client identity; the session identifier cannot replace that identity.
A session begins with initialization, not with the first tool or resource request. The MCP lifecycle specification dated 2025-11-25 requires initialization to be the first client-server interaction. The client proposes a protocol version and capabilities, the server returns its negotiated version and capabilities, and the client sends notifications/initialized after a successful response. If that sequence is not already working, implement the initialization lifecycle from connection to normal operation before adding session state.
Steps
-
Initialize without a session identifier
Send the
initializerequest as a new HTTP POST to the MCP endpoint. Do not attach anMCP-Session-Id: no session exists yet, and a client receiving HTTP 404 for an old session must also restart with a newInitializeRequestthat has no session identifier attached.Under the Streamable HTTP transport requirements dated 2025-11-25, the POST must advertise both
application/jsonandtext/event-streaminAccept. The server may return theInitializeResultas one JSON object or through an SSE stream, so the client must handle both response forms.A stateful server may assign a session identifier by adding this response header to the HTTP response that contains the
InitializeResult:MCP-Session-Id: <server-generated-session-id>Issuing the header is optional. Its absence means the client continues without an MCP session identifier; the client must not invent one or assume that every Streamable HTTP server is stateful. This is the right answer for a server that does not need to associate later HTTP requests with stored session state. Issuing an identifier is appropriate when the server does need that association, but it adds lifecycle and security work: the server must recognize the handle later, decide when it stops being valid, and reject its use after termination.
Capture the header from the response carrying the successful
InitializeResult. Keep it associated with that initialized client-server relationship. Do not promote a value from an unrelated response, a failed initialization, or a previous connection into the new session.Complete initialization by sending
notifications/initialized. Session creation does not remove the lifecycle ordering rules: before the server has answeredinitialize, the client should not send requests other than pings; before the server receivesnotifications/initialized, it should not send requests other than pings and logging messages. -
Return the identifier on every later HTTP request
If initialization returned
MCP-Session-Id, the client must include the same header on all subsequent HTTP requests to that MCP endpoint. Treat this as request construction state, not as a header added only to tool calls.It therefore belongs on:
- POST requests carrying JSON-RPC requests, notifications, or responses;
- GET requests used to open a server-to-client SSE stream;
- GET requests used to resume a stream after disconnection;
- the eventual DELETE request used to terminate the session.
Centralize this behavior in the transport layer. If individual features add the header themselves, a new request path can silently omit it. The server then cannot reliably associate that request with the initialized session.
The server should return HTTP 400 Bad Request when it requires a session identifier and receives a post-initialization request without one. Reserve HTTP 404 Not Found for a supplied identifier whose session the server has terminated. That distinction gives the client a useful diagnosis: 400 points to broken request construction, while 404 triggers a fresh initialization cycle.
The session header is separate from the protocol-version header. After initialization, HTTP clients must also send the negotiated version in
MCP-Protocol-Version; keeping the two fields separate makes a protocol version mismatch distinguishable from missing or expired session state.It is also separate from
Last-Event-ID. An MCP session groups logically related interactions. An SSE event ID is a cursor for one stream. When resuming a disconnected SSE stream, the client sends a GET with both the session identifier and the last event ID it received. The server may useLast-Event-IDto replay later events from that stream, but it must not replay events belonging to a different stream.A client may keep several SSE streams open during one session. The server must deliver each JSON-RPC message on only one of them rather than broadcasting it across every connection. Closing one SSE connection does not by itself mean that the client discarded its session, and a disconnection must not be interpreted as cancellation of the request that opened the stream.
-
Make the session handle unpredictable and bind it to authorization
Generate the identifier on the server. It should be globally unique and cryptographically secure, using a secure random source rather than a counter, timestamp, user name, or other guessable sequence. The transport specification permits visible ASCII characters from
0x21through0x7E; choose an encoding that stays inside that range.Treat the value as an authorization-sensitive handle even though it is not an authentication credential. The MCP security best practices dated 2025-11-25 describe the failure mode: an attacker who obtains a session identifier can send requests under that identifier, and a server that performs no further authorization check may accept the attacker as the original client.
The required boundary is therefore:
- authenticate and authorize every inbound request where authorization applies;
- never accept possession of
MCP-Session-Idas proof of identity; - look up the session in the context of the authenticated client;
- reject a session when its stored owner does not match the identity established by the current request.
Bind the session to server-derived user information, not to an identity field supplied beside the session header. The security guidance gives
<user_id>:<session_id>as an example storage or queue key. Here,user_idcomes from the verified user token andsession_idcomes from the securely generated handle. An attacker cannot switch the lookup to another user merely by submitting that user’s session identifier.Apply the same binding wherever session-related data travels. This matters when several stateful HTTP servers share a queue: one server may enqueue an event and another may retrieve it for later delivery. Keying shared state only by the session identifier allows a stolen or guessed handle to cross the authorization boundary. Combining it with the authenticated user identity keeps routing tied to the client for whom the session was created.
Rotation and expiry can reduce exposure, but they do not repair predictable identifiers or missing authorization checks. Their cost is more client recovery through initialization and more server cleanup. Use them as additional controls, not substitutes for secure generation and identity binding.
A session identifier is the wrong place to encode trust that the server does not independently verify. It is also the wrong answer when the server has no state to recover between HTTP requests: omitting session management avoids creating an authorization-sensitive handle with no operational purpose.
-
Handle server termination as a state transition
A server may terminate a session at any time. After termination, every request carrying that identifier must receive HTTP 404 Not Found. Do not revive the old server-side state merely because another request presents the same value.
On receiving that 404 for a request containing
MCP-Session-Id, the client must discard the expired handle and start a new session. Send a newInitializeRequestwithout the old header, process the newInitializeResult, capture a newly issued identifier if present, and sendnotifications/initializedbefore returning to normal operation.Keep recovery explicit. A new initialization may negotiate a protocol version or capability set different from the terminated session, so the client must use the new result rather than copying the old session’s negotiated state. Replacing only the identifier leaves the transport and lifecycle out of agreement.
Do not treat a closed connection as proof that the server terminated the logical session. Streamable HTTP permits SSE connections to close and later resume, and servers may close streams without ending their sessions. Conversely, a 404 tied to the submitted session identifier is the specified signal that the client must initialize again.
-
Terminate the session with DELETE when the client is finished
When the client no longer needs a session—for example, when the user leaves the client application—it should send HTTP DELETE to the MCP endpoint with the active identifier:
DELETE /mcp HTTP/1.1 MCP-Session-Id: <active-session-id>Use DELETE for logical session termination, not for cancelling one in-flight JSON-RPC request. Disconnection is not cancellation either. If the client needs to stop a request while retaining the surrounding session, use the protocol’s request cancellation mechanism instead.
Explicit termination is optional on the server. A server that does not permit clients to terminate sessions may answer DELETE with HTTP 405 Method Not Allowed. The client must treat that outcome as an unsupported operation rather than retrying DELETE as another HTTP method or assuming that the session disappeared.
If the server accepts explicit termination, later use of the terminated identifier falls under the normal termination rule and must receive 404. The client should remove its local copy after the termination attempt because it has declared that it no longer needs the session; a future interaction begins with initialization rather than reuse of that handle.
DELETE is the wrong operation when the client merely lost an SSE connection and intends to resume it. Resume with GET and
Last-Event-ID, retainingMCP-Session-Id. Terminate only when the logical relationship is finished. -
Test the session boundary from both sides
Test behavior at the HTTP boundary rather than checking only an in-memory session object. The minimum useful cases follow directly from the transport and security rules:
- Initialize without
MCP-Session-Id; verify that a stateful server may return one only with the response containingInitializeResult. - Send a later POST and GET with that value; verify that both reach the same authenticated session state.
- Omit the header from a later request to a server that requires it; verify HTTP 400.
- Present the identifier under a different authenticated identity; verify that the server does not expose or mutate the original client’s session.
- Terminate or expire the session on the server, then reuse the value; verify HTTP 404.
- On that 404, verify that the client sends a new
InitializeRequestwithout the expired identifier and waits for initialization to complete. - Send DELETE with an active identifier; verify either supported termination followed by 404 on reuse, or HTTP 405 when explicit termination is unsupported.
- Close an SSE connection without DELETE; verify that the client does not misclassify the disconnect as request cancellation or confirmed session termination.
Run the authorization case through every server instance and shared queue path that can accept or deliver session-bound work. A secure check in the initialization handler is insufficient if a later POST, GET, resumption, or queued event bypasses the same authenticated-client binding.
- Initialize without
Expected result
A completed implementation creates session state only during successful initialization, returns any server-issued MCP-Session-Id on every later HTTP request, and keeps that unpredictable handle bound to the authenticated client. Missing required handles produce 400, terminated handles produce 404 and fresh initialization, and DELETE either ends the session explicitly or returns 405 when unsupported.
Sources
- MCP lifecycle specification dated 2025-11-25modelcontextprotocol.io
- Streamable HTTP transport requirements dated 2025-11-25modelcontextprotocol.io
- MCP security best practices dated 2025-11-25modelcontextprotocol.io
See also
How MCP servers attach optional icons to tools, resources, resource templates, and prompts without changing capability behavior.
How an IDE assistant and a desktop assistant differ as MCP clients: config shape, repository context, tool approval, OAuth reach, and marketplace plugins.
How an MCP client and server exchange versions, capabilities and implementation details before normal protocol requests begin.
Tell MCP protocol errors from tool failures by inspecting the JSON-RPC envelope, request ID, error code, data, and isError flag.