Skip to content

Stateless servers strip listChanged capabilities even when a custom subscriptions/listen handler can deliver them #1779

Description

@halter73

Problem

The McpServerImpl constructor unconditionally clears the list-changed capabilities on any server without a stateful transport:

else
{
    if (ServerCapabilities.Tools is not null)
        ServerCapabilities.Tools.ListChanged = null;
    if (ServerCapabilities.Prompts is not null)
        ServerCapabilities.Prompts.ListChanged = null;
    if (ServerCapabilities.Resources is not null)
        ServerCapabilities.Resources.ListChanged = null;
}

That was correct when stateless Streamable HTTP had no way to push unsolicited notifications. It is no longer correct now that #1775 adds a public subscriptions/listen handler, because a custom handler can stream list-changed notifications over the held-open POST in exactly that mode.

The result is that an application which sets WithSubscriptionsListenHandler and advertises tools.listChanged = true on a stateless server has that capability silently dropped, so clients never learn the server supports it. This directly contradicts the documented contract of WithSubscriptionsListenHandler, which deliberately advertises nothing on the app's behalf and tells the app to advertise its own capabilities.

There is a second, smaller problem in the same code: the stripping mutates the caller-supplied ServerCapabilities instance in place. So it is not scoped to initialize, it also changes what server/discover reports, and it has an observable side effect on the options object the application handed us.

Why the obvious fix is wrong

Skipping the stripping whenever a custom handler is configured re-exposes the case the stripping was protecting. An initialize-handshake client on a stateless server still cannot open a subscriptions/listen stream, so it would be told listChanged: true and then never receive a notification.

The capability needs to be advertised to clients that can actually receive it and withheld from clients that cannot, which makes this a per-request decision rather than a construction-time mutation:

  • server/discover and clients negotiating 2026-07-28 or later can open a listen stream, so they should see the app's advertised value.
  • Initialize-handshake clients on a stateless transport cannot, so they should keep seeing the capability withheld.

Proposed direction

Stop mutating ServerCapabilities in the constructor. Compute the capabilities returned from initialize at request time instead, withholding list-changed only for clients that have no delivery path. Leave server/discover reporting what the application actually advertised.

Notes

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions