List TTL Migration
Migrating to SEP-2549 ttlMs / cacheScope on list responses.
Source: docs/LIST_TTL_MIGRATION.md
List Results TTL Migration Guide ¶
This guide covers SEP-2549 (TTL for List Results), merged Final on the MCP
specification on 2026-05-15. The SEP adds two cache-control fields to result
objects, explains how mcpkit ships them, and walks through what server
authors need to know about cache scope and authorization.
Status: SEP-2549 is merged Final. mcpkit first shipped an implementation
of an earlier draft (commit256c243, 2026-04-30); the field renamed and
grew a sibling during the spec’s final review, so the pre-merge API is a
breaking step away from what shipped here. See “Migrating from the
pre-merge implementation” below.
TL;DR ¶
ttlMs(integer milliseconds) andcacheScope("public"/"private")
are added to five result types:tools/list,prompts/list,
resources/list,resources/templates/list, andresources/read.- Set both at server registration with
server.WithListCacheControl(ttlMs, scope)
for the four list endpoints andserver.WithReadResourceCacheControl(ttlMs, scope)
forresources/read. - A
resources/readhandler MAY override either hint per-read by setting
core.ResourceResult.TTLMs/.CacheScopeon its return value. - Clients read the hints off the typed envelope:
client.ListToolsPageand
its three siblings, plusclient.ReadResourceFull.
Wire fields ¶
| Type | Go field | JSON tag | Meaning |
|---|---|---|---|
core.ToolsListResult |
TTLMs *int |
ttlMs,omitempty |
freshness window in ms |
core.PromptsListResult |
TTLMs *int |
ttlMs,omitempty |
freshness window in ms |
core.ResourcesListResult |
TTLMs *int |
ttlMs,omitempty |
freshness window in ms |
core.ResourceTemplatesListResult |
TTLMs *int |
ttlMs,omitempty |
freshness window in ms |
core.ResourceResult (resources/read) |
TTLMs *int |
ttlMs,omitempty |
freshness window in ms |
| all five | CacheScope string |
cacheScope,omitempty |
"public" or "private" |
ttlMs semantics ¶
- absent or
0— the response is immediately stale; clients MAY
re-fetch every time the result is needed. Per the merged spec an absent
ttlMsis treated the same as0. > 0— the response is fresh for that many milliseconds from receipt.- negative — clients ignore it and treat it as
0.
mcpkit keeps TTLMs a *int even though absent and 0 are
client-equivalent: the pointer lets a server emit an explicit "ttlMs": 0
distinct from omitting the field. core.CacheScopePublic and
core.CacheScopePrivate are the two cacheScope constants.
cacheScope semantics ¶
cacheScope mirrors HTTP Cache-Control: public vs private:
"public"— the response holds no caller-specific data; any client,
shared gateway, or caching proxy MAY store it and serve it to any user."private"— the response holds caller-specific data; a cache MAY be
reused only within the same authorization context and MUST NOT be shared
across access tokens.- absent — clients default to
"public".
Server API ¶
// All four list endpoints, ttlMs only:
srv := server.NewServer(info, server.WithListTTLMs(60000))
// All four list endpoints, ttlMs + cacheScope in one call:
srv := server.NewServer(info,
server.WithListCacheControl(60000, core.CacheScopePublic))
// resources/read default (a read handler may override per-read):
srv := server.NewServer(info,
server.WithReadResourceCacheControl(30000, core.CacheScopePrivate))
A resource or template handler sets the per-read override on its return
value; the WithReadResourceCacheControl default fills only the fields the
handler left unset:
srv.RegisterResource(def, func(ctx core.ResourceContext, req core.ResourceRequest) (core.ResourceResult, error) {
return core.ResourceResult{
Contents: contents,
TTLMs: core.IntPtr(5000),
CacheScope: core.CacheScopePrivate,
}, nil
})
Client API ¶
The plain client.ListTools() / ReadResource() helpers drop the response
envelope. To read the cache hints, use the typed helpers:
page, _ := c.ListToolsPage("") // also ListPromptsPage / ListResourcesPage / ListResourceTemplatesPage
if page.TTLMs != nil && *page.TTLMs > 0 {
expiresAt := time.Now().Add(time.Duration(*page.TTLMs) * time.Millisecond)
// cache page.Tools until expiresAt; key by access token if page.CacheScope == "private".
}
rr, _ := c.ReadResourceFull("file:///doc") // typed core.ResourceResult with TTLMs / CacheScope
Security implications ¶
The spec’s caching.mdx utility doc carries a Security Considerations
section. Two obligations fall on server authors:
cacheScopeMUST reflect intended visibility. A"public"response
may be served across authorization contexts even when it came from an
authenticated endpoint — different access tokens can share the same
cache entry. Marking a per-user tool list as"public"leaks one user’s
primitives to another. Any response whose contents differ per
authorization context MUST be"private".- Per-primitive access controls are still required.
cacheScopeis a
hint to clients, not an enforcement mechanism. Servers MUST apply
appropriate per-primitive access controls on every request and MUST NOT
rely oncacheScopealone to prevent unauthorized access.
Worked example:
- A single-tenant Calendar server exposes the same tool set to every
caller. Itstools/listresponse is"public". - A multi-tenant CRM filters the tool list by the caller’s role, so
different users see different tools. Itstools/listresponse MUST be
"private", and the server still authorizes everytools/call.
What changed during the spec review cycle ¶
mcpkit’s pre-merge implementation tracked an earlier draft. The spec drifted
in three ways before merging Final:
ttl(integer seconds) renamed tottlMs(integer milliseconds). Driven
by maintainer pushback on units-in-the-name; the SEP author applied it
2026-05-07.cacheScopefield added 2026-05-07. It was not in the original SEP.resources/readadded to the coverage list mid-cycle (it originally
covered only the four list endpoints).
A Security Implications section landed in caching.mdx on 2026-05-14.
Migrating from the pre-merge implementation ¶
If you adopted mcpkit’s pre-merge SEP-2549 release (roughly 2026-04-30
onward), the rename is a breaking change. It is loud — old code stops
compiling rather than silently changing units.
| Pre-merge | Final |
|---|---|
server.WithListTTL(60) (seconds) |
server.WithListTTLMs(60000) (milliseconds) |
core.ToolsListResult.TTL (json:"ttl") |
.TTLMs (json:"ttlMs") |
three-state model (nil / &0 / &N) |
two client behaviors: absent-or-0 (stale) and > 0 (fresh) |
| (no cache scope) | CacheScope field + WithListCacheControl |
(no resources/read coverage) |
core.ResourceResult.TTLMs / .CacheScope + WithReadResourceCacheControl |
WithListTTL was removed rather than reinterpreted: a renamed WithListTTL
that silently switched units would have turned WithListTTL(60) from “60
seconds” into “60 milliseconds” with no compile error. Grep your codebase
for WithListTTL and .TTL on list-result types, then multiply
second-valued arguments by 1000.
References ¶
- SEP-2549 spec PR: https://github.com/modelcontextprotocol/specification/pull/2549
- Caching utility doc:
docs/specification/draft/server/utilities/caching.mdx - Example:
examples/list-ttl/