Cache-hint policy for cacheable MCP results.
The server emits ttlMs and cacheScope hints on modern complete results.
It does not cache responses itself. Without a :cache_policy option or a
definition's :cache policy the
2.3 behaviour is kept: :cache_ttl_ms (default 30_000) and a private
scope, with "public" only when cache_scope: :public,
allow_public_cache: true, and the dispatch context's trusted
:public_catalog flag are all present.
Granular policy
cache_policy: [
methods: %{
"server/discover" => [ttl_ms: 300_000],
"tools/list" => [ttl_ms: 60_000, scope: :public],
"resources/read" => [ttl_ms: 5_000]
},
resolver: {MyApp.MCPCache, :policy, []},
max_ttl_ms: 600_000
]:methods accepts the standard cacheable operations: server/discover,
tools/list, prompts/list, resources/list, resources/templates/list,
and resources/read. A resource or resource-template definition may carry
its own cache: [ttl_ms: ..., scope: ...] policy for resources/read, even
without a server-level :cache_policy option. Each
policy may set :ttl_ms (a non-negative integer), :scope (:private or
:public), or both; an unset field inherits the lower-precedence value.
The optional :resolver callback receives
%{method: method, definition: nil | %{type: type, identity: identity}, policy: policy}
and the trusted request context. It returns {:ok, policy} to override the
fields it sets or :default to keep the static result. It runs in the
request worker under the request deadline. An exception, exit, throw, or
invalid return produces a zero-TTL private hint for that response; it never
falls back to a public or longer-lived policy.
Resolution order is: global default, method policy, definition policy,
resolver output, then valid handler hints on resources/read (which may
shorten ttlMs or select "private" but cannot lengthen or promote), and
finally the mandatory constraints below.
Mandatory constraints
These apply with or without a granular policy:
- Interim
input_requiredresults carry no hints, and complete results from a multi-round retry usettlMs: 0and"private". "public"requiresallow_public_cache: trueand an explicit host choice (a granular:publicpolicy, or the 2.3 options above), and the complete response must be caller-independent. A catalog is caller-independent only when every registered definition of that type has no required scopes, alternative scope sets, orauthorizecallback, no HTTP definition policy applies, and no presentation callback is configured. Paginated catalogs stay private because their cursors bind the caller. A resource read additionally requires the selected definition to be unrestricted and its metadata to use only the configured constant implementation identity. Otherwise the scope is"private". Method-level scope requirements do not make a result caller-dependent, so a public hint lets shared caches serve it to callers without those scopes.- Output from
:instructions_provideror:tool_presentationis private, and its TTL is0unless a method policy, definition policy, or resolver explicitly supplies one. - A list page carrying
nextCursoris never fresher than the cursor's lifetime, and a private hint is never fresher than the remaining lifetime of the verified access token in:attesto_mcp_claims. - TTLs never exceed
:max_ttl_msor the largest JSON-safe integer.
All pages of one list request receive the same scope; continuation cursors are bound to it and are rejected if the effective scope changes.
MCP cache hints do not change HTTP caching. Protected HTTP responses keep
Cache-Control: private, no-store.
Summary
Functions
Validates a static or resolver-supplied policy.
Types
@type policy() :: %{ttl_ms: non_neg_integer() | nil, scope: String.t() | nil}
A normalized policy; nil fields inherit lower-precedence values.