Skip to content

Deterministically cancel background task-store runners on server disposal via a server-lifetime CTS #1707

Description

@PranavSenthilnathan

Background

While investigating the flaky Windows Debug CI hang (#1701, fixed in #1702), a legitimate — but separate — server-lifetime concern was raised in review that should be addressed on its own.

The real root cause of #1701 turned out to be a client-side server/discover probe timeout being spuriously tripped under CI slowness (fixed test-side in #1702). It was not caused by background task disposal. However, the disposal behavior of background task-store runners is still worth hardening.

Problem

Background task runners in McpServerImpl (the tasks extension / MRTR machinery) are not deterministically cancelled and awaited on server disposal. Each handler currently uses a long-lived, per-handler CancellationTokenSource (see McpServerImpl.cs around the "Create a long-lived CTS for the handler that survives across retries" comment, ~L2120), and disposal cancels the set of runners it knows about at that moment.

This leaves a race: a background task registered after the current cancellation sweep has run can escape cancellation entirely, since disposal has already iterated the collection. Such a runner can outlive the server, which is exactly the kind of dangling background work that can leak into the next test or keep resources alive past DisposeAsync.

Proposed fix

Link every background task-store runner's cancellation to a single server-lifetime CancellationTokenSource that is cancelled at the very start of DisposeAsync (before, or as part of, the existing cancellation sweep). Any runner registered after that point would then be created from an already-cancelled token, so it either never starts meaningful work or exits promptly — closing the register-after-sweep race deterministically.

Concretely:

  • Introduce a server-lifetime CTS on McpServerImpl.
  • Cancel it at the start of disposal.
  • Create each background runner's CancellationTokenSource linked to that server-lifetime token (in addition to any per-request/per-handler token).
  • Ensure disposal awaits outstanding runners after cancelling, so no background task outlives the server.

Context / references

Notes

This is intentionally scoped as a follow-up so #1702 can stay a minimal, test-only fix for the flaky hang. This issue tracks the independent server-lifetime/disposal hardening.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions