BuildMcpServerOptions() returns one options instance a host may reuse across connections — the pattern docs/mcp-transports.md prescribes. That server never emits tools/list_changed, resources/list_changed or prompts/list_changed, while the SDK advertises ListChanged = true for it. A client that would refresh on the notification never does, and nothing says why.
PR #71 documented the behaviour truthfully (docs/mcp-transports.md, known-limitation callout) rather than fixing it. This issue is the fix.
Why it cannot be fixed cheaply
Repl cannot simply stop advertising the capability. McpServerImpl.ConfigureTools / ConfigureResources / ConfigurePrompts seed listChanged from options.Capabilities?.X?.ListChanged and then force it to true whenever the matching primitive collection is non-null. The static path always supplies collections, so the flag is the SDK's to set, not BuildCapabilities's. (The SDK does strip it where it genuinely cannot deliver — GetAdvertisedCapabilities nulls all three when !HasStatefulTransport() && !listenStreamCanDeliverListChanged — so the false advertisement survives only on stateful transports.)
EnsureRoutingSubscription would be inert even if called. The static options carry the snapshot collections, not the _toolListChanged / _resourceListChanged / _promptListChanged signal collections, so SignalDiscoveryChanged() clears collections no server on that path subscribes to.
What the real fix looks like
Make the static collections live: rebuild from the catalog context on routing invalidation and reconcile in place under McpServerPrimitiveCollection<T>.DeferChangedEvents(), which coalesces into one Changed event the SDK fans out to every connected server.
Two concrete obstacles, both verified against the SDK 2.2.0 assembly:
- Lifetime. Nothing in the returned options references
McpServerHandler — the primitives hold McpToolAdapter, which holds the app, options and services. EnsureRoutingSubscription uses a WeakReference (McpServerHandler.cs, EnsureRoutingSubscription), so notifications would die silently at the first GC. Making this work means deliberately rooting the handler from the options, which is a design decision rather than a patch.
McpServerResourceCollection is sealed. The clean shape — a Repl subclass swapping an immutable snapshot atomically and raising Changed — is available for tools and prompts but not resources, leaving a window where a concurrent resources/list observes a half-rebuilt list. Given_McpServerOptionsBuilder.cs:24-26,54-57 also pins the "pre-populated collection" contract, so moving to handlers is not a sidestep.
Scope
Not issue #70: this is catalog freshness, not DI scoping. Not blocked by anything — but it needs a decision on rooting the handler, so it deserves its own design pass rather than riding on the next PR.
Origin: PR #71 review panel (issue #51), operability and architect lenses.
BuildMcpServerOptions()returns one options instance a host may reuse across connections — the patterndocs/mcp-transports.mdprescribes. That server never emitstools/list_changed,resources/list_changedorprompts/list_changed, while the SDK advertisesListChanged = truefor it. A client that would refresh on the notification never does, and nothing says why.PR #71 documented the behaviour truthfully (
docs/mcp-transports.md, known-limitation callout) rather than fixing it. This issue is the fix.Why it cannot be fixed cheaply
Repl cannot simply stop advertising the capability.
McpServerImpl.ConfigureTools/ConfigureResources/ConfigurePromptsseedlistChangedfromoptions.Capabilities?.X?.ListChangedand then force it totruewhenever the matching primitive collection is non-null. The static path always supplies collections, so the flag is the SDK's to set, notBuildCapabilities's. (The SDK does strip it where it genuinely cannot deliver —GetAdvertisedCapabilitiesnulls all three when!HasStatefulTransport() && !listenStreamCanDeliverListChanged— so the false advertisement survives only on stateful transports.)EnsureRoutingSubscriptionwould be inert even if called. The static options carry the snapshot collections, not the_toolListChanged/_resourceListChanged/_promptListChangedsignal collections, soSignalDiscoveryChanged()clears collections no server on that path subscribes to.What the real fix looks like
Make the static collections live: rebuild from the catalog context on routing invalidation and reconcile in place under
McpServerPrimitiveCollection<T>.DeferChangedEvents(), which coalesces into oneChangedevent the SDK fans out to every connected server.Two concrete obstacles, both verified against the SDK 2.2.0 assembly:
McpServerHandler— the primitives holdMcpToolAdapter, which holds the app, options and services.EnsureRoutingSubscriptionuses aWeakReference(McpServerHandler.cs,EnsureRoutingSubscription), so notifications would die silently at the first GC. Making this work means deliberately rooting the handler from the options, which is a design decision rather than a patch.McpServerResourceCollectionissealed. The clean shape — a Repl subclass swapping an immutable snapshot atomically and raisingChanged— is available for tools and prompts but not resources, leaving a window where a concurrentresources/listobserves a half-rebuilt list.Given_McpServerOptionsBuilder.cs:24-26,54-57also pins the "pre-populated collection" contract, so moving to handlers is not a sidestep.Scope
Not issue #70: this is catalog freshness, not DI scoping. Not blocked by anything — but it needs a decision on rooting the handler, so it deserves its own design pass rather than riding on the next PR.
Origin: PR #71 review panel (issue #51),
operabilityandarchitectlenses.