modelcontextprotocol / modelcontextprotocol/csharp-sdk
Deterministically cancel background task-store runners on server disposal via a server-lifetime CTS
@jstar0 is already working on this.
Since Jul 19, 2026.
- Dominant language
- C#
- Stars
- 4.5k
- Forks
- 814
- Avg merge
- 9d 19h
- Merged PRs (30d)
- 4
Description
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
CancellationTokenSourcelinked 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
- Review comment: https://github.com/modelcontextprotocol/csharp-sdk/pull/1702#discussion_r3581808811 (@halter73)
- Relevant code:
src/ModelContextProtocol.Core/Server/McpServerImpl.cs—DisposeAsync(~L509+) and the long-lived handler CTS creation (~L2120). - Related: #1701, #1702.
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.
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.
Assessment
This issue has not been assessed yet.