Skip to content

Commit 600982e

Browse files
committed
Control async op expiry by resolved_at, not created_at
1 parent e40055a commit 600982e

File tree

3 files changed

+13
-8
lines changed

3 files changed

+13
-8
lines changed

src/mcp/shared/async_operations.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class ClientAsyncOperation:
2525
@property
2626
def is_expired(self) -> bool:
2727
"""Check if operation has expired based on keepAlive."""
28-
return time.time() > (self.created_at + self.keep_alive)
28+
return time.time() > (self.created_at + self.keep_alive * 2) # Give some buffer before expiration
2929

3030

3131
@dataclass
@@ -38,15 +38,18 @@ class ServerAsyncOperation:
3838
status: AsyncOperationStatus
3939
created_at: float
4040
keep_alive: int
41+
resolved_at: float | None = None
4142
session_id: str | None = None
4243
result: types.CallToolResult | None = None
4344
error: str | None = None
4445

4546
@property
4647
def is_expired(self) -> bool:
4748
"""Check if operation has expired based on keepAlive."""
49+
if not self.resolved_at:
50+
return False
4851
if self.status in ("completed", "failed", "canceled"):
49-
return time.time() > (self.created_at + self.keep_alive)
52+
return time.time() > (self.resolved_at + self.keep_alive)
5053
return False
5154

5255
@property
@@ -197,6 +200,7 @@ def complete_operation(self, token: str, result: types.CallToolResult) -> bool:
197200

198201
operation.status = "completed"
199202
operation.result = result
203+
operation.resolved_at = time.time()
200204
return True
201205

202206
def fail_operation(self, token: str, error: str) -> bool:
@@ -211,6 +215,7 @@ def fail_operation(self, token: str, error: str) -> bool:
211215

212216
operation.status = "failed"
213217
operation.error = error
218+
operation.resolved_at = time.time()
214219
return True
215220

216221
def get_operation_result(self, token: str) -> types.CallToolResult | None:

tests/server/test_lowlevel_async_operations.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ async def check_status_handler(token: str) -> types.GetOperationStatusResult:
5555
manager.complete_operation(operation.token, types.CallToolResult(content=[]))
5656

5757
# Make it expired
58-
operation.created_at = time.time() - 2
58+
operation.resolved_at = time.time() - 2
5959

6060
expired_request = types.GetOperationStatusRequest(params=types.GetOperationStatusParams(token=operation.token))
6161

@@ -164,7 +164,7 @@ async def get_result_handler(token: str) -> types.GetOperationPayloadResult:
164164
manager.complete_operation(operation.token, types.CallToolResult(content=[]))
165165

166166
# Make it expired
167-
operation.created_at = time.time() - 2
167+
operation.resolved_at = time.time() - 2
168168

169169
expired_request = types.GetOperationPayloadRequest(
170170
params=types.GetOperationPayloadParams(token=operation.token)

tests/shared/test_async_operations.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ def test_expiration_and_cleanup(self):
178178
# Complete both and make first expired
179179
for op in [short_op, long_op]:
180180
manager.complete_operation(op.token, Mock())
181-
short_op.created_at = time.time() - 2
181+
short_op.resolved_at = time.time() - 2
182182

183183
# Test expiration detection
184184
assert short_op.is_expired and not long_op.is_expired
@@ -207,7 +207,7 @@ def test_concurrent_operations(self):
207207
for i in range(25):
208208
manager.complete_operation(operations[i].token, Mock())
209209
operations[i].keep_alive = 1
210-
operations[i].created_at = time.time() - 2
210+
operations[i].resolved_at = time.time() - 2
211211

212212
# Cleanup should remove expired operations
213213
removed_count = manager.cleanup_expired_operations()
@@ -286,8 +286,8 @@ def test_terminal_and_expiration_logic(self):
286286

287287
completed_status: AsyncOperationStatus = "completed"
288288
operation.status = completed_status
289-
operation.created_at = now - 1800 # 30 minutes ago
289+
operation.resolved_at = now - 1800 # 30 minutes ago
290290
assert not operation.is_expired # Within keepAlive
291291

292-
operation.created_at = now - 7200 # 2 hours ago
292+
operation.resolved_at = now - 7200 # 2 hours ago
293293
assert operation.is_expired # Past keepAlive

0 commit comments

Comments
 (0)