Skip to content

fix(core): cancelled waiter cancels shared MCP initialization #743

Description

@hsusul

Prerequisites

  • I searched current and closed issues and pull requests, including comments, for shared initialization, cancellation, CancelledError, asyncio.shield, _ensure_initialized, and _init_task.
  • I reproduced this on current upstream/main at 88cc7959d9c7ebc1d92dedeae90e56fb10d2a943.

Toolbox version

Not applicable. The bug is deterministic in the Python SDK transport base and does not require a live Toolbox server.

Environment

  • macOS
  • Python 3.13.5
  • toolbox-core from current upstream/main

Expected behavior

When concurrent first requests share the MCP transport initialization task, cancelling one request should cancel only that waiter. Other concurrent requests and later requests should still be able to complete the same one-time initialization.

Current behavior

_McpHttpTransportBase._ensure_initialized() awaits its shared _init_task directly. Cancelling any waiter propagates cancellation into that shared task, which cancels other waiters and causes every later request to raise CancelledError.

Observed output from a deterministic local reproduction:

first_waiter=cancelled
second_waiter=cancelled
shared_init_cancelled=True
later_initialization=cancelled

Steps to reproduce

  1. Create a concrete _McpHttpTransportBase whose _initialize_session() waits on an asyncio.Event.
  2. Start two concurrent _ensure_initialized() waiters.
  3. Cancel the first waiter while initialization is pending.
  4. Release initialization and await the second waiter.
  5. Call _ensure_initialized() again.

The second and later waits are cancelled even though only the first caller was cancelled.

Root cause and proposed scope

The one shared task is awaited without cancellation isolation. The narrow fix is to await it through asyncio.shield(), with a regression test proving one cancelled waiter does not cancel the shared initialization. Genuine initialization failures should continue to propagate unchanged.

This is independent of open PRs #735, #736, #737, #740, and #741 and does not touch their files or behaviors.

Additional details

No credentials, live services, or timing sleeps are required; the regression can use controlled asyncio events. This report and proposed patch were prepared with AI assistance and manually validated against current upstream source and tests.

Metadata

Metadata

Assignees

Labels

priority: p2Moderately-important priority. Fix may not be included in next release.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions