Skip to content

Commit 02547cb

Browse files
wiggzzclaude
andcommitted
Run handle_request in caller context, not as child task
The previous approach ran handle_request inside a child task within the request-scoped task group. This caused coverage.py on Python 3.11 to lose track of execution when returning from the task group back to the caller — the root cause of the false coverage gaps on test assertions. The fix is simpler: run handle_request directly in the async with body (like the original code on main), with only run_stateless_server as a child task. This preserves the request-scoped task group for preventing task accumulation while keeping the caller's execution context intact. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a7907d0 commit 02547cb

File tree

2 files changed

+9
-20
lines changed

2 files changed

+9
-20
lines changed

src/mcp/server/streamable_http_manager.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -186,19 +186,12 @@ async def run_stateless_server(*, task_status: TaskStatus[None] = anyio.TASK_STA
186186
# finishes, preventing zombie tasks from accumulating.
187187
# See: https://github.com/modelcontextprotocol/python-sdk/issues/1764
188188
async with anyio.create_task_group() as request_tg:
189-
190-
async def run_request_handler(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED):
191-
task_status.started()
192-
# Handle the HTTP request and return the response
193-
await http_transport.handle_request(scope, receive, send)
194-
# Cancel the request-scoped task group to stop the server task.
195-
# This ensures the Cancelled exception reaches the server task
196-
# before terminate() closes the streams, avoiding a race between
197-
# Cancelled and ClosedResourceError in the message router.
198-
request_tg.cancel_scope.cancel()
199-
200189
await request_tg.start(run_stateless_server)
201-
await request_tg.start(run_request_handler)
190+
# Handle the HTTP request directly in the caller's context
191+
# (not as a child task) so execution flows back naturally.
192+
await http_transport.handle_request(scope, receive, send)
193+
# Cancel the request-scoped task group to stop the server task.
194+
request_tg.cancel_scope.cancel()
202195

203196
# Terminate after the task group exits — the server task is already
204197
# cancelled at this point, so this is just cleanup (sets _terminated

tests/server/test_streamable_http_manager.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -258,19 +258,15 @@ async def mock_receive():
258258
await manager.handle_request(scope, mock_receive, mock_send)
259259

260260
# Verify transport was created
261-
assert len(created_transports) == 1, "Should have created one transport" # pragma: lax no cover
261+
assert len(created_transports) == 1, "Should have created one transport"
262262

263-
transport = created_transports[0] # pragma: lax no cover
263+
transport = created_transports[0]
264264

265265
# The key assertion - transport should be terminated
266-
assert transport._terminated, (
267-
"Transport should be terminated after stateless request"
268-
) # pragma: lax no cover
266+
assert transport._terminated, "Transport should be terminated after stateless request"
269267

270268
# Verify internal state is cleaned up
271-
assert len(transport._request_streams) == 0, (
272-
"Transport should have no active request streams"
273-
) # pragma: lax no cover
269+
assert len(transport._request_streams) == 0, "Transport should have no active request streams"
274270

275271

276272
@pytest.mark.anyio

0 commit comments

Comments
 (0)