How does an MCP client keep a cached tools/list fresh in revision 2026-07-28?
answer
- The connection is no longer the cache lifetime
- Two required caching fields on the list result
- Public versus private depends on authorization
- Push requires opting in, never unsolicited
- Stable ordering keeps caches from churning
basics
~20 sListToolsResult is a cacheable result carrying required ttlMs and cacheScope, so a client caches the tool list for that lifetime. For push updates it must open a subscriptions/listen stream with toolsListChanged set; notifications/tools/list_changed arrives nowhere else.
solid answer
~50 sIn MCP revision 2026-07-28 `ListToolsResult` extends `CacheableResult`, so it carries required `ttlMs` (how long the list stays valid) and `cacheScope` — `"public"` when the same list is valid for anyone, `"private"` when it depends on the authorization presented. A client caches accordingly and re-calls `tools/list` when the TTL lapses. That polling is the baseline; for prompt updates the client must explicitly opt in by opening a `subscriptions/listen` stream with `toolsListChanged` set in its `SubscriptionFilter`. `notifications/tools/list_changed` is delivered only on such a stream — there is no unsolicited push and no standalone listening channel, since the old HTTP GET stream was removed in this revision. Servers should also return their tools in a deterministic order so that byte-identical lists compare equal and caches and diffs do not churn. Note that `tools/call` results are not cacheable at all.
code
json · 13 lines{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resultType": "complete",
"ttlMs": 300000,
"cacheScope": "private",
"tools": [
{ "name": "list_projects", "inputSchema": { "type": "object" } },
{ "name": "search_issues", "inputSchema": { "type": "object" } }
]
}
}go deeper
Know that tools/list results come with a ttlMs telling you how long to cache them, and that you re-call tools/list when it expires.
Explain both freshness paths — TTL-based re-fetch, and the opt-in subscriptions/listen stream with toolsListChanged — and what cacheScope public versus private means.
Show you would key caches by credential when cacheScope is private, sort tools deterministically to avoid false change signals, and know a tools/call result is not cacheable at all.
Own the freshness policy across a fleet: TTL budgets against provisioning latency, whether a shared gateway cache is safe, and how tool-surface churn interacts with user approval of what a server exposes.
## Why freshness is a real problem now Before revision 2026-07-28, a client connected, ran the `initialize` handshake, and fetched the tool list once for the life of the connection. That connection *was* the cache lifetime. MCP 2026-07-28 removed the handshake and made the protocol stateless — every request is self-contained and an open connection is not a conversation. So "how long is my tool list good for?" stopped being answered by the connection and became an explicit part of the result. ## The pull path: ttlMs and cacheScope `ListToolsResult` extends `CacheableResult`, which adds two required fields: - **`ttlMs`** — how many milliseconds the result may be considered valid. When it lapses, the client re-issues `tools/list`. - **`cacheScope`** — `"public"` or `"private"`. `"public"` means the result does not depend on who asked, so a shared cache (a gateway, a multi-user host) may serve it to anyone. `"private"` means it does depend on the caller — typically because the tool set varies by the authorization presented — and must be cached per credential, never shared. Getting `cacheScope` wrong is a security bug, not a performance bug: marking an authorization-dependent list `"public"` lets one user's cached view leak tools another user should not see. Choosing `ttlMs` is ordinary cache engineering. A short TTL means chatty clients; a long one means stale tools. A server whose tool set is generated from a config file that changes weekly can afford minutes; one whose tools are provisioned per customer in real time should either use a short TTL or lean on the push path. ## The push path: subscriptions/listen A client that wants to know *promptly* opens a long-lived `subscriptions/listen` POST and sets `toolsListChanged` in the `SubscriptionFilter`. Only then may the server send `notifications/tools/list_changed` on that stream. Two consequences interviewers look for: 1. **There is no unsolicited push.** A server cannot decide to notify a client that never asked. If you did not open the stream and set the flag, your only freshness mechanism is the TTL. 2. **There is no other channel.** The standalone HTTP `GET` listening stream that older revisions used for server-initiated messages was removed in 2026-07-28. Request-scoped notifications like `notifications/progress` still travel on their own request's response stream, but list-changed notifications travel only on the subscription stream. The notification is a nudge, not a payload: it says the list changed, and the client responds by calling `tools/list` again. It does not carry the new tools. ## Deterministic ordering Servers should return tools in a stable, deterministic order. It sounds cosmetic and is not. Clients hash or diff the list to decide whether anything actually changed, gateways may cache the serialised body, and hosts show tools to users in the order given. A server that iterates an unordered map re-orders the array on every call, so every refresh looks like a change: caches miss, diffs are noise, and any "has this server's tool surface changed since I approved it?" check fires constantly. Sort by name, or by any fixed key, and keep it. ## What is not cacheable `CallToolResult` does not extend `CacheableResult` — a `tools/call` result carries no `ttlMs` or `cacheScope`. That is deliberate: an invocation may have side effects and reflects a specific moment. Any memoisation of tool calls is an application-level decision the protocol has not sanctioned. ## Putting it together in a real client A well-behaved 2026-07-28 client typically: 1. calls `server/discover` (which is also a cacheable result) and `tools/list`, honouring both TTLs; 2. keys the cache by server *and* by the credential in use whenever `cacheScope` is `"private"`; 3. opens one `subscriptions/listen` stream with `toolsListChanged` if it needs promptness, and treats a notification as "refetch", not "apply"; 4. falls back to plain TTL expiry when it has no subscription — for example on a short-lived stdio process where opening a listen stream is not worth it; 5. re-issues `tools/list` rather than trusting anything it cached past its TTL, because there is no session that would have kept it warm. ## Interview framing The give-away weak answer is "the client fetches the tool list once at initialize and the server pushes changes when they happen". Both halves belong to the pre-2026-07-28 world: there is no `initialize`, and pushes require an opted-in subscription.
- When should a server set cacheScope to private rather than public on tools/list?Whenever the list it returned depends on the authorization presented — different scopes or tenants seeing different tools. `"public"` promises the answer is caller-independent, so a shared cache in a gateway or multi-user host may hand it to anyone. Marking an authorization-dependent list public leaks one caller's tool view to another; when in doubt, private.
- A client never opens a subscriptions/listen stream. How does it learn the tool list changed?Only by the TTL expiring and re-calling `tools/list`. `notifications/tools/list_changed` is delivered exclusively on a `subscriptions/listen` stream opened with `toolsListChanged`, and revision 2026-07-28 removed the standalone GET listening stream, so there is no unsolicited push path left. Freshness then equals whatever `ttlMs` the server advertised.
- What does a client do when it receives notifications/tools/list_changed?Treat it as an invalidation signal: drop the cached list and call `tools/list` again. The notification announces that the set changed; it does not carry the new tools. A client that tries to infer the delta from the notification alone has nothing to work with, and one that ignores it keeps a stale surface until its TTL lapses.
saying these in an interview costs you the question
- Saying the client fetches tools once at initialize
- Expecting change notifications without opening a subscription
- Caching a private tools/list across users
- Assuming tools/call results carry ttlMs too
- Returning tools in map-iteration order that shuffles per call