MCP: cancelling a _ServerWorker task does not stop the worker (CancelledError swallowed by except BaseException)

Author: RehansanjayCreated Sep 16, 2026Updated Sep 16, 2026

Describe the bug

Cancelling a _ServerWorker task does not stop the worker. _ServerWorker._run (src/agents/mcp/manager.py:93-110) wraps the command it is running in except BaseException, records the error on the command's future and continues its loop. asyncio.CancelledError is a BaseException, so an external cancellation is consumed as if it were a command failure and the worker goes back to await self._queue.get().

The task stays alive in the cancelling state, so is_done stays False, the manager's add_done_callback / _handle_worker_done bookkeeping never runs, and anything awaiting the task (including loop shutdown) waits for a task that will not finish.

Debug information

  • Agents SDK version: main @ fbf59a40
  • Python version: 3.14.0

Repro steps

python
import asyncio
from typing import Any, cast

from agents.mcp.manager import _ServerWorker


class _BlockingServer:
    def __init__(self) -> None:
        self.connect_started = asyncio.Event()

    async def connect(self) -> None:
        self.connect_started.set()
        await asyncio.sleep(3600)

    async def cleanup(self) -> None:
        return None


async def test_cancelling_the_worker_task_stops_the_worker() -> None:
    server = _BlockingServer()
    worker = _ServerWorker(cast(Any, server))

    caller = asyncio.create_task(worker.connect(timeout_seconds=None))
    await asyncio.wait_for(server.connect_started.wait(), timeout=1)

    worker._task.cancel()
    await asyncio.sleep(0.2)

    caller.cancel()
    try:
        await caller
    except BaseException:
        pass

    assert worker._task.done(), "the worker task survived cancellation and is still looping"

Result on main:

AssertionError: the worker task survived cancellation and is still looping
<Task cancelling name='Task-2' coro=<_ServerWorker._run() running at src/agents/mcp/manager.py:95>

Expected behavior

Cancelling the worker task ends it.

Why this is not a one-line re-raise

A server's own connect/cleanup may raise CancelledError deliberately — CancelledServer in tests/mcp/test_mcp_server_manager.py, together with suppress_cancelled_error, depends on that error reaching the caller through the command future. So the two cases need separating: an error raised by the command (settle the future, keep looping) versus a cancellation of the worker itself (settle the future, then stop).

asyncio.current_task().cancelling() > 0 distinguishes them on 3.11+, but the fallback timeout path in _run_with_timeout_in_task (manager.py:135-148) calls task.cancel() and converts the result to TimeoutError without a matching uncancel(), which would leave a pending cancellation request behind on that path. cancelling() also does not exist on 3.10, which the project still supports.

Happy to send a PR with tests once you decide which behaviour you want.

Source: openai/openai-agents-python