Welcome back! You have already made great progress in your MCP journey. In the last unit, you learned how to build an MCP server that communicates over HTTP using the Streamable HTTP transport. You also saw how to connect a client to this server and observed the handshake process and request flow. Up to this point, your server has been stateless — it treats every HTTP request as a completely new interaction, with no memory of previous requests or sessions.
In this lesson, you will take the next step: building a stateful HTTP MCP server. This means your server will be able to remember information about each client session, allowing for richer, more realistic interactions. Managing session state is essential for real-world MCP deployments, where clients may need to maintain context, track progress, or store user-specific data across multiple requests. By the end of this lesson, you will know how to implement session management in your MCP server and understand why this is a key skill for any production-ready MCP integration.
Let’s quickly review what you built in the previous lesson. Your MCP server used Express.js
to listen for HTTP POST requests at the /mcp
endpoint. For each request, the server created a new McpServer
and a new StreamableHTTPServerTransport
, connected them, and then processed the incoming MCP message. After handling the request, the server cleaned up all resources, ensuring that each request was handled in isolation.
This stateless approach is simple and works well for basic scenarios, but it has a major limitation: the server cannot remember anything about previous requests. Each time a client sends a message, it is as if the server is meeting the client for the first time. In real-world applications, you often need to maintain a connection or context across multiple requests from the same client. This is where stateful session management comes in.
It is important to understand the difference between stateless and stateful server behavior. In a stateless server, every request is independent. The server does not keep any information about previous requests or clients. This makes the server simple and easy to scale, but it also means you cannot support features like user authentication, progress tracking, or personalized responses.
A stateful server, on the other hand, keeps track of each client session. When a client connects, the server creates a session and stores information about that client. For every subsequent request from the same client, the server retrieves the session and continues the conversation as if it never ended. This is essential for scenarios where you need to remember user preferences, maintain a conversation history, or manage long-running tasks.
For example, imagine a client that starts a complex operation and needs to check back later for the result. With a stateless server, the client would have to start over every time. With a stateful server, the client can resume where it left off, making the experience much smoother and more powerful.
To make your MCP server stateful, you need a way to create, store, and retrieve sessions. In the context of the Streamable HTTP transport, a session is represented by a StreamableHTTPServerTransport
instance that is associated with a unique session ID. You can use a simple JavaScript object (dictionary) to map session IDs to their corresponding transport instances.
When a client sends a request, it includes a session ID in the HTTP headers (specifically, the mcp-session-id
header). If the session ID is present and matches an existing session, the server retrieves the corresponding transport and continues the session. If the session ID is missing or does not match any existing session, the server checks if the request is an "initialize" request. If so, it creates a new session, generates a new session ID, and stores the transport in the dictionary.
Here is how you can set up the session management logic in your server:
This code checks for a session ID in the request headers and tries to retrieve the corresponding transport from the transports
dictionary. If the session exists, the server can continue handling requests for that session. If not, it will need to create a new session, as you will see in the next section.
Now let's look at how the server decides whether to create a new session or continue an existing one. When a client sends a request, the server first checks if there's an existing session for the provided session ID. If a session ID is present in the headers, the server looks it up in the transports
dictionary to retrieve the corresponding transport.
The server only creates new resources when two specific conditions are both true:
- No existing session found (
!transport
) - This means either the client didn't send a session ID, or the session ID they sent doesn't match any session we have stored - The request is an "initialize" request (
isInitializeRequest(req.body)
) - This is the special first message that clients send when they want to start a new MCP session
Here is the session lookup and condition checking logic:
This code checks for a session ID in the request headers and tries to retrieve the corresponding transport from the transports
dictionary. If the session exists, the server can continue handling requests for that session. The if
condition ensures we only proceed with creating new resources when both conditions are met.
When both conditions are true — no existing session is found and the request is an "initialize" request — the server creates new resources by:
- Creating a new
McpServer
instance - Creating a new
StreamableHTTPServerTransport
- Providing a
sessionIdGenerator
function to generate a unique session ID - Setting an
onsessioninitialized
callback, which stores the transport in thetransports
dictionary when the session is fully initialized
This logic ensures that new sessions are created only when both conditions are true. For all other requests, the server expects a valid session ID and retrieves the existing transport. If the session ID is missing or invalid for non-initialize requests, the server returns an error.
Managing state also means you need to handle errors and clean up resources when sessions end. If a request comes in with an invalid session ID, the server should respond with a clear error message. If an error occurs while processing a request, the server should send a proper JSON-RPC error response and avoid leaving any resources hanging.
Here is how you can handle these cases:
This code checks for a valid session before processing the request. If the session is invalid, it returns a 400 error. If an error occurs during processing, it logs the error and returns a 500 error with a JSON-RPC error object. This helps keep your server robust and reliable, even when things go wrong.
Once you have implemented session management, it is important to verify that it works as expected. You can do this by running your server and the same client from the previous lesson, then observing the output in your terminal. When the client connects and sends a ping request to the server, you should see log messages that indicate which MCP method is being handled and which session is involved.
Here is an example of the expected output from the client:
And here is what you might see on the server:
Notice how the server logs the session ID for each request after the session is initialized. This confirms that the server is correctly tracking sessions and routing requests to the right transport. Unlike the previous stateless version where the server created and destroyed everything for each request, here the server keeps the session alive by storing the transport with its unique ID. This means subsequent requests from the same client can reuse the existing session instead of starting fresh each time. The MCP Server for each session will only be cleaned up when the entire server shuts down, though you could also implement logic to automatically clean up sessions after a period of inactivity. If you see errors about invalid sessions, double-check your session management logic and make sure the client is sending the correct session ID in the headers.
To better understand how stateful session management works in practice, let's walk through the complete request flow from when a client sends a request to when the server responds. This flow diagram illustrates the decision-making process your server goes through for each incoming request:
Here's what happens at each step:
1. Request Reception: When a POST request arrives at the /mcp
endpoint, the server first extracts the mcp-session-id
header to determine if this is part of an existing session.
2. Session Lookup: If a session ID is present, the server looks it up in the transports
dictionary. If no session ID is provided, the transport is set to undefined
.
3. Transport Validation: For existing session IDs, the server checks if a corresponding transport actually exists. Invalid or expired session IDs result in undefined
transport.
4. Initialize Request Check: When no valid transport is found, the server checks if this is an "initialize" request using isInitializeRequest()
. This is the only type of request allowed without an existing session.
- New Session Creation: For initialize requests, the server creates a new
McpServer
instance, then creates and connect a newStreamableHTTPServerTransport
with a session ID generator and callback.
6. Session Storage: The onsessioninitialized
callback automatically stores the new transport in the dictionary when the session is fully established.
In this lesson, you learned how to build a stateful HTTP MCP server that can manage and persist session state across multiple client requests. You saw how to create and store sessions using a transport dictionary, how to detect and initialize new sessions, and how to handle errors and clean up resources. You also learned how to verify that your session management is working by checking the server logs and client output.
Managing state is a crucial skill for building real-world MCP servers. It allows you to support more complex and interactive scenarios, where clients and servers can maintain context and work together over time. In the next section, you will get hands-on practice with these concepts, implementing and testing your own stateful MCP server. You are making excellent progress — keep up the great work!
