openai / openai/openai-agents-python
MCP: cancelling a _ServerWorker task does not stop the worker (CancelledError swallowed by except BaseException)
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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