Skip to content

Commit 75078cf

Browse files
committed
Add statelessMode to streamableHttp.ts
1 parent 64653f5 commit 75078cf

File tree

2 files changed

+44
-32
lines changed

2 files changed

+44
-32
lines changed

src/server/streamableHttp.test.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ interface TestServerConfig {
1414
enableJsonResponse?: boolean;
1515
customRequestHandler?: (req: IncomingMessage, res: ServerResponse, parsedBody?: unknown) => Promise<void>;
1616
eventStore?: EventStore;
17+
statelessMode?: boolean;
1718
}
1819

1920
/**
2021
* Helper to create and start test HTTP server with MCP setup
2122
*/
22-
async function createTestServer(config: TestServerConfig = { sessionIdGenerator: (() => randomUUID()) }): Promise<{
23+
async function createTestServer(config: TestServerConfig = { sessionIdGenerator: (() => randomUUID()), statelessMode: false }): Promise<{
2324
server: Server;
2425
transport: StreamableHTTPServerTransport;
2526
mcpServer: McpServer;
@@ -41,6 +42,7 @@ async function createTestServer(config: TestServerConfig = { sessionIdGenerator:
4142

4243
const transport = new StreamableHTTPServerTransport({
4344
sessionIdGenerator: config.sessionIdGenerator,
45+
statelessMode: config.statelessMode,
4446
enableJsonResponse: config.enableJsonResponse ?? false,
4547
eventStore: config.eventStore
4648
});
@@ -313,7 +315,7 @@ describe("StreamableHTTPServerTransport", () => {
313315
// First initialize to get a session ID
314316
sessionId = await initializeServer();
315317

316-
// Open a standalone SSE stream
318+
// Open a standalone SSE stream
317319
const sseResponse = await fetch(baseUrl, {
318320
method: "GET",
319321
headers: {
@@ -810,7 +812,7 @@ describe("StreamableHTTPServerTransport with pre-parsed body", () => {
810812
id: "preparsed-1",
811813
};
812814

813-
// Send an empty body since we'll use pre-parsed body
815+
// Send an empty body since we'll use pre-parsed body
814816
const response = await fetch(baseUrl, {
815817
method: "POST",
816818
headers: {
@@ -1028,7 +1030,7 @@ describe("StreamableHTTPServerTransport with resumability", () => {
10281030
expect(idMatch).toBeTruthy();
10291031
const firstEventId = idMatch![1];
10301032

1031-
// Send a second notification
1033+
// Send a second notification
10321034
await mcpServer.server.sendLoggingMessage({ level: "info", data: "Second notification from MCP server" });
10331035

10341036
// Close the first SSE stream to simulate a disconnect
@@ -1064,7 +1066,7 @@ describe("StreamableHTTPServerTransport in stateless mode", () => {
10641066
let baseUrl: URL;
10651067

10661068
beforeEach(async () => {
1067-
const result = await createTestServer({ sessionIdGenerator: undefined });
1069+
const result = await createTestServer({ sessionIdGenerator: undefined, statelessMode: true });
10681070
server = result.server;
10691071
transport = result.transport;
10701072
baseUrl = result.baseUrl;
@@ -1075,13 +1077,6 @@ describe("StreamableHTTPServerTransport in stateless mode", () => {
10751077
});
10761078

10771079
it("should operate without session ID validation", async () => {
1078-
// Initialize the server first
1079-
const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize);
1080-
1081-
expect(initResponse.status).toBe(200);
1082-
// Should NOT have session ID header in stateless mode
1083-
expect(initResponse.headers.get("mcp-session-id")).toBeNull();
1084-
10851080
// Try request without session ID - should work in stateless mode
10861081
const toolsResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList);
10871082

@@ -1117,7 +1112,7 @@ describe("StreamableHTTPServerTransport in stateless mode", () => {
11171112
});
11181113

11191114
it("should reject second SSE stream even in stateless mode", async () => {
1120-
// Despite no session ID requirement, the transport still only allows
1115+
// Despite no session ID requirement, the transport still only allows
11211116
// one standalone SSE stream at a time
11221117

11231118
// Initialize the server first

src/server/streamableHttp.ts

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,18 @@ export interface StreamableHTTPServerTransportOptions {
3434
/**
3535
* Function that generates a session ID for the transport.
3636
* The session ID SHOULD be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash)
37-
*
38-
* Return undefined to disable session management.
37+
*
38+
* Not used in stateless mode.
3939
*/
4040
sessionIdGenerator: (() => string) | undefined;
4141

42+
/**
43+
* Whether to run in stateless mode (no session management).
44+
* In stateless mode, no initialization is required and there's no session tracking.
45+
* Default is false (stateful mode).
46+
*/
47+
statelessMode?: boolean;
48+
4249
/**
4350
* A callback for session initialization events
4451
* This is called when the server initializes a new session.
@@ -64,37 +71,39 @@ export interface StreamableHTTPServerTransportOptions {
6471

6572
/**
6673
* Server transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification.
67-
* It supports both SSE streaming and direct HTTP responses.
68-
*
74+
* It supports both SSE streaming and direct HTTP responses.r
75+
*
6976
* Usage example:
70-
*
77+
*
7178
* ```typescript
7279
* // Stateful mode - server sets the session ID
7380
* const statefulTransport = new StreamableHTTPServerTransport({
74-
* sessionId: randomUUID(),
81+
* sessionIdGenerator: () => randomUUID(),
7582
* });
76-
*
77-
* // Stateless mode - explicitly set session ID to undefined
83+
*
84+
* // Stateless mode - explicitly enable stateless mode
7885
* const statelessTransport = new StreamableHTTPServerTransport({
79-
* sessionId: undefined,
86+
* statelessMode: true,
87+
* enableJsonResponse: true, // Optional
8088
* });
81-
*
89+
*
8290
* // Using with pre-parsed request body
8391
* app.post('/mcp', (req, res) => {
8492
* transport.handleRequest(req, res, req.body);
8593
* });
8694
* ```
87-
*
95+
*
8896
* In stateful mode:
8997
* - Session ID is generated and included in response headers
9098
* - Session ID is always included in initialization responses
9199
* - Requests with invalid session IDs are rejected with 404 Not Found
92100
* - Non-initialization requests without a session ID are rejected with 400 Bad Request
93101
* - State is maintained in-memory (connections, message history)
94-
*
102+
*
95103
* In stateless mode:
96-
* - Session ID is only included in initialization responses
104+
* - No initialization is required before handling requests
97105
* - No session validation is performed
106+
* - No state is maintained between requests
98107
*/
99108
export class StreamableHTTPServerTransport implements Transport {
100109
// when sessionId is not set (undefined), it means the transport is in stateless mode
@@ -108,6 +117,7 @@ export class StreamableHTTPServerTransport implements Transport {
108117
private _standaloneSseStreamId: string = '_GET_stream';
109118
private _eventStore?: EventStore;
110119
private _onsessioninitialized?: (sessionId: string) => void;
120+
private _statelessMode: boolean = false;
111121

112122
sessionId?: string | undefined;
113123
onclose?: () => void;
@@ -119,6 +129,12 @@ export class StreamableHTTPServerTransport implements Transport {
119129
this._enableJsonResponse = options.enableJsonResponse ?? false;
120130
this._eventStore = options.eventStore;
121131
this._onsessioninitialized = options.onsessioninitialized;
132+
this._statelessMode = options.statelessMode ?? false;
133+
134+
// In stateless mode, no initialization is required
135+
if (this._statelessMode) {
136+
this._initialized = true;
137+
}
122138
}
123139

124140
/**
@@ -166,7 +182,7 @@ export class StreamableHTTPServerTransport implements Transport {
166182
}
167183

168184
// If an Mcp-Session-Id is returned by the server during initialization,
169-
// clients using the Streamable HTTP transport MUST include it
185+
// clients using the Streamable HTTP transport MUST include it
170186
// in the Mcp-Session-Id header on all of their subsequent HTTP requests.
171187
if (!this.validateSession(req, res)) {
172188
return;
@@ -180,7 +196,7 @@ export class StreamableHTTPServerTransport implements Transport {
180196
}
181197
}
182198

183-
// The server MUST either return Content-Type: text/event-stream in response to this HTTP GET,
199+
// The server MUST either return Content-Type: text/event-stream in response to this HTTP GET,
184200
// or else return HTTP 405 Method Not Allowed
185201
const headers: Record<string, string> = {
186202
"Content-Type": "text/event-stream",
@@ -376,7 +392,7 @@ export class StreamableHTTPServerTransport implements Transport {
376392

377393
}
378394
// If an Mcp-Session-Id is returned by the server during initialization,
379-
// clients using the Streamable HTTP transport MUST include it
395+
// clients using the Streamable HTTP transport MUST include it
380396
// in the Mcp-Session-Id header on all of their subsequent HTTP requests.
381397
if (!isInitializationRequest && !this.validateSession(req, res)) {
382398
return;
@@ -475,11 +491,12 @@ export class StreamableHTTPServerTransport implements Transport {
475491
}));
476492
return false;
477493
}
478-
if (this.sessionId === undefined) {
479-
// If the session ID is not set, the session management is disabled
480-
// and we don't need to validate the session ID
494+
495+
// If in stateless mode or the session ID is undefined, no session validation is needed
496+
if (this._statelessMode || this.sessionId === undefined) {
481497
return true;
482498
}
499+
483500
const sessionId = req.headers["mcp-session-id"];
484501

485502
if (!sessionId) {

0 commit comments

Comments
 (0)