googleapis / googleapis/mcp-toolbox-sdk-python
fix(core): cancelled waiter cancels shared MCP initialization
- Dominant language
- Python
- Stars
- 191
- Forks
- 60
- Avg merge
- 5d 20m
- Merged PRs (30d)
- 13
Description
### Prerequisites
- [x] I searched current and closed issues and pull requests, including comments, for shared initialization, cancellation, `CancelledError`, `asyncio.shield`, `_ensure_initialized`, and `_init_task`.
- [x] 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:
```text
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.
Contributor guide
Assessment
This issue has not been assessed yet.