Skip to content

Add Opt-In ttlMs / cacheScope Cache Hints to List and Read Results per SEP-2549#436

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:list_result_ttl
Open

Add Opt-In ttlMs / cacheScope Cache Hints to List and Read Results per SEP-2549#436
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:list_result_ttl

Conversation

@koic

@koic koic commented Jul 2, 2026

Copy link
Copy Markdown
Member

Motivation and Context

SEP-2549 (modelcontextprotocol/modelcontextprotocol#2549, merged for the 2026-07-28 spec release) adds a CacheableResult shape with ttlMs (freshness lifetime in milliseconds, max-age semantics, 0 = do not cache) and cacheScope ("public" or "private", whether shared intermediaries may cache) to ListToolsResult, ListPromptsResult, ListResourcesResult, ListResourceTemplatesResult, and ReadResourceResult, complementing the listChanged notifications.

The field shape follows the Python SDK's implementation (python-sdk#2824, ttlMs + cacheScope with ttlMs: 0 / cacheScope: "public" defaults; the TypeScript SDK's earlier ttl-seconds prototype #2070 was superseded by this shape). Because this SDK negotiates protocol versions up to 2025-11-25, where the fields do not exist, emission is opt-in: when neither ttl_ms nor cache_scope is configured, responses are serialized exactly as before. The fields become REQUIRED in 2026-07-28; a follow-up can auto-emit defaults once that version enters SUPPORTED_STABLE_PROTOCOL_VERSIONS.

  • MCP::Server.new gains ttl_ms: and cache_scope: keywords with validating writers modeled on page_size= (ttl_ms must be nil or a non-negative Integer; cache_scope must be nil, "public", or "private").
  • A private apply_cache_metadata wraps the five emission points. When the user sets only one of the two values, the other is filled with the spec defaults (ttlMs: 0, the only universally safe freshness value, and cacheScope: "public", the spec and Python default).
  • A resources_read_handler may return a full { contents:, ttlMs:, cacheScope: } hash to override the server-level hints per result; the documented bare-contents return shape keeps working unchanged.
  • The client's ListToolsResult / ListPromptsResult / ListResourcesResult / ListResourceTemplatesResult structs gain ttl_ms and cache_scope members populated from the response fields.

Resolves #387.

How Has This Been Tested?

New tests in test/mcp/server_test.rb:

  • with ttl_ms: 5000, all four list results carry ttlMs: 5000 and the filled cacheScope: "public" default
  • with only cache_scope: "private", resources/read carries ttlMs: 0
  • with neither configured, list and read results omit both fields (wire-format regression)
  • the hints appear alongside nextCursor when paginating
  • a resources_read_handler returning { contents:, ttlMs: 60_000 } overrides the server-level value while the missing cacheScope is filled
  • invalid writer values (-1, 1.5, "internal") raise ArgumentError

New tests in test/mcp/client_test.rb assert all four list result structs expose ttl_ms / cache_scope, and that both are nil when the server omits the fields.

Breaking Changes

None. Emission is opt-in, the new constructor keywords default to nil, and the client structs gain additive members.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

…s per SEP-2549

## Motivation and Context

SEP-2549 (modelcontextprotocol/modelcontextprotocol#2549, merged for the 2026-07-28 spec release) adds a `CacheableResult`
shape with `ttlMs` (freshness lifetime in milliseconds, max-age semantics, 0 = do not cache) and `cacheScope`
(`"public"` or `"private"`, whether shared intermediaries may cache) to `ListToolsResult`, `ListPromptsResult`,
`ListResourcesResult`, `ListResourceTemplatesResult`, and `ReadResourceResult`, complementing the `listChanged` notifications.

The field shape follows the Python SDK's implementation (python-sdk#2824, `ttlMs` + `cacheScope` with `ttlMs: 0` /
`cacheScope: "public"` defaults; the TypeScript SDK's earlier `ttl`-seconds prototype #2070 was superseded by this shape).
Because this SDK negotiates protocol versions up to 2025-11-25, where the fields do not exist, emission is opt-in:
when neither `ttl_ms` nor `cache_scope` is configured, responses are serialized exactly as before. The fields become REQUIRED
in 2026-07-28; a follow-up can auto-emit defaults once that version enters `SUPPORTED_STABLE_PROTOCOL_VERSIONS`.

- `MCP::Server.new` gains `ttl_ms:` and `cache_scope:` keywords with validating writers modeled on `page_size=`
  (`ttl_ms` must be nil or a non-negative Integer; `cache_scope` must be nil, `"public"`, or `"private"`).
- A private `apply_cache_metadata` wraps the five emission points. When the user sets only one of the two values,
  the other is filled with the spec defaults (`ttlMs: 0`, the only universally safe freshness value, and `cacheScope: "public"`,
  the spec and Python default).
- A `resources_read_handler` may return a full `{ contents:, ttlMs:, cacheScope: }` hash to override the server-level hints per result;
  the documented bare-contents return shape keeps working unchanged.
- The client's `ListToolsResult` / `ListPromptsResult` / `ListResourcesResult` / `ListResourceTemplatesResult` structs gain
  `ttl_ms` and `cache_scope` members populated from the response fields.

Resolves modelcontextprotocol#387.

## How Has This Been Tested?

New tests in `test/mcp/server_test.rb`:

- with `ttl_ms: 5000`, all four list results carry `ttlMs: 5000` and the filled `cacheScope: "public"` default
- with only `cache_scope: "private"`, `resources/read` carries `ttlMs: 0`
- with neither configured, list and read results omit both fields (wire-format regression)
- the hints appear alongside `nextCursor` when paginating
- a `resources_read_handler` returning `{ contents:, ttlMs: 60_000 }` overrides the server-level value while the missing `cacheScope` is filled
- invalid writer values (`-1`, `1.5`, `"internal"`) raise `ArgumentError`

New tests in `test/mcp/client_test.rb` assert all four list result structs expose `ttl_ms` / `cache_scope`,
and that both are nil when the server omits the fields.

## Breaking Changes

None. Emission is opt-in, the new constructor keywords default to nil, and the client structs gain additive members.
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