50
50
CONTENT_TYPE_JSON = "application/json"
51
51
CONTENT_TYPE_SSE = "text/event-stream"
52
52
53
+ # Special key for the standalone GET stream
54
+ GET_STREAM_KEY = "_GET_stream"
55
+
53
56
# Session ID validation pattern (visible ASCII characters ranging from 0x21 to 0x7E)
54
57
# Pattern ensures entire string contains only valid characters by using ^ and $ anchors
55
58
SESSION_ID_PATTERN = re .compile (r"^[\x21-\x7E]+$" )
@@ -443,10 +446,19 @@ async def sse_writer():
443
446
return
444
447
445
448
async def _handle_get_request (self , request : Request , send : Send ) -> None :
446
- """Handle GET requests for SSE stream establishment."""
447
- # Validate session ID if server has one
448
- if not await self ._validate_session (request , send ):
449
- return
449
+ """
450
+ Handle GET request to establish SSE.
451
+
452
+ This allows the server to communicate to the client without the client
453
+ first sending data via HTTP POST. The server can send JSON-RPC requests
454
+ and notifications on this stream.
455
+ """
456
+ writer = self ._read_stream_writer
457
+ if writer is None :
458
+ raise ValueError (
459
+ "No read stream writer available. Ensure connect() is called first."
460
+ )
461
+
450
462
# Validate Accept header - must include text/event-stream
451
463
_ , has_sse = self ._check_accept_headers (request )
452
464
@@ -458,13 +470,80 @@ async def _handle_get_request(self, request: Request, send: Send) -> None:
458
470
await response (request .scope , request .receive , send )
459
471
return
460
472
461
- # TODO: Implement SSE stream for GET requests
462
- # For now, return 405 Method Not Allowed
463
- response = self ._create_error_response (
464
- "SSE stream from GET request not implemented yet" ,
465
- HTTPStatus .METHOD_NOT_ALLOWED ,
473
+ if not await self ._validate_session (request , send ):
474
+ return
475
+
476
+ headers = {
477
+ "Cache-Control" : "no-cache, no-transform" ,
478
+ "Connection" : "keep-alive" ,
479
+ "Content-Type" : CONTENT_TYPE_SSE ,
480
+ }
481
+
482
+ if self .mcp_session_id :
483
+ headers [MCP_SESSION_ID_HEADER ] = self .mcp_session_id
484
+
485
+ # Check if we already have an active GET stream
486
+ if GET_STREAM_KEY in self ._request_streams :
487
+ response = self ._create_error_response (
488
+ "Conflict: Only one SSE stream is allowed per session" ,
489
+ HTTPStatus .CONFLICT ,
490
+ )
491
+ await response (request .scope , request .receive , send )
492
+ return
493
+
494
+ # Create SSE stream
495
+ sse_stream_writer , sse_stream_reader = anyio .create_memory_object_stream [
496
+ dict [str , Any ]
497
+ ](0 )
498
+
499
+ async def standalone_sse_writer ():
500
+ try :
501
+ # Create a standalone message stream for server-initiated messages
502
+ standalone_stream_writer , standalone_stream_reader = (
503
+ anyio .create_memory_object_stream [JSONRPCMessage ](0 )
504
+ )
505
+
506
+ # Register this stream using the special key
507
+ self ._request_streams [GET_STREAM_KEY ] = standalone_stream_writer
508
+
509
+ async with sse_stream_writer , standalone_stream_reader :
510
+ # Process messages from the standalone stream
511
+ async for received_message in standalone_stream_reader :
512
+ # For the standalone stream, we handle:
513
+ # - JSONRPCNotification (server sends notifications to client)
514
+ # - JSONRPCRequest (server sends requests to client)
515
+ # We should NOT receive JSONRPCResponse
516
+
517
+ # Send the message via SSE
518
+ event_data = {
519
+ "event" : "message" ,
520
+ "data" : received_message .model_dump_json (
521
+ by_alias = True , exclude_none = True
522
+ ),
523
+ }
524
+
525
+ await sse_stream_writer .send (event_data )
526
+ except Exception as e :
527
+ logger .exception (f"Error in standalone SSE writer: { e } " )
528
+ finally :
529
+ logger .debug ("Closing standalone SSE writer" )
530
+ # Remove the stream from request_streams
531
+ self ._request_streams .pop (GET_STREAM_KEY , None )
532
+
533
+ # Create and start EventSourceResponse
534
+ response = EventSourceResponse (
535
+ content = sse_stream_reader ,
536
+ data_sender_callable = standalone_sse_writer ,
537
+ headers = headers ,
466
538
)
467
- await response (request .scope , request .receive , send )
539
+
540
+ try :
541
+ # This will send headers immediately and establish the SSE connection
542
+ await response (request .scope , request .receive , send )
543
+ except Exception as e :
544
+ logger .exception (f"Error in standalone SSE response: { e } " )
545
+ # Clean up the request stream
546
+ self ._request_streams .pop (GET_STREAM_KEY , None )
468
547
469
548
async def _handle_delete_request (self , request : Request , send : Send ) -> None :
470
549
"""Handle DELETE requests for explicit session termination."""
@@ -611,21 +690,18 @@ async def message_router():
611
690
else :
612
691
target_request_id = str (message .root .id )
613
692
614
- # Send to the specific request stream if available
615
- if (
616
- target_request_id
617
- and target_request_id in self ._request_streams
618
- ):
693
+ request_stream_id = target_request_id or GET_STREAM_KEY
694
+ if request_stream_id in self ._request_streams :
619
695
try :
620
- await self ._request_streams [target_request_id ].send (
696
+ await self ._request_streams [request_stream_id ].send (
621
697
message
622
698
)
623
699
except (
624
700
anyio .BrokenResourceError ,
625
701
anyio .ClosedResourceError ,
626
702
):
627
703
# Stream might be closed, remove from registry
628
- self ._request_streams .pop (target_request_id , None )
704
+ self ._request_streams .pop (request_stream_id , None )
629
705
except Exception as e :
630
706
logger .exception (f"Error in message router: { e } " )
631
707
0 commit comments