2525use Mcp \Schema \JsonRpc \HasMethodInterface ;
2626use Mcp \Schema \JsonRpc \Response ;
2727use Mcp \Server \MethodHandlerInterface ;
28+ use Mcp \Server \Session \SessionInterface ;
2829use Mcp \Server \NotificationHandler ;
2930use Mcp \Server \RequestHandler ;
31+ use Mcp \Server \Session \SessionFactoryInterface ;
32+ use Mcp \Server \Session \SessionStoreInterface ;
33+ use Mcp \Schema \Request \InitializeRequest ;
3034use Symfony \Component \Uid \Uuid ;
3135use Psr \Log \LoggerInterface ;
3236use Psr \Log \NullLogger ;
@@ -48,6 +52,8 @@ class Handler
4852 */
4953 public function __construct (
5054 private readonly MessageFactory $ messageFactory ,
55+ private readonly SessionFactoryInterface $ sessionFactory ,
56+ private readonly SessionStoreInterface $ sessionStore ,
5157 iterable $ methodHandlers ,
5258 private readonly LoggerInterface $ logger = new NullLogger (),
5359 ) {
@@ -63,10 +69,14 @@ public static function make(
6369 ToolCallerInterface $ toolCaller ,
6470 ResourceReaderInterface $ resourceReader ,
6571 PromptGetterInterface $ promptGetter ,
72+ SessionStoreInterface $ sessionStore ,
73+ SessionFactoryInterface $ sessionFactory ,
6674 LoggerInterface $ logger = new NullLogger (),
6775 ): self {
6876 return new self (
6977 messageFactory: MessageFactory::make (),
78+ sessionFactory: $ sessionFactory ,
79+ sessionStore: $ sessionStore ,
7080 methodHandlers: [
7181 new NotificationHandler \InitializedHandler (),
7282 new RequestHandler \InitializeHandler ($ registry ->getCapabilities (), $ implementation ),
@@ -83,7 +93,7 @@ public static function make(
8393 }
8494
8595 /**
86- * @return iterable<string|null>
96+ * @return iterable<array{ string|null, array<string, mixed>} >
8797 *
8898 * @throws ExceptionInterface When a handler throws an exception during message processing
8999 * @throws \JsonException When JSON encoding of the response fails
@@ -92,20 +102,65 @@ public function process(string $input, ?Uuid $sessionId): iterable
92102 {
93103 $ this ->logger ->info ('Received message to process. ' , ['message ' => $ input ]);
94104
105+ $ this ->runGarbageCollection ();
106+
95107 try {
96- $ messages = $ this ->messageFactory ->create ($ input );
108+ $ messages = iterator_to_array ( $ this ->messageFactory ->create ($ input) );
97109 } catch (\JsonException $ e ) {
98110 $ this ->logger ->warning ('Failed to decode json message. ' , ['exception ' => $ e ]);
99-
100- yield $ this ->encodeResponse (Error:: forParseError ( $ e -> getMessage ())) ;
111+ $ error = Error:: forParseError ( $ e -> getMessage ());
112+ yield [ $ this ->encodeResponse ($ error ), []] ;
101113
102114 return ;
103115 }
104116
117+ $ hasInitializeRequest = false ;
118+ foreach ($ messages as $ message ) {
119+ if ($ message instanceof InitializeRequest) {
120+ $ hasInitializeRequest = true ;
121+ break ;
122+ }
123+ }
124+
125+ $ session = null ;
126+
127+ if ($ hasInitializeRequest ) {
128+ // Spec: An initialize request must not be part of a batch.
129+ if (count ($ messages ) > 1 ) {
130+ $ error = Error::forInvalidRequest ('The "initialize" request MUST NOT be part of a batch. ' );
131+ yield [$ this ->encodeResponse ($ error ), []];
132+ return ;
133+ }
134+
135+ // Spec: An initialize request must not have a session ID.
136+ if ($ sessionId ) {
137+ $ error = Error::forInvalidRequest ('A session ID MUST NOT be sent with an "initialize" request. ' );
138+ yield [$ this ->encodeResponse ($ error ), []];
139+ return ;
140+ }
141+
142+ $ session = $ this ->sessionFactory ->create ($ this ->sessionStore );
143+ } else {
144+ if (!$ sessionId ) {
145+ $ error = Error::forInvalidRequest ('A valid session id is REQUIRED for non-initialize requests. ' );
146+ yield [$ this ->encodeResponse ($ error ), ['status_code ' => 400 ]];
147+ return ;
148+ }
149+
150+ if (!$ this ->sessionStore ->exists ($ sessionId )) {
151+ $ error = Error::forInvalidRequest ('Session not found or has expired. ' );
152+ yield [$ this ->encodeResponse ($ error ), ['status_code ' => 404 ]];
153+ return ;
154+ }
155+
156+ $ session = $ this ->sessionFactory ->createWithId ($ sessionId , $ this ->sessionStore );
157+ }
158+
105159 foreach ($ messages as $ message ) {
106160 if ($ message instanceof InvalidInputMessageException) {
107161 $ this ->logger ->warning ('Failed to create message. ' , ['exception ' => $ message ]);
108- yield $ this ->encodeResponse (Error::forInvalidRequest ($ message ->getMessage (), 0 ));
162+ $ error = Error::forInvalidRequest ($ message ->getMessage (), 0 );
163+ yield [$ this ->encodeResponse ($ error ), []];
109164 continue ;
110165 }
111166
@@ -114,26 +169,32 @@ public function process(string $input, ?Uuid $sessionId): iterable
114169 ]);
115170
116171 try {
117- yield $ this ->encodeResponse ($ this ->handle ($ message ));
172+ $ response = $ this ->encodeResponse ($ this ->handle ($ message , $ session ));
173+ yield [$ response , ['session_id ' => $ session ->getId ()]];
118174 } catch (\DomainException ) {
119- yield null ;
175+ yield [ null , []] ;
120176 } catch (NotFoundExceptionInterface $ e ) {
121177 $ this ->logger ->warning (
122178 \sprintf ('Failed to create response: %s ' , $ e ->getMessage ()),
123179 ['exception ' => $ e ],
124180 );
125181
126- yield $ this ->encodeResponse (Error::forMethodNotFound ($ e ->getMessage ()));
182+ $ error = Error::forMethodNotFound ($ e ->getMessage ());
183+ yield [$ this ->encodeResponse ($ error ), []];
127184 } catch (\InvalidArgumentException $ e ) {
128185 $ this ->logger ->warning (\sprintf ('Invalid argument: %s ' , $ e ->getMessage ()), ['exception ' => $ e ]);
129186
130- yield $ this ->encodeResponse (Error::forInvalidParams ($ e ->getMessage ()));
187+ $ error = Error::forInvalidParams ($ e ->getMessage ());
188+ yield [$ this ->encodeResponse ($ error ), []];
131189 } catch (\Throwable $ e ) {
132190 $ this ->logger ->critical (\sprintf ('Uncaught exception: %s ' , $ e ->getMessage ()), ['exception ' => $ e ]);
133191
134- yield $ this ->encodeResponse (Error::forInternalError ($ e ->getMessage ()));
192+ $ error = Error::forInternalError ($ e ->getMessage ());
193+ yield [$ this ->encodeResponse ($ error ), []];
135194 }
136195 }
196+
197+ $ session ->save ();
137198 }
138199
139200 /**
@@ -162,26 +223,28 @@ private function encodeResponse(Response|Error|null $response): ?string
162223 * @throws NotFoundExceptionInterface When no handler is found for the request method
163224 * @throws ExceptionInterface When a request handler throws an exception
164225 */
165- private function handle (HasMethodInterface $ message ): Response |Error |null
226+ private function handle (HasMethodInterface $ message, SessionInterface $ session ): Response |Error |null
166227 {
167228 $ this ->logger ->info (\sprintf ('Handling message for method "%s". ' , $ message ::getMethod ()), [
168229 'message ' => $ message ,
169230 ]);
170231
171232 $ handled = false ;
172233 foreach ($ this ->methodHandlers as $ handler ) {
173- if ($ handler ->supports ($ message )) {
174- $ return = $ handler ->handle ($ message );
175- $ handled = true ;
176-
177- $ this ->logger ->debug (\sprintf ('Message handled by "%s". ' , $ handler ::class), [
178- 'method ' => $ message ::getMethod (),
179- 'response ' => $ return ,
180- ]);
181-
182- if (null !== $ return ) {
183- return $ return ;
184- }
234+ if (!$ handler ->supports ($ message )) {
235+ continue ;
236+ }
237+
238+ $ return = $ handler ->handle ($ message , $ session );
239+ $ handled = true ;
240+
241+ $ this ->logger ->debug (\sprintf ('Message handled by "%s". ' , $ handler ::class), [
242+ 'method ' => $ message ::getMethod (),
243+ 'response ' => $ return ,
244+ ]);
245+
246+ if (null !== $ return ) {
247+ return $ return ;
185248 }
186249 }
187250
@@ -191,4 +254,32 @@ private function handle(HasMethodInterface $message): Response|Error|null
191254
192255 throw new HandlerNotFoundException (\sprintf ('No handler found for method "%s". ' , $ message ::getMethod ()));
193256 }
257+
258+ /**
259+ * Run garbage collection on expired sessions.
260+ * Uses the session store's internal TTL configuration.
261+ */
262+ private function runGarbageCollection (): void
263+ {
264+ if (random_int (0 , 100 ) > 1 ) {
265+ return ;
266+ }
267+
268+ $ deletedSessions = $ this ->sessionStore ->gc ();
269+ if (!empty ($ deletedSessions )) {
270+ $ this ->logger ->debug ('Garbage collected expired sessions. ' , [
271+ 'count ' => count ($ deletedSessions ),
272+ 'session_ids ' => array_map (fn (Uuid $ id ) => $ id ->toRfc4122 (), $ deletedSessions ),
273+ ]);
274+ }
275+ }
276+
277+ /**
278+ * Destroy a specific session.
279+ */
280+ public function destroySession (Uuid $ sessionId ): void
281+ {
282+ $ this ->sessionStore ->destroy ($ sessionId );
283+ $ this ->logger ->info ('Session destroyed. ' , ['session_id ' => $ sessionId ->toRfc4122 ()]);
284+ }
194285}
0 commit comments