openai / openai/openai-agents-python

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

Open
#5,054 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
29.6k
Forks
4.8k
Avg merge
1d 20h
Merged PRs (30d)
123

Description

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
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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with _ServerWorker._run in src/agents/mcp/manager.py:93-110 and the timeout fallback in manager.py:135-148. Run the cancellation scenario against tests/mcp/test_mcp_server_manager.py, including CancelledServer and suppress_cancelled_error. Done means external worker cancellation finishes the task while command-raised CancelledError still reaches its caller and existing timeout behavior remains correct.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
57/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.