feat(client,server): implement TTL and client cache for list and resource operations (SEP-2549) - #1123
Conversation
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
…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.
|
Pushed The load-bearing fixes: cache lookups moved inside Listings are scoped Two additions, because the caching was otherwise unreachable or unavoidable:
Docs for both hooks are in Verified: This comment was created with AI assistance. |
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
Schema Layer (SEP-2549):
ttlMs(Long, non-negative) andcacheScope(CacheScope.PUBLIC/CacheScope.PRIVATE) to cacheable result records:ListToolsResult,ListPromptsResult,ListResourcesResult,ListResourceTemplatesResult, andReadResourceResult.@Deprecatedconstructors and builder support.Server-Side Default Stamping:
McpAsyncServerandMcpStatelessAsyncServer, list results default tottlMs = 0LandcacheScope = PUBLIC.ReadResourceResultdefaults missing cache fields tottlMs = 0LandcacheScope = PRIVATE(preserving explicit values set by handlers).Client-Side Cache (
McpClientCache):McpClientCachewith TTL-based expiration.McpAsyncClientfortools/list,prompts/list,resources/list,resources/templates/list, andresources/read.notifications/tools/list_changed,notifications/resources/list_changed,notifications/resources/updated,notifications/prompts/list_changed).Testing:
McpSchemaTests.McpClientCacheTests.spring-javaformat:0.0.47.Co-authored-by: Mohammed Aboullaite maboullaite@spotify.me