1212from collections .abc import AsyncGenerator , Awaitable , Callable
1313from contextlib import asynccontextmanager
1414from dataclasses import dataclass
15+ from functools import partial
1516from http import HTTPStatus
1617from typing import Any , Final
1718
7172# Type aliases
7273StreamId = str
7374EventId = str
75+ # An SSE event-dict as accepted by sse-starlette (`event`, `data`, `id`, `retry`).
76+ SSEEvent = dict [str , Any ]
7477
7578
7679@dataclass
@@ -174,7 +177,7 @@ def __init__(
174177 MemoryObjectReceiveStream [EventMessage ],
175178 ],
176179 ] = {}
177- self ._sse_stream_writers : dict [RequestId , MemoryObjectSendStream [dict [ str , str ] ]] = {}
180+ self ._sse_stream_writers : dict [RequestId , MemoryObjectSendStream [SSEEvent ]] = {}
178181 self ._terminated = False
179182 # Idle timeout cancel scope; managed by the session manager.
180183 self .idle_scope : anyio .CancelScope | None = None
@@ -261,31 +264,48 @@ async def close_standalone_stream_callback() -> None:
261264
262265 return SessionMessage (message , metadata = metadata )
263266
264- async def _maybe_send_priming_event (
265- self ,
266- request_id : RequestId ,
267- sse_stream_writer : MemoryObjectSendStream [dict [str , Any ]],
268- protocol_version : str ,
269- ) -> None :
270- """Send priming event for SSE resumability if event_store is configured.
267+ async def _mint_priming_event (self , stream_id : StreamId , protocol_version : str ) -> SSEEvent | None :
268+ """Store the priming cursor for `stream_id` and return its SSE wire form.
271269
272- Only sends priming events to clients with protocol version >= 2025-11-25,
273- which includes the fix for handling empty SSE data. Older clients would
274- crash trying to parse empty data as JSON.
270+ Called before the request is dispatched so the priming row precedes
271+ anything `message_router` can store for this stream. Returns `None`
272+ when no event store is configured or the client predates 2025-11-25
273+ (older clients cannot parse the empty-data event).
275274 """
276275 if not self ._event_store :
277- return
278- # Priming events have empty data which older clients cannot handle.
276+ return None
279277 if not is_version_at_least (protocol_version , "2025-11-25" ):
280- return
281- priming_event_id = await self ._event_store .store_event (
282- str (request_id ), # Convert RequestId to StreamId (str)
283- None , # Priming event has no payload
284- )
285- priming_event : dict [str , str | int ] = {"id" : priming_event_id , "data" : "" }
278+ return None
279+ priming_event_id = await self ._event_store .store_event (stream_id , None )
280+ priming_event : SSEEvent = {"id" : priming_event_id , "data" : "" }
286281 if self ._retry_interval is not None :
287282 priming_event ["retry" ] = self ._retry_interval
288- await sse_stream_writer .send (priming_event )
283+ return priming_event
284+
285+ async def _run_sse_writer (
286+ self ,
287+ request_id : RequestId ,
288+ sse_stream_writer : MemoryObjectSendStream [SSEEvent ],
289+ request_stream_reader : MemoryObjectReceiveStream [EventMessage ],
290+ priming_event : SSEEvent | None ,
291+ ) -> None :
292+ """Forward `_request_streams[request_id]` onto the SSE wire for one POST."""
293+ try :
294+ async with sse_stream_writer , request_stream_reader :
295+ if priming_event is not None :
296+ await sse_stream_writer .send (priming_event )
297+ async for event_message in request_stream_reader :
298+ await sse_stream_writer .send (self ._create_event_data (event_message ))
299+ if isinstance (event_message .message , JSONRPCResponse | JSONRPCError ):
300+ break
301+ except anyio .ClosedResourceError : # pragma: lax no cover
302+ logger .debug ("SSE stream closed by close_sse_stream()" )
303+ except Exception : # pragma: lax no cover
304+ logger .exception ("Error in SSE writer" )
305+ finally :
306+ logger .debug ("Closing SSE writer" )
307+ self ._sse_stream_writers .pop (request_id , None )
308+ await self ._clean_up_memory_streams (request_id )
289309
290310 def _create_error_response (
291311 self ,
@@ -339,7 +359,7 @@ def _get_session_id(self, request: Request) -> str | None:
339359 """Extract the session ID from request headers."""
340360 return request .headers .get (MCP_SESSION_ID_HEADER )
341361
342- def _create_event_data (self , event_message : EventMessage ) -> dict [ str , str ] :
362+ def _create_event_data (self , event_message : EventMessage ) -> SSEEvent :
343363 """Create event data dictionary from an EventMessage."""
344364 event_data = {
345365 "event" : "message" ,
@@ -579,40 +599,16 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re
579599 await self ._clean_up_memory_streams (request_id )
580600 else :
581601 # Create SSE stream
582- sse_stream_writer , sse_stream_reader = anyio .create_memory_object_stream [dict [ str , str ] ](0 )
602+ sse_stream_writer , sse_stream_reader = anyio .create_memory_object_stream [SSEEvent ](0 )
583603
584604 # Store writer reference so close_sse_stream() can close it
585605 self ._sse_stream_writers [request_id ] = sse_stream_writer
586606
587- async def sse_writer ():
588- # Get the request ID from the incoming request message
589- try :
590- async with sse_stream_writer , request_stream_reader :
591- # Send priming event for SSE resumability
592- await self ._maybe_send_priming_event (request_id , sse_stream_writer , protocol_version )
593-
594- # Process messages from the request-specific stream
595- async for event_message in request_stream_reader :
596- # Build the event data
597- event_data = self ._create_event_data (event_message )
598- await sse_stream_writer .send (event_data )
599-
600- # If response, remove from pending streams and close
601- if isinstance (event_message .message , JSONRPCResponse | JSONRPCError ):
602- break
603- except anyio .ClosedResourceError : # pragma: lax no cover
604- # Expected when close_sse_stream() is called
605- logger .debug ("SSE stream closed by close_sse_stream()" )
606- except Exception : # pragma: lax no cover
607- logger .exception ("Error in SSE writer" )
608- finally :
609- logger .debug ("Closing SSE writer" )
610- self ._sse_stream_writers .pop (request_id , None )
611- await self ._clean_up_memory_streams (request_id )
612-
613- # Create and start EventSourceResponse
614- # SSE stream mode (original behavior)
615- # Set up headers
607+ # Store the priming event before the request is dispatched so its
608+ # event-store position precedes anything message_router can store
609+ # for this id (storage order == wire order by construction).
610+ priming_event = await self ._mint_priming_event (request_id , protocol_version )
611+
616612 headers = {
617613 "Cache-Control" : "no-cache, no-transform" ,
618614 "Connection" : "keep-alive" ,
@@ -621,7 +617,9 @@ async def sse_writer():
621617 }
622618 response = EventSourceResponse (
623619 content = sse_stream_reader ,
624- data_sender_callable = sse_writer ,
620+ data_sender_callable = partial (
621+ self ._run_sse_writer , request_id , sse_stream_writer , request_stream_reader , priming_event
622+ ),
625623 headers = headers ,
626624 )
627625
@@ -704,7 +702,7 @@ async def _handle_get_request(self, request: Request, send: Send) -> None:
704702 return
705703
706704 # Create SSE stream
707- sse_stream_writer , sse_stream_reader = anyio .create_memory_object_stream [dict [ str , str ] ](0 )
705+ sse_stream_writer , sse_stream_reader = anyio .create_memory_object_stream [SSEEvent ](0 )
708706
709707 async def standalone_sse_writer ():
710708 try :
@@ -880,7 +878,7 @@ async def _replay_events(self, last_event_id: str, request: Request, send: Send)
880878 replay_protocol_version = request .headers .get (MCP_PROTOCOL_VERSION_HEADER , DEFAULT_NEGOTIATED_VERSION )
881879
882880 # Create SSE stream for replay
883- sse_stream_writer , sse_stream_reader = anyio .create_memory_object_stream [dict [ str , str ] ](0 )
881+ sse_stream_writer , sse_stream_reader = anyio .create_memory_object_stream [SSEEvent ](0 )
884882
885883 async def replay_sender ():
886884 try :
@@ -898,8 +896,12 @@ async def send_event(event_message: EventMessage) -> None:
898896 # Register SSE writer so close_sse_stream() can close it
899897 self ._sse_stream_writers [stream_id ] = sse_stream_writer
900898
901- # Send priming event for this new connection
902- await self ._maybe_send_priming_event (stream_id , sse_stream_writer , replay_protocol_version )
899+ # Prime the resumed connection so the client sees the stream
900+ # is re-registered. The replay→live-tail ordering window here
901+ # is pre-existing and tracked separately.
902+ priming_event = await self ._mint_priming_event (stream_id , replay_protocol_version )
903+ if priming_event is not None : # pragma: no branch
904+ await sse_stream_writer .send (priming_event )
903905
904906 # Create new request streams for this connection
905907 self ._request_streams [stream_id ] = anyio .create_memory_object_stream [EventMessage ](
0 commit comments