modelcontextprotocol / modelcontextprotocol/csharp-sdk

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

Open
#1,707 1 comment 0 reactions 1 assignee View on GitHub

@jstar0 is already working on this.

Since Jul 19, 2026.

bug enhancement P2 ready for work
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 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.

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.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.