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.
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/discoverprobe 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-handlerCancellationTokenSource(seeMcpServerImpl.csaround 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
CancellationTokenSourcethat is cancelled at the very start ofDisposeAsync(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:
McpServerImpl.CancellationTokenSourcelinked to that server-lifetime token (in addition to any per-request/per-handler token).Context / references
src/ModelContextProtocol.Core/Server/McpServerImpl.cs—DisposeAsync(~L509+) and the long-lived handler CTS creation (~L2120).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.