Repository navigation
Description
What happened?
I ran into the client side of this in goose (aaif-goose/goose#13055), which never received tools/list_changed from 2026-07-28 servers because it doesn't open subscriptions/listen, so I checked how the Python SDK docs explain it.
The Client callbacks page calls message_handler "the catch-all: every server notification the session surfaces reaches it". By default (mode="auto"), Client(...) negotiates 2026-07-28 with any server that supports it. On such a connection, spec-compliant servers send the list_changed notifications and notifications/resources/updated only on a subscriptions/listen stream the client opened (migration guide); MCPServer drops the v1-style send_*_list_changed() calls there. So a client that registers only message_handler, as v1 clients did, gets no change notifications from those servers.
The page mentions client.listen() only to say its acknowledgment is consumed by the stream. It doesn't say change notifications need one, and doesn't link Subscriptions. The Caching page already has this pointer.
What did you expect?
Two sentences in the message_handler paragraph, after "…is consumed by that stream.", for example:
On a 2026-07-28 connection, change notifications (
notifications/tools/list_changed, its prompts and resources siblings, andnotifications/resources/updated) arrive only on a stream you open withclient.listen(...). They reachmessage_handlerfrom there, for the kinds that stream asked for; Subscriptions has the details.
Related: #3173 (open, last updated in July) rewrites the same paragraph but doesn't add this; the sentence could go there instead. I'd like to fix this myself (a one-sentence docs PR) if you take an outside PR for it.
AI disclosure: text drafted with AI assistance; the goose finding is mine, and I checked the behavior against src/mcp/client/session.py and the docs myself.
Code to reproduce
# Docs feedback: https://github.com/modelcontextprotocol/python-sdk/blob/77001c9/docs/client/callbacks.md#the-notification-callbacksSDK version
v2.3.0; docs checked on main @ 77001c9
Area
Documentation
Activity
rohandravid commentedon Nov 9, 2026
I’d like to take this. I’ll add a note to the notification callbacks section of callbacks.md explaining that list_changed notifications require client.listen() on 2026-07-28 connections, with a link to the Subscriptions page (matching how caching.md handles it).
rohandravid commentedon Nov 10, 2026
Hi! I have a fix ready on my fork. Could you assign this issue to me so I can open the PR? Thanks!
paolocostanzo commentedon Nov 10, 2026
Thanks @rohandravid, your plan matches what I had in mind. I also have a fix ready, but I'm happy for yours to be the one that goes in, and I'll review it. One thing to include: the same applies to
notifications/resources/updated, not only thelist_changednotifications.I can't assign issues in this repo, so could a maintainer assign this to Rohan so he can open the PR?
media-ctrl commentedon Nov 11, 2026
Your reading is correct, and the sentence you want to add is already written down on the server side, which makes the PR easy to defend.
src/mcp/server/connection.py:156(NotifyOnlyOutbound) states it directly, and the sentence lands at line 165: at the 2026-07-28 era these notifications "are dropped with a debug log: at this era they reach a client only through asubscriptions/listenstream it opened". So the client page is missing the counterpart of a rule the server documents, anddocs/client/callbacks.md:138is the paragraph to extend, right after "is consumed by that stream".Two additions I would put in the same PR, because the obvious migration snippet is a trap without them:
client.listen()is 2026-07-28 only.src/mcp/client/subscriptions.py:224raisesListenNotSupportedErrorwhen the negotiated version predates it, so a client that also talks to older servers needs a version branch, withmessage_handlerkept for everything else. Three lines showing both branches is worth more than the extra sentence, because the copy-paste version throws on exactly the servers it is not meant for.listen()requires an entered session. Without one it raises a RuntimeError whose message is listen() requires an entered session, so the example has to sit inside the session context manager. That is the second thing a reader will get wrong.Worth saying plainly as well: for goose this is not a docs bug. On a 2026-07-28 connection there is no channel for those notifications unless a listen stream was opened, so no handler change can recover them. The fix there is to open the stream and refetch on event, not to widen
message_handler.