In MCP 2026-07-28, how does a server learn that the client's roots changed?
answer
- that notification no longer exists
- nothing to invalidate without a session
- check what the listen filter actually covers
- valid for one request only
- the migration removes the problem
basics
~20 sIt does not get told. Revision 2026-07-28 removed notifications/roots/list_changed and the roots.listChanged sub-capability, so a roots list is only ever valid for the request that fetched it; a server needing current roots asks again on the next request.
solid answer
~50 sThere is no change notification for roots any more. Revision 2026-07-28 removed `notifications/roots/list_changed` outright and reduced the client `roots` capability to an empty object `{}`, dropping the `listChanged` flag it used to carry. Nothing replaced it: `subscriptions/listen` has a `SubscriptionFilter` covering `toolsListChanged`, `promptsListChanged`, `resourcesListChanged` and `resourceSubscriptions`, and no roots field at all. The consequence is that a roots list a server received is a snapshot scoped to one request, not standing configuration. A server that cares must re-ask — return another input-required result carrying `roots/list` on the next `tools/call` — and it must never cache roots across requests as though they were a session fact, since 2026-07-28 is stateless and a later request may come from a different client instance entirely. In practice this is another argument for the migration: take the path as a tool parameter and it is current by construction.
go deeper
Know the plain fact: MCP 2026-07-28 has no roots-changed notification, so a server asks for roots again when it needs them.
Explain what was removed and when — notifications/roots/list_changed and the roots.listChanged sub-capability — and that the surviving subscriptions/listen filter has no roots field.
Connect the removal to statelessness and say what goes wrong in production if a server caches roots: silent staleness, and no safe cache key behind a load balancer where requests may be different clients.
Argue the design consequence: client-side context that a server must hold across calls is the anti-pattern, and moving the path into an explicit tool argument removes the round trip and the invalidation problem together.
## What was removed In revisions up to 2025-11-25, roots came with a push channel. A client that declared `roots` with `listChanged: true` promised to send `notifications/roots/list_changed` whenever the user opened or closed a project folder; a server that had cached the list knew to re-fetch. Revision 2026-07-28 removed that notification and reduced the `roots` capability object to `{}`. Nothing took its place. ## Why the removal is coherent The notification only made sense in a world where a server *held* a roots list across time. That world was the session world. In 2026-07-28 MCP is stateless: every request is self-contained and carries its own protocol version and capabilities, servers MUST NOT rely on prior requests over the same connection to establish context, and an open connection — a running stdio process included — is explicitly not a conversation. Once there is no session to attach a cached roots list to, a notification telling you the cache is stale has nothing to invalidate. It is also worth checking what the surviving notification machinery covers. `subscriptions/listen` is the one channel for server → client change pushes, opened by the client with a `SubscriptionFilter` naming `toolsListChanged`, `promptsListChanged`, `resourcesListChanged` and `resourceSubscriptions`. Every one of those is about *server-side* state changing and the client needing to know. Roots are the opposite direction — client-side state the server consumes — and that direction has no push mechanism at all in 2026-07-28. ## The operating rule Treat a roots answer as scoped to the request that produced it: 1. During a `tools/call`, the server returns an interim `input_required` result asking for `roots/list`. 2. The client answers on retry; the server uses the list to complete *that* call. 3. The list is then finished. It is not configuration, not a session fact, and not something to key a cache on. If the next `tools/call` needs roots, it asks again. That costs a round trip, which is a real price — and precisely the price the deprecation is telling you to stop paying by putting the directory in the tool's arguments instead. ## Why caching them is a genuine bug, not just impolite Three failure modes, all realistic: - **Staleness with no signal.** The user closes a project and opens another. Nothing tells the server. A cached list now names directories the user is no longer working in, and the server happily searches them — surprising at best, a disclosure the user did not expect at worst. - **Wrong client.** Behind a load balancer, consecutive requests from the same host may land on different server instances, and requests arriving at one instance may come from entirely different clients and users. There is no session identity to key a roots cache on that is safe. - **Authorization drift.** Roots say nothing about authorization, so a cached list has no relationship to whichever token accompanies the next request. Two facts from different moments get combined into a decision neither supports. If a server genuinely needs cross-call state, 2026-07-28's answer is an explicit server-minted handle passed back as an ordinary tool argument — visible, intentional, and not smuggled in as remembered context. ## The deprecation frame Roots are deprecated as of 2026-07-28 (SEP-2577), staying in the specification for at least twelve months so the earliest removal is a revision on or after 2027-07-28. The named migration — tool parameters, resource URIs, server configuration — dissolves this whole question. A directory passed as an argument to the call that uses it cannot go stale between calls, needs no change notification, and is visible to the user in the tool call they approve. ## Answering well State the removal precisely and name the revision. Then give the reasoning rather than just the fact: no session means no cache means nothing for a change notification to invalidate. Finish with the practical rule — re-ask per request, never cache — and note that the migration makes the problem disappear.
- Could a server watch roots through subscriptions/listen instead?No. `SubscriptionFilter` exposes `toolsListChanged`, `promptsListChanged`, `resourcesListChanged` and `resourceSubscriptions` — all server-side state that the client wants pushed to it. Roots flow the other way, from client to server, and 2026-07-28 has no client-to-server push channel for them. The listen stream is also opened by the client for its own benefit, not something a server can subscribe to.
- A server caches roots for five minutes to save a round trip. What breaks?Correctness and, potentially, privacy. Nothing signals a change, so the cache can name folders the user has closed and the server will keep reading them. Worse, under Streamable HTTP behind a load balancer there is no session identity to key the cache on — consecutive requests may be different clients or different users. If cross-call state is truly needed, mint an explicit handle and take it as a tool argument.
- What is the low-cost design that avoids re-asking for roots on every call?Stop asking. Declare the directory as a parameter in the tool's `inputSchema` and let the client fill it from whatever it knows about the user's workspace. The value then arrives with the call that needs it, is current by construction, is visible in the call the user approves, and costs no extra round trip. This is exactly the migration 2026-07-28 names for deprecated roots.
- Does the removal of roots.listChanged mean a client can no longer signal roots support?It can still signal support — `roots` remains a client capability, just as an empty object `{}` in `io.modelcontextprotocol/clientCapabilities`. What it can no longer signal is the ability to push updates, because the update mechanism itself is gone. And because the protocol is stateless, that capability must be re-declared in the `_meta` of every request; a server must not remember it from an earlier one.
saying these in an interview costs you the question
- Claims notifications/roots/list_changed still exists
- Caches a roots list across requests as session state
- Says roots can be watched via subscriptions/listen
- Thinks roots.listChanged is still a client sub-capability
- Treats an open stdio process as a session holding roots