40
40
41
41
42
42
class 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
+
43
58
def __init__ (
44
59
self ,
45
60
request_id : RequestId ,
@@ -55,19 +70,36 @@ def __init__(
55
70
self ._completed = False
56
71
self ._cancel_scope = anyio .CancelScope ()
57
72
self ._on_complete = on_complete
73
+ self ._entered = False # Track if we're in a context manager
58
74
59
75
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 ()
60
79
self ._cancel_scope .__enter__ ()
61
80
return self
62
81
63
82
def __exit__ (self , exc_type , exc_val , exc_tb ) -> None :
83
+ """Exit the context manager, performing cleanup and notifying completion."""
64
84
try :
65
85
if self ._completed :
66
86
self ._on_complete (self )
67
87
finally :
88
+ self ._entered = False
89
+ if not self ._cancel_scope :
90
+ raise RuntimeError ("No active cancel scope" )
68
91
self ._cancel_scope .__exit__ (exc_type , exc_val , exc_tb )
69
92
70
93
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" )
71
103
assert not self ._completed , "Request already responded to"
72
104
73
105
if not self .cancelled :
@@ -79,6 +111,11 @@ async def respond(self, response: SendResultT | ErrorData) -> None:
79
111
80
112
async def cancel (self ) -> None :
81
113
"""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
+
82
119
self ._cancel_scope .cancel ()
83
120
self ._completed = True # Mark as completed so it's removed from in_flight
84
121
# Send an error response to indicate cancellation
0 commit comments