
# Caching

> Every cacheable modern result states its own freshness.

Six methods must carry `ttlMs` and `cacheScope` on the modern era:

`server/discover` · `tools/list` · `prompts/list` · `resources/list` · `resources/templates/list` · `resources/read`

The default is `{ ttlMs: 0, cacheScope: "private" }` — immediately stale, never shared — so caching is always something you opt into deliberately.

```ts
defineMcpHandler({
  name: "my-server",
  version: "1.0.0",
  cache: {
    ttlMs: 60_000,
    cacheScope: "public",
    discover: { ttlMs: 3_600_000 },
    resourceRead: { ttlMs: 5_000, cacheScope: "private" },
  },
  resources: [
    defineMcpResource({
      name: "profile",
      uri: "app:///me",
      cache: { ttlMs: 1_000, cacheScope: "private" }, // per-definition override
      handler: async (uri) => ({ contents: [{ uri: uri.toString(), text: "..." }] }),
    }),
  ],
});
```

## Precedence

From most specific to least:

1. `ttlMs` / `cacheScope` returned by the handler itself
2. per-definition `cache` on a resource or resource template
3. per-method option — `discover`, `tools`, `prompts`, `resources`, `resourceTemplates`, `resourceRead`
4. the handler-level default

Interim (`input_required`) results never carry caching hints, and legacy results never carry them at all — the fields are stripped on the legacy path even if a shared handler set them.

## Scopes

| Scope       | Meaning                                                                    |
| ----------- | -------------------------------------------------------------------------- |
| `"private"` | Cacheable only for the authorization context that fetched it. The default. |
| `"public"`  | May be shared across clients and users.                                    |

> [!WARNING]
> `cacheScope: "public"` responses may be shared across authorization contexts, even on an authenticated endpoint. If a result depends on _who_ asked — a tenant's records, a user's files, anything behind `auth` — it must stay `"private"`. A single mislabeled result is a cross-tenant leak, not a performance regression.

The safe pattern is to widen the scope per method rather than globally: `server/discover` and `tools/list` are usually identical for everyone, while `resources/read` rarely is.

```ts
cache: {
  // safe: same for every caller
  discover: { ttlMs: 3_600_000, cacheScope: "public" },
  tools: { ttlMs: 600_000, cacheScope: "public" },
  // per-user: keep private, short
  resourceRead: { ttlMs: 5_000, cacheScope: "private" },
}
```

## Dynamic TTLs

A handler can decide freshness per result — useful when you know when the underlying data next changes:

```ts
defineMcpResource({
  name: "forecast",
  uri: "app:///forecast",
  handler: async (uri) => {
    const { data, expiresInMs } = await getForecast();
    return {
      contents: [{ uri: uri.toString(), text: JSON.stringify(data) }],
      ttlMs: expiresInMs,
      cacheScope: "public",
    };
  },
});
```
