4040
4141
4242class RequestResponder (Generic [ReceiveRequestT , SendResultT ]):
43+ """Handles responding to MCP requests and manages request lifecycle.
44+
45+ This class MUST be used as a context manager to ensure proper cleanup and
46+ cancellation handling:
47+
48+ Example:
49+ with request_responder as resp:
50+ await resp.respond(result)
51+
52+ The context manager ensures:
53+ 1. Proper cancellation scope setup and cleanup
54+ 2. Request completion tracking
55+ 3. Cleanup of in-flight requests
56+ """
57+
4358 def __init__ (
4459 self ,
4560 request_id : RequestId ,
@@ -55,19 +70,36 @@ def __init__(
5570 self ._completed = False
5671 self ._cancel_scope = anyio .CancelScope ()
5772 self ._on_complete = on_complete
73+ self ._entered = False # Track if we're in a context manager
5874
5975 def __enter__ (self ) -> "RequestResponder[ReceiveRequestT, SendResultT]" :
76+ """Enter the context manager, enabling request cancellation tracking."""
77+ self ._entered = True
78+ self ._cancel_scope = anyio .CancelScope ()
6079 self ._cancel_scope .__enter__ ()
6180 return self
6281
6382 def __exit__ (self , exc_type , exc_val , exc_tb ) -> None :
83+ """Exit the context manager, performing cleanup and notifying completion."""
6484 try :
6585 if self ._completed :
6686 self ._on_complete (self )
6787 finally :
88+ self ._entered = False
89+ if not self ._cancel_scope :
90+ raise RuntimeError ("No active cancel scope" )
6891 self ._cancel_scope .__exit__ (exc_type , exc_val , exc_tb )
6992
7093 async def respond (self , response : SendResultT | ErrorData ) -> None :
94+ """Send a response for this request.
95+
96+ Must be called within a context manager block.
97+ Raises:
98+ RuntimeError: If not used within a context manager
99+ AssertionError: If request was already responded to
100+ """
101+ if not self ._entered :
102+ raise RuntimeError ("RequestResponder must be used as a context manager" )
71103 assert not self ._completed , "Request already responded to"
72104
73105 if not self .cancelled :
@@ -79,6 +111,11 @@ async def respond(self, response: SendResultT | ErrorData) -> None:
79111
80112 async def cancel (self ) -> None :
81113 """Cancel this request and mark it as completed."""
114+ if not self ._entered :
115+ raise RuntimeError ("RequestResponder must be used as a context manager" )
116+ if not self ._cancel_scope :
117+ raise RuntimeError ("No active cancel scope" )
118+
82119 self ._cancel_scope .cancel ()
83120 self ._completed = True # Mark as completed so it's removed from in_flight
84121 # Send an error response to indicate cancellation
0 commit comments