Repository navigation
MCP server: list-change notifications are lost for clients that negotiate 2026-07-28 (no subscriptions/listen) #1383
Copy link
Copy link
Open
Description
Summary
When a client negotiates protocol revision 2026-07-28 (via server/discover), a StdioMcpServer cannot deliver tool or prompt list changes to it. A server that proxies MCP servers with dynamic tool discovery therefore stops working for those clients. In that pattern, an upstream adds tools during a tools/call and emits notifications/tools/list_changed, but the client never re-lists, so the new tools are never offered to the model.
Details
- In 2026-07-28, list-change notifications are only delivered on a
subscriptions/listenstream that the client opened withtoolsListChanged: true.- Each notification on that stream is tagged with
io.modelcontextprotocol/subscriptionId. notifications/subscriptions/acknowledgedcomes first.- The spec says "the server MUST NOT send notification types the client has not explicitly requested" (see also Tools › List Changed Notification).
- Each notification on that stream is tagged with
mcp-serverdoes not implementsubscriptions/listen(it returns-32601).McpDomainOperations.discoverCapabilitiestherefore correctly advertisestools: {}andprompts: {}withoutlistChanged, as planned in MCP server: support protocol revision 2026-07-28 (stateless / modern era) #1337.McpCatalog.onRemoteNotificationstill writes the upstream's untaggednotifications/tools/list_changedto the transport, regardless of the negotiated revision. A client that negotiated 2026-07-28 ignores it, because the capability was not advertised and it has no subscription.- The same clients work against the same server when it only offers the handshake versions. They fall back to
initialize, the server advertisestools.listChanged: true, and the forwarded notification triggers a re-list.
Reproduction
- Build a
StdioMcpServerwith anHttpMcpClientremote. Ontools/call discover, the remote adds a tool and writesnotifications/tools/list_changedfollowed by the result into the SSE response. - The client sends
server/discoverwith_meta["io.modelcontextprotocol/protocolVersion"] = "2026-07-28". The result lists 2026-07-28 first, andcapabilities.toolshas nolistChanged. - The client calls
discover. The server writes an untaggedlist_changedand the result. A 2026-07-28 client does not re-list, andtools/callfor the new tool fails with-32602 No such tooluntil something else triggers atools/list. - If
listChanged: trueis advertised, a client sendssubscriptions/listenwith{"notifications":{"toolsListChanged":true,"promptsListChanged":true}}and receives-32601.
Proposed fix
- Short term (PR to follow): add an opt-in
protocolVersions(...)toMcpEngine.BuilderandStdioMcpServerBuilder, so a server that relays list changes can serve the handshake versions only. A client that prefers 2026-07-28 then receives-32022with the supported list fromserver/discover, falls back toinitialize, and keeps receiving list changes. - Longer term: implement
subscriptions/listenon stdio and advertiselistChangedfromserver/discoveronce it is available. Points that came up while looking at this:- Use a subscription registry owned by the connection and keyed by the listen request id, preserving the id's JSON type.
- Send the acknowledgement strictly before any notification on that subscription.
- Handle
notifications/cancelledfor the listen id, and send the completion result on graceful shutdown. - Define explicit behavior when handshake and stateless requests share one stdio connection; today there is one mutable session version.
- Install notification callbacks for upstreams that are reached through 2026-07-28; today they are installed only during
initialize. - HTTP needs a streaming POST response before it can support this.
- Separately: do not send untagged list-change notifications to 2026-07-28 clients that have not subscribed.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
adwsingh commentedon Oct 8, 2026
I would just implement
subscriptions/listen, are there any blockers to doing it now?psp65 commentedon Oct 8, 2026
No hard blockers. It's mostly plumbing. From reading the code, here's what it needs:
Stdio (downstream)
subscriptions/listenandnotifications/cancelled. Both areUnknownCalltoday, so listen gets-32601and cancellation is silently dropped.StdioMcpServer. Key it by the listen request id, keeping the id's JSON type, and store the accepted filter (toolsListChanged/promptsListChanged; resources omitted).notifications/subscriptions/acknowledgedbefore any notification on that subscription. Keep the listen request open untilnotifications/cancelledarrives, or until shutdown, which answers it with{resultType: "complete", _meta: {subscriptionId}}._meta.subscriptionId: remotelist_changed,addService/addRemoteClient, andrefreshTools. Stop writing untaggedlist_changedto 2026-07-28 clients that haven't subscribed. Handshake-era clients keep today's untagged broadcast.listChangedfromdiscoverCapabilitiesonly on transports that can deliver it.McpSessionkeeps a single mutable version that each request's_metaoverwrites, so "initialized via handshake" needs tracking separately to choose between untagged delivery and subscription delivery.Upstreams (only relevant when the proxied server itself speaks 2026-07-28)
prepareRemoteClientskipsinitializefor stateless-capable remotes, butMcpRemoteClientinstalls the notification consumer only ininitialize. A stateless upstream's list changes are therefore never seen. The client side would have to open its ownsubscriptions/listento the upstream, which over HTTP means a long-lived POST SSE stream.HTTP (downstream)
McpHttpHandler.Responseis(statusCode, body)and can't hold a long-lived streaming response. HTTP would keep not advertisinglistChangeduntil that's supported.A reasonable split would be one PR for stdio downstream (listen, cancel, ack ordering, tagged delivery,
listChangedin discover, no unsolicited notifications), with HTTP and stateless upstreams as follow-ups. #1385 can then be closed, or kept as a configuration option — your call.psp65 commentedon Oct 13, 2026
#1385 was merged as immediate unblocker.