Repository navigation
fix(mcp): emit the SEP-2549 cache hints 2026-07-28 clients require - #241
Merged
Merged
Conversation
A client negotiating protocol 2026-07-28 gets zero tools and zero resources from pond while the connection itself looks healthy: SEP-2549 makes ttlMs and cacheScope required on list results at that version, and a strict client (Claude Code) rejects the whole response without them. rmcp stamps the hints in its handler macros (rust-sdk#1120, in 3.1.1), but pond overrides both list_tools and list_resources - to annotate tool limits and to serve the schema:// resources - and the overrides build their result with with_all_items, which hardcodes ttl_ms: None and cache_scope: None. list_tools discarded the RequestContext outright, which is what the macro reads to make the decision. Introduced in v0.17.1 by #223 (rmcp 1.7 -> 3.2.0), the bump that made pond advertise 2026-07-28 at all; before it, clients fell back to 2025-11-25 and never asked for the fields. ttl_ms = 0 matches rmcp's own choice and is right for pond regardless: the tool surface varies with instance config, so a cached list would advertise the wrong one. Legacy clients keep the pre-SEP wire shape. The existing discovery test could not catch this - it asserts through list_all_tools(), and rmcp's own client parses both fields as optional - so the new tests assert on the envelope instead.
…/list
The first commit covered tools/list and resources/list only. SEP-2549
makes ttlMs and cacheScope required on every cacheable result, and
Claude Code 2.1.270 still rejected every resources/read against it
("Invalid result for resources/read: Invalid input for ttlMs"), leaving
schema://pond-sql - the doc pond_sql tells agents to read first - and
every pond-sql-export:// artifact unreadable. resources/templates/list
fell back to rmcp's default, which carries no hints either.
read_resource now stamps its result once after the match: public for the
two static schema docs, private for stats://pond and exports, which are
the store's own data. list_resource_templates is overridden to return
the empty list with hints.
cache_hints() returns the (ttl_ms, cache_scope) pair so the TTL policy
lives on one constant rather than at each call site, and its reason is
corrected: nothing listed changes within a process, but a shared gateway
may serve a public result across instances.
The tests share one connect() helper with the discovery test and now
assert all five surfaces, including one read per scope arm.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A client that negotiates protocol
2026-07-28gets zero tools and no readable resources from pond, while the connection itself looks perfectly healthy. SEP-2549 makesttlMsandcacheScoperequired on every cacheable result, pond omits both, and a strict client rejects the whole response.Observed in production against
pond serve0.17.2, from Claude Code 2.1.258 (~/.cache/claude-cli-nodejs/<project>/mcp-logs-pond/):It retries three times, gives up, and the agent behaves as if pond is not wired at all. The same schema check rejects
resources/read, which is what breaksschema://pond-sql- the docpond_sqltells agents to read first - and everypond-sql-export://artifact.Root cause
rmcp already fixed this for servers that use the documented handler macros -- rust-sdk#1114, fixed by rust-sdk#1120, shipped in rmcp 3.1.1. pond is on 3.2.0, so it has the fix.
It just never reaches the wire, because pond overrides the handlers the fix lives in, or relies on rmcp defaults that never got it:
tools/listpond_searchrewrite)resources/listresources/readresources/templates/listhandler/server.rs:396)Every one builds its result with
with_all_items/ReadResourceResult::new/::default(), which leavettl_ms: None, cache_scope: None; rmcp's own doc on those fields says "Required by spec version 2026-07-28". The spec requires them on all four: caching says servers MUST include hints on them, andschema/2026-07-28/schema.tsmakes bothCacheableResultfields non-optional.So this is pond's bug, not rmcp's; no rmcp bump helps. It was introduced in v0.17.1 by #223 (rmcp 1.7 -> 3.2.0), which is what made pond advertise
2026-07-28in the first place. Before that, clients fell back to2025-11-25and never asked for the fields.What changed
cache_hints(&context, scope)returns the(ttl_ms, cache_scope)pair, gated on the negotiated version>= 2026-07-28exactly like #1120. Each handler assigns it in one line. Legacy clients keep the exact pre-SEP wire shape - both fields unset.tools/list,resources/list,resources/templates/list:public.resources/read: stamped once after the match -publicforschema://pondandschema://pond-sql(identical for every caller),privateforstats://pondandpond-sql-export://(the store's own data, which a shared cache must not hand to another caller).list_resource_templatesis overridden to return the empty list with hints, since rmcp's default carries none.ttlMsis0(immediately stale) everywhere, viaCACHE_HINT_TTL_MS. Nothing listed changes within one process, but a shared gateway may serve apublicresult across pond instances, whose config (thepond_searchsurface) and data differ.0also matches what clients did before: no hint meant no caching, so request volume is unchanged.server/discoverneeded nothing - rmcp already stamps it.Tests
One
connect(modern)helper now backs both the existing discovery test and the new ones.cache_hints_seen(modern)collects(ttl_ms, cache_scope)from all five cacheable calls - the three lists plus one read per scope arm:modern_results_carry_the_required_cache_hints-- the expected pair on every surface at2026-07-28.legacy_results_omit_the_cache_hints-- both unset on the classic handshake.Verified to fail without the fix: with
cache_hintsforced to(None, None), the modern test reports all five surfaces unset.Note why the existing
discovery_startup_exposes_pond_toolsstayed green through all of this: it asserts vialist_all_tools(), which returns only the items, and rmcp's own client parsesttlMs/cacheScopeas optional. Only a strict client notices, so the new tests assert on the envelope rather than the tool names.Validation
cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo testall green.pond serveagainst Claude Code 2.1.270, negotiating2026-07-28, sees all four tools, three resources, and readsschema://pond-sqlandstats://pondsuccessfully. On the first commit alone, the reads still failed withInvalid result for resources/read: Invalid input for ttlMs (expected number) and cacheScope (expected "public" or "private").Release note
Clients that negotiate MCP protocol 2026-07-28, such as recent Claude Code, see pond's tools again and can read its resources. pond omitted the ttlMs and cacheScope fields that version requires, so strict clients rejected its lists and resource reads.