You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The caching page covered only server authoring. Add a 'What the client
sees' section: the hints arrive as parsed ttl_ms/cache_scope fields on
every cacheable result, the SDK does not act on them, and the supported
path today is reading the fields and doing your own freshness and scope
bookkeeping. Covers the legacy-server case (absent fields show the
conservative model defaults) and the model_fields_set wire-presence
check, with a tested example.
Copy file name to clipboardExpand all lines: docs/advanced/caching.md
+17Lines changed: 17 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -35,6 +35,22 @@ This is also the escape hatch for dynamics the constructor can't know: a handler
35
35
36
36
One caveat on paginated lists: the protocol requires the **same `cacheScope` on every page** of one list. The constructor map satisfies that by construction — it's keyed by method, not by page. But a handler that overrides the scope itself owns that consistency: override it on *every* page, never only when a cursor is present, or page one and page two will disagree.
37
37
38
+
## What the client sees
39
+
40
+
On the client, the hints arrive as plain fields on every cacheable result — `ttl_ms` and `cache_scope`, already parsed:
41
+
42
+
```python title="client.py" hl_lines="15"
43
+
--8<--"docs_src/caching/tutorial003.py"
44
+
```
45
+
46
+
The SDK parses; it does not (yet) act. There is no built-in response cache: calling `list_tools()` twice makes two round trips, whatever the TTL said. The spec makes honoring optional — a client that ignores the hints entirely is fully conformant — so until the SDK grows a response cache, the supported path is to read the fields and do your own bookkeeping:
47
+
48
+
***Freshness** is `now < t_received + ttl_ms / 1000`: record the clock when the response arrives, and treat the result as reusable until the TTL runs out. `ttl_ms == 0` means *immediately stale* — don't reuse it at all.
49
+
***Scope is a sharing rule, not a suggestion.** A `"private"` result may be reused only within the same authorization context — same access token, same cache. Never put `"private"` results in a cache shared across users.
50
+
***Notifications beat TTL.** If the server sends `list_changed` while your copy is still fresh, the copy is stale now — re-fetch.
51
+
52
+
Against an **older server** (pre-2026 protocol), the fields are simply absent from the wire, and the models show their conservative defaults: `ttl_ms == 0`, `cache_scope == "private"` — stale and unshared, the right assumption for a server that declared nothing. If you need to distinguish "the server said 0" from "the server said nothing", check `"ttl_ms" in result.model_fields_set`: it's only set when the field actually arrived.
53
+
38
54
## Older clients
39
55
40
56
Clients on pre-2026 protocol versions never see either field — the SDK strips them at serialization for those connections. Configure your hints once; there is nothing version-specific to write.
@@ -45,3 +61,4 @@ Clients on pre-2026 protocol versions never see either field — the SDK strips
45
61
*`cache_hints={method: CacheHint(...)}` at construction (both `MCPServer` and `Server`) sets server-wide values per method.
46
62
* A handler that sets the fields on its result overrides the map, per field.
47
63
*`"public"` is a promise that the result is identical for every caller. It is not access control.
64
+
* Clients read the hints as `result.ttl_ms` / `result.cache_scope` and own the caching decision themselves — the SDK has no built-in response cache yet.
0 commit comments