You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Stop later-revision result fields from failing pre-2026 sessions
A client that negotiated a pre-2026 protocol version failed
`list_tools()` (and every other cacheable list/read) with a
ValidationError whenever the server put a `cacheScope` value on the
result that the 2026-07-28 enum doesn't allow. Those cache-hint fields
aren't part of a 2025 session's schema at all, so the call should
never have been able to fail on them.
Inbound results are validated against the negotiated version's
schema-exact surface and then parsed into the version-free result
models, which carry every revision's fields. Because that second step
was fed the raw wire dict, a field belonging to a newer revision than
the one negotiated (`ttlMs`, `cacheScope`, `resultType`) still reached
the version-free model, and that revision's constraints fired on a
session that never negotiated the field.
Add `strip_era_foreign_fields` to `mcp_types.methods` and apply it at
every point that parses a result into the version-free models. It
removes exactly the keys a newer revision's surface declares for the
method but the negotiated version's does not, derived from the
surfaces themselves. Keys no surface knows about (extension
result-claim payloads) pass through untouched, legal fields a strict
surface merely omits (`_meta` on an empty result) are never treated
as foreign, and on a session speaking the newest revision nothing is
stripped.
A legacy server that put valid hint keys on the wire previously leaked
those values onto the model even though the cache never consulted
them there; they now show the documented defaults consistently, so
the caching page and its wire-presence tip are updated to match.
Copy file name to clipboardExpand all lines: docs/client/caching.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -101,7 +101,7 @@ Cache keys also carry the **server's identity**: the URL string you dialed, with
101
101
102
102
The hints are also plain fields on every cacheable result (`result.ttl_ms` and `result.cache_scope`, already parsed), in case you want to layer your own bookkeeping on top of (or instead of) the built-in cache.
103
103
104
-
Against an **older server** (pre-2026 protocol), the fields are simply absent from the wire, and the models show their conservative defaults: `ttl_ms == 0` and `cache_scope == "private"`, stale and unshared, the right assumption for a server that declared nothing. The cache treats a legacy session the same way: hints are never consulted there (whatever keys appear on the wire), only `default_ttl_ms` applies, and its default of `0` caches nothing, so a pre-2026 connection behaves exactly as it did before the cache existed. If you need to distinguish "the server said 0" from "the server said nothing", check `"ttl_ms" in result.model_fields_set`: it's only set when the field actually arrived.
104
+
Against an **older server** (pre-2026 protocol), the fields are outside that session's schema, so the models always show their conservative defaults: `ttl_ms == 0` and `cache_scope == "private"`, stale and unshared, the right assumption for a server that declared nothing. This holds even if a legacy server puts `ttlMs`/`cacheScope` keys on the wire anyway: those fields belong to a later protocol revision than the one negotiated, so the client drops them before parsing, and a smuggled hint (even a value the 2026-07-28 enum would reject) never reaches the model. The cache treats a legacy session the same way: hints are never consulted, only `default_ttl_ms` applies, and its default of `0` caches nothing, so a pre-2026 connection behaves exactly as it did before the cache existed.
0 commit comments