@@ -75,6 +75,7 @@ public function __construct(
75
75
private string $ mcpPath = '/mcp ' ,
76
76
private ?array $ sslContext = null ,
77
77
private readonly bool $ enableJsonResponse = true ,
78
+ private readonly bool $ stateless = false ,
78
79
?EventStoreInterface $ eventStore = null
79
80
) {
80
81
$ this ->logger = new NullLogger ();
@@ -171,9 +172,9 @@ private function createRequestHandler(): callable
171
172
172
173
try {
173
174
return match ($ method ) {
174
- 'GET ' => $ this ->handleGetRequest ($ request )->then ($ addCors , fn ($ e ) => $ addCors ($ this ->handleRequestError ($ e , $ request ))),
175
- 'POST ' => $ this ->handlePostRequest ($ request )->then ($ addCors , fn ($ e ) => $ addCors ($ this ->handleRequestError ($ e , $ request ))),
176
- 'DELETE ' => $ this ->handleDeleteRequest ($ request )->then ($ addCors , fn ($ e ) => $ addCors ($ this ->handleRequestError ($ e , $ request ))),
175
+ 'GET ' => $ this ->handleGetRequest ($ request )->then ($ addCors , fn ($ e ) => $ addCors ($ this ->handleRequestError ($ e , $ request ))),
176
+ 'POST ' => $ this ->handlePostRequest ($ request )->then ($ addCors , fn ($ e ) => $ addCors ($ this ->handleRequestError ($ e , $ request ))),
177
+ 'DELETE ' => $ this ->handleDeleteRequest ($ request )->then ($ addCors , fn ($ e ) => $ addCors ($ this ->handleRequestError ($ e , $ request ))),
177
178
default => $ addCors ($ this ->handleUnsupportedRequest ($ request )),
178
179
};
179
180
} catch (Throwable $ e ) {
@@ -184,6 +185,11 @@ private function createRequestHandler(): callable
184
185
185
186
private function handleGetRequest (ServerRequestInterface $ request ): PromiseInterface
186
187
{
188
+ if ($ this ->stateless ) {
189
+ $ error = Error::forInvalidRequest ("GET requests (SSE streaming) are not supported in stateless mode. " );
190
+ return resolve (new HttpResponse (405 , ['Content-Type ' => 'application/json ' ], json_encode ($ error )));
191
+ }
192
+
187
193
$ acceptHeader = $ request ->getHeaderLine ('Accept ' );
188
194
if (!str_contains ($ acceptHeader , 'text/event-stream ' )) {
189
195
$ error = Error::forInvalidRequest ("Not Acceptable: Client must accept text/event-stream for GET requests. " );
@@ -264,24 +270,29 @@ private function handlePostRequest(ServerRequestInterface $request): PromiseInte
264
270
$ isInitializeRequest = ($ message instanceof Request && $ message ->method === 'initialize ' );
265
271
$ sessionId = null ;
266
272
267
- if ($ isInitializeRequest ) {
268
- if ($ request ->hasHeader ('Mcp-Session-Id ' )) {
269
- $ this ->logger ->warning ("Client sent Mcp-Session-Id with InitializeRequest. Ignoring. " , ['clientSentId ' => $ request ->getHeaderLine ('Mcp-Session-Id ' )]);
270
- $ error = Error::forInvalidRequest ("Invalid request: Session already initialized. Mcp-Session-Id header not allowed with InitializeRequest. " , $ message ->getId ());
271
- $ deferred ->resolve (new HttpResponse (400 , ['Content-Type ' => 'application/json ' ], json_encode ($ error )));
272
- return $ deferred ->promise ();
273
- }
274
-
273
+ if ($ this ->stateless ) {
275
274
$ sessionId = $ this ->generateId ();
276
275
$ this ->emit ('client_connected ' , [$ sessionId ]);
277
276
} else {
278
- $ sessionId = $ request ->getHeaderLine ('Mcp-Session-Id ' );
277
+ if ($ isInitializeRequest ) {
278
+ if ($ request ->hasHeader ('Mcp-Session-Id ' )) {
279
+ $ this ->logger ->warning ("Client sent Mcp-Session-Id with InitializeRequest. Ignoring. " , ['clientSentId ' => $ request ->getHeaderLine ('Mcp-Session-Id ' )]);
280
+ $ error = Error::forInvalidRequest ("Invalid request: Session already initialized. Mcp-Session-Id header not allowed with InitializeRequest. " , $ message ->getId ());
281
+ $ deferred ->resolve (new HttpResponse (400 , ['Content-Type ' => 'application/json ' ], json_encode ($ error )));
282
+ return $ deferred ->promise ();
283
+ }
284
+
285
+ $ sessionId = $ this ->generateId ();
286
+ $ this ->emit ('client_connected ' , [$ sessionId ]);
287
+ } else {
288
+ $ sessionId = $ request ->getHeaderLine ('Mcp-Session-Id ' );
279
289
280
- if (empty ($ sessionId )) {
281
- $ this ->logger ->warning ("POST request without Mcp-Session-Id. " );
282
- $ error = Error::forInvalidRequest ("Mcp-Session-Id header required for POST requests. " , $ message ->getId ());
283
- $ deferred ->resolve (new HttpResponse (400 , ['Content-Type ' => 'application/json ' ], json_encode ($ error )));
284
- return $ deferred ->promise ();
290
+ if (empty ($ sessionId )) {
291
+ $ this ->logger ->warning ("POST request without Mcp-Session-Id. " );
292
+ $ error = Error::forInvalidRequest ("Mcp-Session-Id header required for POST requests. " , $ message ->getId ());
293
+ $ deferred ->resolve (new HttpResponse (400 , ['Content-Type ' => 'application/json ' ], json_encode ($ error )));
294
+ return $ deferred ->promise ();
295
+ }
285
296
}
286
297
}
287
298
@@ -344,7 +355,7 @@ private function handlePostRequest(ServerRequestInterface $request): PromiseInte
344
355
'X-Accel-Buffering ' => 'no ' ,
345
356
];
346
357
347
- if (!empty ($ sessionId )) {
358
+ if (!empty ($ sessionId ) && ! $ this -> stateless ) {
348
359
$ headers ['Mcp-Session-Id ' ] = $ sessionId ;
349
360
}
350
361
@@ -355,6 +366,8 @@ private function handlePostRequest(ServerRequestInterface $request): PromiseInte
355
366
}
356
367
}
357
368
369
+ $ context ['stateless ' ] = $ this ->stateless ;
370
+
358
371
$ this ->loop ->futureTick (function () use ($ message , $ sessionId , $ context ) {
359
372
$ this ->emit ('message ' , [$ message , $ sessionId , $ context ]);
360
373
});
@@ -364,6 +377,10 @@ private function handlePostRequest(ServerRequestInterface $request): PromiseInte
364
377
365
378
private function handleDeleteRequest (ServerRequestInterface $ request ): PromiseInterface
366
379
{
380
+ if ($ this ->stateless ) {
381
+ return resolve (new HttpResponse (204 ));
382
+ }
383
+
367
384
$ sessionId = $ request ->getHeaderLine ('Mcp-Session-Id ' );
368
385
if (empty ($ sessionId )) {
369
386
$ this ->logger ->warning ("DELETE request without Mcp-Session-Id. " );
@@ -466,6 +483,12 @@ public function sendMessage(Message $message, string $sessionId, array $context
466
483
if ($ this ->activeSseStreams [$ streamId ]['context ' ]['nResponses ' ] >= $ this ->activeSseStreams [$ streamId ]['context ' ]['nRequests ' ]) {
467
484
$ this ->logger ->info ("All expected responses sent for POST SSE stream. Closing. " , ['streamId ' => $ streamId , 'sessionId ' => $ sessionId ]);
468
485
$ stream ->end (); // Will trigger 'close' event.
486
+
487
+ if ($ context ['stateless ' ] ?? false ) {
488
+ $ this ->loop ->futureTick (function () use ($ sessionId ) {
489
+ $ this ->emit ('client_disconnected ' , [$ sessionId , 'Stateless request completed ' ]);
490
+ });
491
+ }
469
492
}
470
493
}
471
494
@@ -483,12 +506,19 @@ public function sendMessage(Message $message, string $sessionId, array $context
483
506
484
507
$ responseBody = json_encode ($ message , JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
485
508
$ headers = ['Content-Type ' => 'application/json ' ];
486
- if ($ isInitializeResponse ) {
509
+ if ($ isInitializeResponse && ! $ this -> stateless ) {
487
510
$ headers ['Mcp-Session-Id ' ] = $ sessionId ;
488
511
}
489
512
490
513
$ statusCode = $ context ['status_code ' ] ?? 200 ;
491
514
$ deferred ->resolve (new HttpResponse ($ statusCode , $ headers , $ responseBody . "\n" ));
515
+
516
+ if ($ context ['stateless ' ] ?? false ) {
517
+ $ this ->loop ->futureTick (function () use ($ sessionId ) {
518
+ $ this ->emit ('client_disconnected ' , [$ sessionId , 'Stateless request completed ' ]);
519
+ });
520
+ }
521
+
492
522
return resolve (null );
493
523
494
524
default :
0 commit comments