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.
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:
ttlMs / cacheScope returned by the handler itselfcache on a resource or resource templatediscover, tools, prompts, resources, resourceTemplates, resourceReadInterim (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.
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:
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",
};
},
});