Drop later-revision cache-hint fields on pre-2026 sessions
A client that negotiated a pre-2026 protocol version fails list_tools() (and every other cacheable list/read) with a ValidationError when the server puts a cacheScope value on the result that the 2026-07-28 enum doesn't recognise — e.g. a server that has moved to a newer scheme, or a non-conformant one. On a 2025 session that field isn't part of the negotiated schema at all, and the client already treats these hints as inert on legacy sessions, so the call shouldn't be able to fail on it.
Motivation and Context
ClientSession.send_request validates a result against the negotiated version's schema-exact surface (the version gate), then parses the same raw wire dict into the version-free result type, which carries every revision's fields. So a field belonging to a later revision than the one negotiated (ttlMs, cacheScope, resultType at 2025-11-25) still reached that type, and its 2026-07-28 constraints (closed Literal, ge=0) fired on a session that never negotiated the field — even though the surface had already ruled the key out-of-schema.
send_request now drops exactly the keys a newer revision's surface declares for the method but the negotiated version's does not, before parsing. The set is derived from the surfaces themselves, so 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 dropped. The helpers are private and sit next to the existing _clamp_inbound_ttl, which handles the same class of concern.
Not changed here: on a modern (2026-07-28) session cacheScope is in-schema and the enum stays enforced, so an unrecognised value there still rejects the result. Whether an advisory hint should fail closed rather than fatally in-era is a separate policy question.
How Has This Been Tested?
- New parametrized client test: a handshake (2025) session receives every legacy-reachable cacheable result (
tools/list,prompts/list,resources/list,resources/templates/list,resources/read) carryingttlMs: -1andcacheScope: "session", and each call succeeds with the defaults and neither hint inmodel_fields_set. - Full suite green at 100% coverage with
strict-no-coverclean. - End-to-end against a raw (non-SDK) stdio JSON-RPC server driven through
Client(stdio_client(...)): a 2025-06-18 session with a badcacheScope, garbageresultType, and a vendor key now completeslist_tools()/call_tool(); the identical script againstmainreproduces the reportedValidationError. A 2026-07-28 session sending the same bad value still rejects, as expected.
Breaking Changes
None to public signatures. Observable behaviour change: on a pre-2026 session a result no longer carries ttl_ms/cache_scope/result_type values a server smuggled onto the wire — they were outside that session's schema and are now dropped, so the models show their documented defaults (which the response cache never consulted on legacy sessions anyway).
Types of changes
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing functionality to change)
- Documentation update
Checklist
- I have read the MCP Documentation
- My code follows the repository's style guidelines
- New and existing tests pass locally
- I have added appropriate error handling
- I have added or updated documentation as needed
Additional context
The fix is deliberately client-local. The public parse_server_result/parse_client_result helpers in mcp_types.methods share the same parse shape and still hit this on a legacy body with a bad hint; that's left for a follow-up rather than widened into this change.