Skip to content

fix(mcp): emit the SEP-2549 cache hints 2026-07-28 clients require - #241

Merged
tenequm merged 2 commits into
mainfrom
fix/list-result-cache-hints
Sep 14, 2026
Merged

tenequm merged 2 commits into
mainfrom
fix/list-result-cache-hints

Conversation

@tenequm

@tenequm tenequm commented Sep 12, 2026 •

Copy link
Copy Markdown
Owner

Summary

A client that negotiates protocol 2026-07-28 gets zero tools and no readable resources from pond, while the connection itself looks perfectly healthy. SEP-2549 makes ttlMs and cacheScope required on every cacheable result, pond omits both, and a strict client rejects the whole response.

Observed in production against pond serve 0.17.2, from Claude Code 2.1.258 (~/.cache/claude-cli-nodejs/<project>/mcp-logs-pond/):

"Successfully connected (transport: http) in 163ms"
"negotiatedProtocolVersion":"2026-07-28"
...
{"error":"Failed to fetch tools: Invalid result for tools/list: [
   {"path":["ttlMs"],      "message":"expected number, received undefined"},
   {"path":["cacheScope"], "message":"expected one of \"public\"|\"private\""} ]}
{"error":"Failed to fetch resources: Invalid result for resources/list: [ ...same... ]}

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 breaks schema://pond-sql - the doc pond_sql tells agents to read first - and every pond-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:

Surface pond's handler Hints before
tools/list override (tool limits, pond_search rewrite) none
resources/list override (the three resources) none
resources/read override (docs, stats, exports) none
resources/templates/list rmcp default (handler/server.rs:396) none

Every one builds its result with with_all_items / ReadResourceResult::new / ::default(), which leave ttl_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, and schema/2026-07-28/schema.ts makes both CacheableResult fields 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-28 in the first place. Before that, clients fell back to 2025-11-25 and 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-28 exactly 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 - public for schema://pond and schema://pond-sql (identical for every caller), private for stats://pond and pond-sql-export:// (the store's own data, which a shared cache must not hand to another caller).
  • list_resource_templates is overridden to return the empty list with hints, since rmcp's default carries none.

ttlMs is 0 (immediately stale) everywhere, via CACHE_HINT_TTL_MS. Nothing listed changes within one process, but a shared gateway may serve a public result across pond instances, whose config (the pond_search surface) and data differ. 0 also matches what clients did before: no hint meant no caching, so request volume is unchanged.

server/discover needed 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 at 2026-07-28.
  • legacy_results_omit_the_cache_hints -- both unset on the classic handshake.

Verified to fail without the fix: with cache_hints forced to (None, None), the modern test reports all five surfaces unset.

Note why the existing discovery_startup_exposes_pond_tools stayed green through all of this: it asserts via list_all_tools(), which returns only the items, and rmcp's own client parses ttlMs / cacheScope as 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 test all green.
  • End to end with a real strict client: this branch's pond serve against Claude Code 2.1.270, negotiating 2026-07-28, sees all four tools, three resources, and reads schema://pond-sql and stats://pond successfully. On the first commit alone, the reads still failed with Invalid 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.

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.
@tenequm tenequm self-assigned this Sep 14, 2026
@tenequm
tenequm merged commit ed1f932 into main Sep 14, 2026
12 checks passed
@tenequm
tenequm deleted the fix/list-result-cache-hints branch September 14, 2026 12:32
@tenequm tenequm mentioned this pull request Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant