Skip to content

caching

Server-side caching hints (SEP-2549, protocol revision 2026-07-28).

Results for the cacheable methods carry ttlMs/cacheScope freshness hints. A handler sets them by returning a result with explicit ttl_ms/cache_scope values; Server(cache_hints={method: CacheHint(...)}) fills them for handlers that don't. Fields the handler set win, per field, so a server-wide hint never overrides a handler's explicit choice.

CacheHint dataclass

Freshness hint for one cacheable method's results.

ttl_ms is how long, in milliseconds, a client may consider the result fresh (0 means immediately stale). scope is whether a cached result may be shared across authorization contexts ("public") or only reused within the one that produced it ("private").

Source code in src/mcp/server/caching.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@dataclass(frozen=True, slots=True)
class CacheHint:
    """Freshness hint for one cacheable method's results.

    `ttl_ms` is how long, in milliseconds, a client may consider the result
    fresh (`0` means immediately stale). `scope` is whether a cached result may
    be shared across authorization contexts (`"public"`) or only reused within
    the one that produced it (`"private"`).
    """

    ttl_ms: int = 0
    scope: Literal["public", "private"] = "private"

    def __post_init__(self) -> None:
        if self.ttl_ms < 0:
            raise ValueError(f"ttl_ms must be >= 0, got {self.ttl_ms}")
        if self.scope not in ("public", "private"):
            raise ValueError(f"scope must be 'public' or 'private', got {self.scope!r}")

apply_cache_hint

apply_cache_hint(
    result: CacheableResultT, hint: CacheHint
) -> CacheableResultT

Fill ttl_ms/cache_scope on result from hint.

Per-field: a field the handler set explicitly - even to its default value, tracked via model_fields_set - is left alone; only unset fields take the hint. A handler constructing results with model_construct bypasses that tracking and is treated as having set nothing.

Source code in src/mcp/server/caching.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def apply_cache_hint(result: CacheableResultT, hint: CacheHint) -> CacheableResultT:
    """Fill `ttl_ms`/`cache_scope` on `result` from `hint`.

    Per-field: a field the handler set explicitly - even to its default value,
    tracked via `model_fields_set` - is left alone; only unset fields take the
    hint. A handler constructing results with `model_construct` bypasses that
    tracking and is treated as having set nothing.
    """
    update: dict[str, int | str] = {}
    if "ttl_ms" not in result.model_fields_set:
        update["ttl_ms"] = hint.ttl_ms
    if "cache_scope" not in result.model_fields_set:
        update["cache_scope"] = hint.scope
    return result.model_copy(update=update) if update else result

validate_cache_hints

validate_cache_hints(
    cache_hints: Mapping[Any, Any] | None,
) -> dict[str, CacheHint]

Validate a cache_hints constructor argument into a plain dict.

The Server/MCPServer signatures already close the key set and value type for type-checked callers; this runtime gate is deliberately loose in its parameter so it covers everyone else (e.g. a map deserialized from config) - a bad entry fails at construction, not on the first request to that method.

Raises:

Type Description
ValueError

If a key is not a cacheable method.

TypeError

If a value is not a CacheHint.

Source code in src/mcp/server/caching.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def validate_cache_hints(cache_hints: Mapping[Any, Any] | None) -> dict[str, CacheHint]:
    """Validate a `cache_hints` constructor argument into a plain dict.

    The `Server`/`MCPServer` signatures already close the key set and value
    type for type-checked callers; this runtime gate is deliberately loose in
    its parameter so it covers everyone else (e.g. a map deserialized from
    config) - a bad entry fails at construction, not on the first request to
    that method.

    Raises:
        ValueError: If a key is not a cacheable method.
        TypeError: If a value is not a `CacheHint`.
    """
    if cache_hints is None:
        return {}
    # repr-format keys so a non-string key raises this ValueError, not a TypeError from sorted/join.
    unknown = sorted(repr(method) for method in cache_hints if method not in CACHEABLE_METHODS)
    if unknown:
        raise ValueError(f"cache_hints keys must be cacheable methods (see CacheableMethod); got: {', '.join(unknown)}")
    validated: dict[str, CacheHint] = {}
    for method, hint in cache_hints.items():
        if not isinstance(hint, CacheHint):
            raise TypeError(f"cache_hints[{method!r}] must be a CacheHint, got {type(hint).__name__}")
        validated[method] = hint
    return validated