@@ -75,6 +75,7 @@ public function __construct(
7575 private string $ mcpPath = '/mcp ' ,
7676 private ?array $ sslContext = null ,
7777 private readonly bool $ enableJsonResponse = true ,
78+ private readonly bool $ stateless = false ,
7879 ?EventStoreInterface $ eventStore = null
7980 ) {
8081 $ this ->logger = new NullLogger ();
@@ -171,9 +172,9 @@ private function createRequestHandler(): callable
171172
172173 try {
173174 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 ))),
177178 default => $ addCors ($ this ->handleUnsupportedRequest ($ request )),
178179 };
179180 } catch (Throwable $ e ) {
@@ -184,6 +185,11 @@ private function createRequestHandler(): callable
184185
185186 private function handleGetRequest (ServerRequestInterface $ request ): PromiseInterface
186187 {
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+
187193 $ acceptHeader = $ request ->getHeaderLine ('Accept ' );
188194 if (!str_contains ($ acceptHeader , 'text/event-stream ' )) {
189195 $ error = Error::forInvalidRequest ("Not Acceptable: Client must accept text/event-stream for GET requests. " );
@@ -264,24 +270,29 @@ private function handlePostRequest(ServerRequestInterface $request): PromiseInte
264270 $ isInitializeRequest = ($ message instanceof Request && $ message ->method === 'initialize ' );
265271 $ sessionId = null ;
266272
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 ) {
275274 $ sessionId = $ this ->generateId ();
276275 $ this ->emit ('client_connected ' , [$ sessionId ]);
277276 } 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 ' );
279289
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+ }
285296 }
286297 }
287298
@@ -344,7 +355,7 @@ private function handlePostRequest(ServerRequestInterface $request): PromiseInte
344355 'X-Accel-Buffering ' => 'no ' ,
345356 ];
346357
347- if (!empty ($ sessionId )) {
358+ if (!empty ($ sessionId ) && ! $ this -> stateless ) {
348359 $ headers ['Mcp-Session-Id ' ] = $ sessionId ;
349360 }
350361
@@ -355,6 +366,8 @@ private function handlePostRequest(ServerRequestInterface $request): PromiseInte
355366 }
356367 }
357368
369+ $ context ['stateless ' ] = $ this ->stateless ;
370+
358371 $ this ->loop ->futureTick (function () use ($ message , $ sessionId , $ context ) {
359372 $ this ->emit ('message ' , [$ message , $ sessionId , $ context ]);
360373 });
@@ -364,6 +377,10 @@ private function handlePostRequest(ServerRequestInterface $request): PromiseInte
364377
365378 private function handleDeleteRequest (ServerRequestInterface $ request ): PromiseInterface
366379 {
380+ if ($ this ->stateless ) {
381+ return resolve (new HttpResponse (204 ));
382+ }
383+
367384 $ sessionId = $ request ->getHeaderLine ('Mcp-Session-Id ' );
368385 if (empty ($ sessionId )) {
369386 $ this ->logger ->warning ("DELETE request without Mcp-Session-Id. " );
@@ -466,6 +483,12 @@ public function sendMessage(Message $message, string $sessionId, array $context
466483 if ($ this ->activeSseStreams [$ streamId ]['context ' ]['nResponses ' ] >= $ this ->activeSseStreams [$ streamId ]['context ' ]['nRequests ' ]) {
467484 $ this ->logger ->info ("All expected responses sent for POST SSE stream. Closing. " , ['streamId ' => $ streamId , 'sessionId ' => $ sessionId ]);
468485 $ 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+ }
469492 }
470493 }
471494
@@ -483,12 +506,19 @@ public function sendMessage(Message $message, string $sessionId, array $context
483506
484507 $ responseBody = json_encode ($ message , JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
485508 $ headers = ['Content-Type ' => 'application/json ' ];
486- if ($ isInitializeResponse ) {
509+ if ($ isInitializeResponse && ! $ this -> stateless ) {
487510 $ headers ['Mcp-Session-Id ' ] = $ sessionId ;
488511 }
489512
490513 $ statusCode = $ context ['status_code ' ] ?? 200 ;
491514 $ 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+
492522 return resolve (null );
493523
494524 default :
0 commit comments