# `AttestoMCP.Server.CachePolicy`
[🔗](https://github.com/XukuLLC/attesto_mcp_server/blob/v2.8.0/lib/attesto_mcp_server/cache_policy.ex#L1)

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_required` results carry no hints, and complete results
    from a multi-round retry use `ttlMs: 0` and `"private"`.
  * `"public"` requires `allow_public_cache: true` and an explicit host
    choice (a granular `:public` policy, 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, or `authorize` callback,
    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_provider` or `:tool_presentation` is
    private, and its TTL is `0` unless a method policy, definition policy, or
    resolver explicitly supplies one.
  * A list page carrying `nextCursor` is 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_ms` or 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`.

# `policy`

```elixir
@type policy() :: %{ttl_ms: non_neg_integer() | nil, scope: String.t() | nil}
```

A normalized policy; `nil` fields inherit lower-precedence values.

# `normalize_policy`

```elixir
@spec normalize_policy(term()) :: {:ok, policy()} | {:error, :invalid_cache_policy}
```

Validates a static or resolver-supplied policy.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
