Skip to content

Add ttlMs and cacheScope to list and resource results with a server listCache option (SEP-2549) - #1131

Open
slachiewicz wants to merge 5 commits into
modelcontextprotocol:mainfrom
slachiewicz:feat/sep-2549-schema-server
Open

slachiewicz wants to merge 5 commits into
modelcontextprotocol:mainfrom
slachiewicz:feat/sep-2549-schema-server

Conversation

@slachiewicz

Copy link
Copy Markdown
Contributor

Closes #1009. Supersedes #1062, whose three commits are kept with @aboullaite's authorship; this PR adds the server hook on top.

Adds ttlMs and cacheScope to ListToolsResult, ListPromptsResult, ListResourcesResult, ListResourceTemplatesResult and ReadResourceResult, with deprecated constructors so existing callers compile unchanged. Servers stamp ttlMs: 0 on listings and cacheScope: private on resources/read when a handler sets neither.

McpServer builders gain listCache(Duration, CacheScope) so a server can advertise a positive TTL; without it the server half of SEP-2549 could not be switched on. Listings default to private because tools/list is filtered per caller since #1111, so a shared cache must never serve one principal's listing to another.

Two deliberate deviations from the draft schema: ttlMs is a nullable Long rather than a required field so results from older servers still deserialize, and the absent-scope default for listings is private where the schema comment says public.

The client-side cache from #1123 follows as a separate PR stacked on this one.

This change was created with AI assistance.

Mohammed Aboullaite and others added 5 commits September 10, 2026 14:44
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.
Listings previously hardcoded ttlMs to 0, so the server half of
SEP-2549 could not be switched on. Listings default to a PRIVATE
scope because tools/list has been filtered per caller since modelcontextprotocol#1111.
ttlMs is validated non-negative on the affected result builders.

Copy link
Copy Markdown

One API-level safety issue seems worth resolving before making PUBLIC easy to configure: listCache(ttl, scope) applies one cache scope to all four listing methods — tools, prompts, resources, and resource templates.

That becomes awkward once tools/list is filtered per caller. The Javadoc correctly warns that any registered tool list filter makes a public tools listing unsafe to share, but the current API cannot express a common case like:

  • resources/prompts/templates are identical for every principal → PUBLIC
  • tools are authorization-filtered per caller → PRIVATE

Calling:

.listCache(Duration.ofMinutes(5), CacheScope.PUBLIC)

currently marks all four result types public, including the filtered tools listing. A shared intermediary following the server hint could then reuse one principal's filtered tool list for another.

The warning reduces misuse, but the type/API shape still permits an unsafe combination and makes the safe mixed-scope configuration impossible.

I'd consider either:

  1. per-operation cache options (toolsListCache, resourcesListCache, etc.); or
  2. automatically force tools/list to PRIVATE whenever any tool filter is registered, with a test that a globally requested PUBLIC scope cannot leak through the filtered tools path.

A regression could configure listCache(..., PUBLIC) + a request-dependent tool filter and assert that the emitted ListToolsResult.cacheScope() is not public.

That would make the security property enforced rather than advisory while still allowing public caching for caller-invariant lists.

This branch has not been deployed

No deployments
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

2 participants