@@ -638,6 +638,60 @@ def rejecting() -> str:
638638 assert recording .puts == []
639639
640640
641+ async def test_abandoned_augmented_call_leaves_no_task_record () -> None :
642+ """SDK-defined: a client abandoning an augmented `tools/call` cancels the tool body
643+ through the Tasks interceptor, which lets cancellation propagate -- no task record
644+ is written (nothing unfetchable sits in the store until TTL), and the same
645+ connection's later augmented calls still complete normally."""
646+ handler_started = anyio .Event ()
647+ handler_cancelled = anyio .Event ()
648+ recording = _RecordingStore ()
649+ tasks = Tasks (clock = lambda : _FIXED_NOW , store = recording )
650+ mcp = MCPServer ("demo" , extensions = [tasks ])
651+
652+ @mcp .tool (structured_output = False )
653+ async def block () -> str :
654+ handler_started .set ()
655+ try :
656+ await anyio .Event ().wait () # parked until the client's abandonment cancels it
657+ except anyio .get_cancelled_exc_class ():
658+ handler_cancelled .set ()
659+ raise
660+ raise NotImplementedError # unreachable: the wait only ends by cancellation
661+
662+ @mcp .tool (structured_output = False )
663+ def echo (text : str ) -> str :
664+ return text
665+
666+ async with Client (mcp , extensions = [TasksExtension ()]) as client :
667+ abandon = anyio .CancelScope ()
668+
669+ async def call_and_abandon () -> None :
670+ with abandon :
671+ await _send_raw (
672+ client , types .CallToolRequest (params = types .CallToolRequestParams (name = "block" , arguments = {}))
673+ )
674+ raise NotImplementedError # unreachable: the abandoned call never resolves
675+
676+ async with anyio .create_task_group () as tg :
677+ tg .start_soon (call_and_abandon )
678+ with anyio .fail_after (5 ):
679+ await handler_started .wait ()
680+ abandon .cancel ()
681+ with anyio .fail_after (5 ):
682+ await handler_cancelled .wait ()
683+
684+ # Let the interceptor's unwind finish before inspecting the store.
685+ await anyio .wait_all_tasks_blocked ()
686+ assert recording .puts == []
687+ after = await _augmented_call (client )
688+
689+ assert after ["resultType" ] == "task"
690+ assert after ["status" ] == "completed"
691+ assert len (recording .puts ) == 1
692+ assert recording .puts [0 ].task .status == "completed"
693+
694+
641695async def test_get_task_result_parses_completed_and_failed_wire_shapes () -> None :
642696 """SDK-defined: `GetTaskResult` is the lenient client-side parse model for
643697 `tasks/get` -- it parses the real wire dict of both terminal shapes, carrying
0 commit comments