Skip to content

feat(client,server): implement TTL and client cache for list and resource operations (SEP-2549) - #1123

Open
slachiewicz wants to merge 6 commits into
modelcontextprotocol:mainfrom
slachiewicz:feat/sep-2549-client-cache
Open

feat(client,server): implement TTL and client cache for list and resource operations (SEP-2549)#1123
slachiewicz wants to merge 6 commits into
modelcontextprotocol:mainfrom
slachiewicz:feat/sep-2549-client-cache

Conversation

@slachiewicz

@slachiewicz slachiewicz commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Motivation and Context

Implements SEP-2549 (TTL & Cache Scope for List and Resource Operations), fully addressing #1009.

This PR incorporates and builds upon the schema and server-side work from @aboullaite in #1062, resolving merge conflicts against main (specifically reconciling with the recently merged list filtering from #1111), and delivers the complete client-side caching layer with notification-driven invalidation.

Fixes #1009
Supersedes #1062

Summary of Changes

  1. Schema Layer (SEP-2549):

    • Added ttlMs (Long, non-negative) and cacheScope (CacheScope.PUBLIC / CacheScope.PRIVATE) to cacheable result records: ListToolsResult, ListPromptsResult, ListResourcesResult, ListResourceTemplatesResult, and ReadResourceResult.
    • Maintained full binary and wire-format backwards compatibility with @Deprecated constructors and builder support.
  2. Server-Side Default Stamping:

  3. Client-Side Cache (McpClientCache):

    • Implemented thread-safe, in-memory McpClientCache with TTL-based expiration.
    • Transparently integrated with McpAsyncClient for tools/list, prompts/list, resources/list, resources/templates/list, and resources/read.
    • Automatic cache eviction on corresponding change notifications (notifications/tools/list_changed, notifications/resources/list_changed, notifications/resources/updated, notifications/prompts/list_changed).
  4. Testing:

    • 254 lines of schema serialization, deserialization, and boundary tests in McpSchemaTests.
    • 356 lines of cache unit and integration tests in McpClientCacheTests.
    • Formatted with spring-javaformat:0.0.47.

Co-authored-by: Mohammed Aboullaite maboullaite@spotify.me

Mohammed Aboullaite and others added 5 commits September 2, 2026 22:24
The draft spec introduces TTL caching hints on list and read results.
This adds the optional ttlMs (Integer) and cacheScope (CacheScope enum)
fields to ListToolsResult, ListPromptsResult, ListResourcesResult,
ListResourceTemplatesResult, and ReadResourceResult, following the
wire-record evolution rules in CONTRIBUTING.md.

Purely additive, schema-layer only: no server or client behavior changes.
Server-side TTL configuration hooks are deferred to modelcontextprotocol#578.
The draft spec requires cacheable results to carry ttlMs and cacheScope
on the wire. Other SDKs (Go, Python, TypeScript) all stamp defaults
after handlers return: ttlMs=0 (immediately stale) and cacheScope=public.

This adds the same default stamping in McpAsyncServer and
McpStatelessAsyncServer for all five cacheable result types. For list
results the defaults are set at the build site. For ReadResourceResult,
which is built by user handlers, a withCacheDefaults helper stamps
missing fields while preserving any values the handler set explicitly.
Two fixes from review:

1. Widen ttlMs from Integer to Long across all five cacheable result
   records. The spec defines ttlMs as a non-negative integer with no
   upper bound, and Jackson rejects values exceeding Integer.MAX_VALUE.
   Long handles any realistic TTL without interop failures.

2. Default cacheScope to PRIVATE (not PUBLIC) for resources/read in
   both server classes. The spec's caching guidance says resources/read
   results that depend on the authenticated user should be private.
   List results keep the PUBLIC default since they are not user-specific.
…rations (SEP-2549)

- Add thread-safe McpClientCache with TTL support and typed invalidations
- Integrate caching with McpAsyncClient for tools, prompts, resources, and resource templates
- Invalidate cache entries on corresponding change/update notifications
- Add comprehensive unit and integration tests in McpClientCacheTests
@slachiewicz
slachiewicz marked this pull request as draft September 3, 2026 08:34
…ing configurable

Listings are scoped private, not public. tools/list is filtered per caller
(modelcontextprotocol#1111), so a shared cache must never serve one principal's listing to
another; do not restore PUBLIC without gating it on the absence of a list
filter.

Client-side, the cache lookup moved inside Mono.defer so an assembled Mono
stays cold, a generation counter drops a response that was in flight when its
group was invalidated, and a listing spanning several pages is not cached at
all, because stitching a cached first page to a freshly fetched later page
mixes two server snapshots.

ttlMs is validated on the result builders rather than in the compact
constructor: a bad server hint must not make the whole listing unparseable.

McpClientCacheStore keeps a Caffeine or shared-store implementation a user
choice, so mcp-core stays at its three compile dependencies.
@slachiewicz

Copy link
Copy Markdown
Contributor Author

Pushed 07e8a0c2, addressing a self-review of this branch.

The load-bearing fixes: cache lookups moved inside Mono.defer (an assembled Mono was replaying a snapshot past its TTL); a generation counter so a response already in flight when *_changed arrives is not stored; entries dropped on re-initialization and close; TTL clamped at 24h with saturating addition; resources/read keys include _meta; the store is bounded at 512 entries. A listing spanning several pages is no longer cached at all — stitching a cached first page to a freshly fetched later page mixes two server snapshots.

Listings are scoped private, not public: tools/list is filtered per caller since #1111, so a shared cache must never serve one principal's listing to another.

Two additions, because the caching was otherwise unreachable or unavoidable:

  • listCache(Duration, CacheScope) on all four server builders. Listings hardcoded ttlMs: 0, so the server half of SEP-2549 could not be switched on.
  • enableResultCaching(boolean), invalidateCache(), and an McpClientCacheStore SPI on the client. The bundled in-memory store stays the default, so backing the cache with Caffeine is a user choice and mcp-core keeps its three compile dependencies.

ttlMs is now validated non-negative on the result builders, which the description already claimed. Deserialization stays lenient — a bad server hint should not make a whole listing unparseable.

Docs for both hooks are in docs/server.md and docs/client.md.

Verified: ./mvnw test → BUILD SUCCESS, all 11 modules, 0 failures.

This comment was created with AI assistance.

@slachiewicz
slachiewicz marked this pull request as ready for review September 3, 2026 09:09
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.

SEP-2549: TTL for List Results

1 participant