4444from mcp .shared ._context_streams import ContextReceiveStream , ContextSendStream , create_context_streams
4545from mcp .shared ._stream_protocols import ReadStream , WriteStream
4646from mcp .shared .inbound import MCP_PROTOCOL_VERSION_HEADER
47- from mcp .shared .message import ServerMessageMetadata , SessionMessage
47+ from mcp .shared .message import CloseSSEStreamCallback , ServerMessageMetadata , SessionMessage
4848
4949logger = logging .getLogger (__name__ )
5050
@@ -174,12 +174,11 @@ def __init__(
174174 mcp_session_id: Optional session identifier for this connection.
175175 Must contain only visible ASCII characters (0x21-0x7E).
176176 is_json_response_enabled: If True, answer each request POST with a single
177- JSON body instead of an SSE stream. The body carries
178- only the response, so this transport marks its messages
179- as having no request-scoped back-channel: a
180- server-initiated request on that channel raises
181- `NoBackChannelError` and request-scoped notifications
182- are dropped. Default is False.
177+ JSON body instead of an SSE stream, which removes
178+ the request-scoped back-channel: a server-initiated
179+ request tied to the call raises `NoBackChannelError`
180+ and its notifications are dropped (see
181+ `TransportContext.can_send_request`). Default is False.
183182 event_store: Event store for resumability support. If provided,
184183 resumability will be enabled, allowing clients to
185184 reconnect and resume messages.
@@ -217,15 +216,28 @@ def is_terminated(self) -> bool:
217216 """Check if this transport has been explicitly terminated."""
218217 return self ._terminated
219218
220- @property
221- def _request_channel_can_send_request (self ) -> bool :
222- """Whether the request-scoped channel of a message this transport delivers can carry a
223- server-initiated request. It cannot in JSON-response mode: the POST is answered with one
224- JSON body, which holds only the response. Stamped on each message's
225- `ServerMessageMetadata` so any dispatcher's builder reads it; whether a reply has a session
226- to land on is the session manager's fact, not the transport's, so it is not decided here.
219+ def _message_metadata (
220+ self ,
221+ request : Request ,
222+ * ,
223+ close_sse_stream : CloseSSEStreamCallback | None = None ,
224+ close_standalone_sse_stream : CloseSSEStreamCallback | None = None ,
225+ on_request_unanswered : Callable [[], Awaitable [None ]] | None = None ,
226+ ) -> ServerMessageMetadata :
227+ """The metadata this transport frames every inbound message with.
228+
229+ The one place `can_send_request` is stamped, so no construction site can
230+ forget it: a JSON body carries only the response, so in JSON-response mode
231+ the request-scoped channel cannot carry a server-initiated request (see
232+ `TransportContext.can_send_request`).
227233 """
228- return not self .is_json_response_enabled
234+ return ServerMessageMetadata (
235+ request_context = request ,
236+ close_sse_stream = close_sse_stream ,
237+ close_standalone_sse_stream = close_standalone_sse_stream ,
238+ on_request_unanswered = on_request_unanswered ,
239+ can_send_request = not self .is_json_response_enabled ,
240+ )
229241
230242 def close_sse_stream (self , request_id : RequestId ) -> None :
231243 """Close SSE connection for a specific request without terminating the stream.
@@ -297,19 +309,14 @@ async def close_stream_callback() -> None:
297309 async def close_standalone_stream_callback () -> None :
298310 self .close_standalone_sse_stream ()
299311
300- metadata = ServerMessageMetadata (
301- request_context = request ,
312+ metadata = self . _message_metadata (
313+ request ,
302314 close_sse_stream = close_stream_callback ,
303315 close_standalone_sse_stream = close_standalone_stream_callback ,
304316 on_request_unanswered = end_stream ,
305- can_send_request = self ._request_channel_can_send_request ,
306317 )
307318 else :
308- metadata = ServerMessageMetadata (
309- request_context = request ,
310- on_request_unanswered = end_stream ,
311- can_send_request = self ._request_channel_can_send_request ,
312- )
319+ metadata = self ._message_metadata (request , on_request_unanswered = end_stream )
313320
314321 return SessionMessage (message , metadata = metadata )
315322
@@ -577,10 +584,7 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re
577584 await response (scope , receive , send )
578585
579586 # Process the message after sending the response
580- metadata = ServerMessageMetadata (
581- request_context = request , can_send_request = self ._request_channel_can_send_request
582- )
583- session_message = SessionMessage (message , metadata = metadata )
587+ session_message = SessionMessage (message , metadata = self ._message_metadata (request ))
584588 await writer .send (session_message )
585589
586590 return
@@ -602,10 +606,8 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re
602606 )
603607 request_stream_reader = self ._request_streams [request_id ][1 ]
604608 # Process the message
605- metadata = ServerMessageMetadata (
606- request_context = request ,
607- on_request_unanswered = partial (self ._terminate_unanswered_request , message .id ),
608- can_send_request = self ._request_channel_can_send_request ,
609+ metadata = self ._message_metadata (
610+ request , on_request_unanswered = partial (self ._terminate_unanswered_request , message .id )
609611 )
610612 session_message = SessionMessage (message , metadata = metadata )
611613 await writer .send (session_message )
@@ -1030,15 +1032,10 @@ async def message_router():
10301032 ):
10311033 related_request_id = session_message .metadata .related_request_id
10321034 if self .is_json_response_enabled :
1033- # A JSON body carries exactly the one response: a
1034- # message that only rides this request's stream
1035- # has no wire form (and no POST could replay it),
1036- # so drop it before storing or queueing - never
1037- # park it in a queue nothing drains (#1764).
1038- logger .debug (
1039- f"Dropped message related to request { related_request_id } : "
1040- "a JSON response carries only its own response"
1041- )
1035+ # A JSON body carries only the response: this message
1036+ # has no wire form (nor a replay), so drop it before
1037+ # storing or queueing rather than park it (#1764).
1038+ logger .debug (f"Dropped message related to request { related_request_id } in JSON mode" )
10421039 continue
10431040 target_request_id = str (related_request_id )
10441041
0 commit comments