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 MessageMetadata , ServerMessageMetadata , SessionMessage
48- from mcp .shared .transport_context import TransportContext
47+ from mcp .shared .message import ServerMessageMetadata , SessionMessage
4948
5049logger = logging .getLogger (__name__ )
5150
@@ -176,9 +175,9 @@ def __init__(
176175 Must contain only visible ASCII characters (0x21-0x7E).
177176 is_json_response_enabled: If True, answer each request POST with a single
178177 JSON body instead of an SSE stream. The body carries
179- only the response, so `transport_context_for` reports
180- `can_send_request=False` for it: a server-initiated
181- request on the request-scoped channel raises
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
182181 `NoBackChannelError` and request-scoped notifications
183182 are dropped. Default is False.
184183 event_store: Event store for resumability support. If provided,
@@ -218,24 +217,14 @@ def is_terminated(self) -> bool:
218217 """Check if this transport has been explicitly terminated."""
219218 return self ._terminated
220219
221- def transport_context_for (self , metadata : MessageMetadata ) -> TransportContext :
222- """Build the `TransportContext` for an inbound message this transport delivered.
223-
224- The transport owns what a request's response can carry, so it supplies
225- the loop dispatcher's `transport_builder`. A JSON body holds exactly one
226- JSON-RPC response, so in JSON-response mode the request-scoped channel has
227- no room for a server-initiated request and `can_send_request` is `False`:
228- `ctx.elicit()` raises `NoBackChannelError` at once instead of parking a
229- waiter no reply can reach. An SSE response streams whatever the handler
230- emits. The connection's standalone GET stream is a separate channel and
231- is not described here.
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) nor with no session (the client's reply would have no session to land on);
225+ stamped on each message's `ServerMessageMetadata` so any dispatcher's builder reads it.
232226 """
233- request = metadata .request_context if isinstance (metadata , ServerMessageMetadata ) else None
234- return TransportContext (
235- kind = "streamable-http" ,
236- can_send_request = not self .is_json_response_enabled ,
237- headers = request .headers if isinstance (request , Request ) else None ,
238- )
227+ return self .mcp_session_id is not None and not self .is_json_response_enabled
239228
240229 def close_sse_stream (self , request_id : RequestId ) -> None :
241230 """Close SSE connection for a specific request without terminating the stream.
@@ -312,9 +301,14 @@ async def close_standalone_stream_callback() -> None:
312301 close_sse_stream = close_stream_callback ,
313302 close_standalone_sse_stream = close_standalone_stream_callback ,
314303 on_request_unanswered = end_stream ,
304+ can_send_request = self ._request_channel_can_send_request ,
315305 )
316306 else :
317- metadata = ServerMessageMetadata (request_context = request , on_request_unanswered = end_stream )
307+ metadata = ServerMessageMetadata (
308+ request_context = request ,
309+ on_request_unanswered = end_stream ,
310+ can_send_request = self ._request_channel_can_send_request ,
311+ )
318312
319313 return SessionMessage (message , metadata = metadata )
320314
@@ -582,7 +576,9 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re
582576 await response (scope , receive , send )
583577
584578 # Process the message after sending the response
585- metadata = ServerMessageMetadata (request_context = request )
579+ metadata = ServerMessageMetadata (
580+ request_context = request , can_send_request = self ._request_channel_can_send_request
581+ )
586582 session_message = SessionMessage (message , metadata = metadata )
587583 await writer .send (session_message )
588584
@@ -608,6 +604,7 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re
608604 metadata = ServerMessageMetadata (
609605 request_context = request ,
610606 on_request_unanswered = partial (self ._terminate_unanswered_request , message .id ),
607+ can_send_request = self ._request_channel_can_send_request ,
611608 )
612609 session_message = SessionMessage (message , metadata = metadata )
613610 await writer .send (session_message )
@@ -992,12 +989,6 @@ async def connect(
992989 ]:
993990 """Context manager that provides read and write streams for a connection.
994991
995- Drive the yielded streams through a `JSONRPCDispatcher` built with
996- `transport_context_for` as its `transport_builder`, as the session
997- manager does: that is what makes the request-scoped channel refuse
998- server-initiated requests in JSON-response mode instead of parking
999- them where nothing can deliver them.
1000-
1001992 Yields:
1002993 Tuple of (read_stream, write_stream) for bidirectional communication
1003994 """
0 commit comments