Problem
The current http-stream transport cannot be safely deployed behind a load balancer with multiple replicas.
HttpStreamTransport stores each StreamableHTTPServerTransport instance in an in-memory object indexed by MCP-Session-Id:
private _transports: {
[sessionId: string]: StreamableHTTPServerTransport
} = {};
When a client initializes a session, the session is created and stored only in the pod that handled the initialization request.
Subsequent requests include the returned MCP-Session-Id. If one of those requests is routed to another pod, that pod cannot find the session in its local _transports map and returns:
This effectively requires sticky sessions and also means that sessions are lost whenever a pod restarts or is replaced during a rolling deployment.
Current behaviour
With two replicas behind a round-robin load balancer:
-
initialize is routed to pod A.
-
Pod A creates session abc.
-
The client sends a subsequent request with:
-
The load balancer routes the request to pod B.
-
Pod B does not have abc in its local _transports object.
-
Pod B returns 404 Session not found.
Expected behaviour
It should be possible to configure the Streamable HTTP transport in stateless mode when the MCP server does not require server-side session state.
The official TypeScript SDK supports this by omitting the session ID generator:
new StreamableHTTPServerTransport({
sessionIdGenerator: undefined
});
In stateless mode:
- no
MCP-Session-Id is returned;
- incoming requests do not require a session ID;
- requests can be routed to any replica;
- Kubernetes deployments can scale horizontally without session affinity.
Existing configuration appears unused
HttpStreamTransportConfig already exposes a session configuration:
session?: {
enabled?: boolean;
headerName?: string;
allowClientTermination?: boolean;
maxConcurrentSessions?: number;
sessionTimeout?: number;
};
However, session.enabled does not appear to be used by HttpStreamTransport.
The transport currently always creates the SDK transport with:
sessionIdGenerator: () => randomUUID()
It also always rejects non-initialization requests without a session ID:
this.sendError(
res,
400,
-32000,
'Bad Request: No valid session ID provided'
);
As a result, configuring:
session: {
enabled: false
}
does not actually disable session management.
Proposed solution
Make session.enabled control whether the transport runs in stateful or stateless mode.
For example:
const sessionsEnabled = this._config.session?.enabled !== false;
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: sessionsEnabled
? () => randomUUID()
: undefined,
enableJsonResponse: this._enableJsonResponse,
});
The request handling logic would also need a stateless path that:
- creates or reuses an appropriate transport without requiring
MCP-Session-Id;
- does not store transports in
_transports;
- does not return
400 when the header is absent;
- does not return
404 for unknown session IDs when sessions are disabled.
A possible configuration would then be:
transport: {
type: 'http-stream',
options: {
responseMode: 'batch',
session: {
enabled: false
}
}
}
Alternative
If stateless mode is intentionally unsupported, this limitation should at least be documented explicitly:
- horizontal scaling requires session affinity;
- sessions are tied to a single process;
- pod restarts invalidate active sessions;
- rolling deployments may interrupt clients.
However, since the underlying MCP TypeScript SDK already supports stateless Streamable HTTP, exposing that capability through mcp-framework seems preferable.
Environment
mcp-framework: 0.2.22
- Transport:
http-stream
- Deployment: multiple Kubernetes replicas behind a load balancer
Problem
The current
http-streamtransport cannot be safely deployed behind a load balancer with multiple replicas.HttpStreamTransportstores eachStreamableHTTPServerTransportinstance in an in-memory object indexed byMCP-Session-Id:When a client initializes a session, the session is created and stored only in the pod that handled the initialization request.
Subsequent requests include the returned
MCP-Session-Id. If one of those requests is routed to another pod, that pod cannot find the session in its local_transportsmap and returns:This effectively requires sticky sessions and also means that sessions are lost whenever a pod restarts or is replaced during a rolling deployment.
Current behaviour
With two replicas behind a round-robin load balancer:
initializeis routed to pod A.Pod A creates session
abc.The client sends a subsequent request with:
MCP-Session-Id: abcThe load balancer routes the request to pod B.
Pod B does not have
abcin its local_transportsobject.Pod B returns
404 Session not found.Expected behaviour
It should be possible to configure the Streamable HTTP transport in stateless mode when the MCP server does not require server-side session state.
The official TypeScript SDK supports this by omitting the session ID generator:
In stateless mode:
MCP-Session-Idis returned;Existing configuration appears unused
HttpStreamTransportConfigalready exposes a session configuration:However,
session.enableddoes not appear to be used byHttpStreamTransport.The transport currently always creates the SDK transport with:
It also always rejects non-initialization requests without a session ID:
As a result, configuring:
does not actually disable session management.
Proposed solution
Make
session.enabledcontrol whether the transport runs in stateful or stateless mode.For example:
The request handling logic would also need a stateless path that:
MCP-Session-Id;_transports;400when the header is absent;404for unknown session IDs when sessions are disabled.A possible configuration would then be:
Alternative
If stateless mode is intentionally unsupported, this limitation should at least be documented explicitly:
However, since the underlying MCP TypeScript SDK already supports stateless Streamable HTTP, exposing that capability through
mcp-frameworkseems preferable.Environment
mcp-framework:0.2.22http-stream