Home›Journal›This post

MCP Tool Caching Without Stale Catalogs

Partition tool catalogs by effective authority, publish paginated snapshots atomically, invalidate on change, and fence execution with a digest.

JP
JP Casabianca
AI Engineer and Product Designer · full-stack delivery · Bogotá

MCP tool caching is safe only when the cached catalog has an explicit authority scope, bounded lifetime, and invalidation path. This guide builds a five-state cache, completes paginated tool lists atomically, and fences tool execution when the planning digest no longer matches runtime.

MCP tool caching is a coherence problem

MCP tool caching looks like an ordinary latency optimization until a catalog changes between the moment a model plans a call and the moment the client validates it. Then the cached object is not just stale text. It is an obsolete executable contract: a removed tool may still be offered, a changed input schema may reject valid-looking arguments, or an authorization downgrade may leave a privileged definition in prompt context.

The useful unit is therefore not “the response from tools/list.” It is a catalog snapshot with an authority boundary, acquisition state, expiry, and invalidation history. The current MCP tools specification says the returned set can vary with authorization presented on the request, supports pagination, can carry cache metadata, and may change over time. Those facts rule out a process-wide key such as server URL alone.

Treat reuse as a small coherence protocol. A hit is eligible only when its cache scope matches the caller, every page belongs to one accepted acquisition, its TTL is alive, and no later change event has invalidated it. Otherwise refresh before the model sees the definitions. This framing also separates catalog discovery from transport migration details: transport health does not establish catalog freshness.

The design goal is not “never make another list call.” It is “never present a catalog you cannot explain.” A useful receipt says why a snapshot was reused, why it was rejected, which authority partition it occupied, and which digest the model actually received.

Five-state MCP catalog cacheEmpty and fetching lead to fresh; TTL creates stale; listChanged creates invalidated; only fresh can be reused.ONLY FRESH + MATCHING SCOPE MAY REUSEEMPTYno snapshotFETCHINGprivate pagesFRESHeligibleSTALETTL elapsedINVALIDATEDlistChanged / taintINVALIDATED REFRESHES NOW · STALE REFRESHES BEFORE USE
Five-state MCP catalog cache. The diagram and visible semantic equivalent state the same conclusion.
  1. Empty: no accepted snapshot.
  2. Fetching: pages remain private and bounded.
  3. Fresh: complete, unexpired, matching scope; reuse is eligible.
  4. Stale: TTL elapsed; bytes may remain for diagnostics but cannot enter a prompt.
  5. Invalidated: a change signal or tainted traversal forbids reuse immediately.

Reading rule: text, symbols, patterns, and structure carry every conclusion; color is supplementary.

Model the cache as five explicit states

A boolean hit/miss flag hides the moments where stale definitions escape. I use five states: empty, fetching, fresh, stale, and invalidated. Empty has no accepted catalog. Fetching owns one bounded page chain. Fresh may be reused until expiry. Stale still holds bytes for diagnostics but is ineligible for prompt construction. Invalidated means a change signal arrived and reuse is forbidden immediately.

The transition from fetching to fresh is the most important one. Do not publish page one and append later pages into a live array. Accumulate pages in a private candidate, validate each cursor, reject loops, and accept only after the terminal page. A malformed or repeated cursor leaves the previously accepted snapshot untouched but stale if its TTL has expired. This makes MCP tools/list caching atomic from the model’s point of view.

TTL is a ceiling, not a freshness guarantee. The MCP caching utility defines how caching metadata constrains reuse; it does not make change notifications infallible. If a response omits an allowed reuse interval, choose a conservative client policy or do not persist it. If it supplies one, clamp it to an application maximum so a mistaken server hint cannot create an effectively permanent tool catalog.

A reconnect also deserves an explicit transition. Network continuity, server identity, and authorization continuity are separate claims. Re-enter fetching when any cache-key component changes. Keep old bytes for observability, never as a fallback silently shown to the model.

Partition every MCP cache scope

The cache key must encode everything that can legitimately change the returned tools. At minimum that means a stable server identity, protocol revision, requested capability context, and normalized authorization partition. Never place bearer tokens in the key or receipt. Derive a non-reversible partition identifier from the principal and effective grant set, then keep that identifier local to the client.

Public scope can be shared only when the server explicitly permits that scope and the result contains no caller-shaped definitions. Private scope belongs to one authorization partition. A no-store result never becomes an accepted reusable entry. When scope metadata is absent, defaulting to private is safer than inferring public from identical bytes.

This is where MCP tool caching and MCP OAuth audience validation meet. Audience validation decides whether a credential belongs at the resource server. Cache partitioning decides whether a catalog learned under that credential may be shown again. Passing the first check does not imply the second.

Use a matrix in code review: public versus private versus no-store on one axis; same principal, changed grant, and different principal on the other. Every cell must state reuse, refetch, or reject. The dangerous cell is “same user, fewer scopes.” A username-only key would hit, yet the old catalog could still advertise administrative tools. Grant changes need a new partition even when the human identity is unchanged.

Finally, cap partitions per server and evict least-recently-used entries without writing tool descriptions, schemas, or identity material into analytics. Operational telemetry needs state transitions and counts, not the sensitive catalog itself.

Finish pagination before publishing the snapshot

Pagination creates a second coherence boundary. A cursor usually means “continue this traversal,” not “retrieve any page whenever convenient.” The client should begin with no cursor, validate each response, follow the returned cursor at most once, and stop at a declared page and tool budget. If the cursor repeats, disappears inconsistently, or exceeds the cap, reject the candidate.

The specification does not promise that independently fetched pages form a transactional snapshot. That is why the client-side digest fence is a design pattern, not a protocol guarantee. Compute a canonical digest only after all pages are present: deterministic tool order, names, descriptions, schemas, annotations, and relevant metadata. Record page count and cursor witnesses, but never pretend the digest proves the server held still during traversal.

For high-change catalogs, fetch twice and compare terminal digests before publication, or rely on a server-provided revision mechanism when one exists. The second pass costs bandwidth, so use it where stale definitions carry real consequence. A single-pass result can still be useful if its limitation is explicit and notifications plus short TTL reduce the exposure window.

Do not merge a refresh into an old array by name. A removed tool would survive. Replace the snapshot as one value, then allow downstream systems to compare the old and new digests. That comparison pairs well with tool schema evolution tests: a catalog change can trigger corpus validation before the new definitions become eligible for production prompts.

Authorization and cache-scope partition matrixA matrix distinguishes public, private, and no-store results across same grants, changed grants, and different principals.IDENTITY IS NOT THE AUTHORIZATION PARTITIONSCOPEsame grantschanged grantsother principalPUBLICREUSEREFETCHREFETCHPRIVATEREUSEMISSMISSNO-STOREREFETCHREFETCHREFETCHKEY: server · protocol · capability context · non-reversible grant partition
Authorization and cache-scope partition matrix. The diagram and visible semantic equivalent state the same conclusion.
Reuse decision by declared scope
ScopeSame effective grantsChanged grantsDifferent principal
PublicReuse if unexpiredRefetch and verify public scopeRefetch and verify public scope
PrivateReuse inside the same partitionMissMiss
No-storeRefetchRefetchRefetch

The partition uses a non-reversible principal and effective-grant digest; it never stores a bearer token.

Reading rule: text, symbols, patterns, and structure carry every conclusion; color is supplementary.

Invalidate on listChanged, expire on TTL

A server advertising listChanged says it can emit tool-list change notifications, and clients listening for them should treat an observed notification as an immediate invalidation. The MCP subscription pattern provides the long-lived change channel. In the tools feature, sending the notification is a SHOULD, not a mathematical promise that every deployment will deliver every event.

That yields two independent safety nets. TTL bounds silent staleness. A listChanged notification shortens the window when the subscription is healthy. Do not extend expiry merely because the listener remains connected; connectivity is not evidence that no event was lost. Conversely, do not wait for TTL after receiving a notification. Mark the matching partition invalidated before scheduling refresh.

The listener needs its own observable state: requested, acknowledged, healthy, dropped, and retrying. When it drops, existing entries may remain eligible only until their original expiry. Apply a shorter local ceiling if missing events would have serious consequences. The next request after invalidation should coalesce with any refresh already in flight rather than starting a thundering herd.

MCP tool caching also needs cancellation rules. If authorization changes while a refresh is running, discard the completed candidate because its partition no longer matches. If listChanged arrives during pagination, mark the candidate tainted and restart within a bounded retry budget. That policy is stricter than simply refreshing after publication, and it prevents an already-obsolete traversal from briefly becoming fresh.

Fence planning from execution with a digest

The model should plan against an immutable catalog digest. Store that digest beside the turn or run, and require the execution layer to compare it with the current eligible digest before dispatching the tool. This client-side fence is not required by MCP; it is a practical answer to the time gap between discovery and invocation.

If the digests match, validate arguments against the frozen tool schema and continue with ordinary authorization. If they differ, do not silently execute under the new definition. Rebuild tool context and ask the model to plan again, or surface a recoverable catalog-changed result. The exact UX depends on consequence, but the stale plan must not cross the fence unnoticed.

This matters even when a tool name survives. A schema can narrow an enum, add a required field, change annotations, or point to a different operational policy. Digest only the fields your client actually uses, canonicalize object keys, and version the canonicalization algorithm. Hashing raw JSON bytes would treat harmless serialization order as a change and miss semantic normalization choices.

Large catalogs can combine the fence with tool search for large agent toolboxes. Cache the authoritative full catalog per valid partition, then build search indexes keyed by its digest. When the digest changes, discard or rebuild the derived index. Never let a fast search layer become a second, independently stale source of tool truth.

Pagination, invalidation, and digest fenceA three-lane timeline rejects a listChanged-tainted page chain and blocks execution when the planning digest differs from the current digest.PRIVATE PAGES → ATOMIC SNAPSHOT → PLAN/EXECUTE FENCEFETCHPAGE 1cursor APAGE 2cursor BEVENTlistChangedREJECTtaintedRETRYPAGE 1privatePAGE 2terminalACCEPTdigest 7APLANdigest 7AEXECUTECURRENTdigest 91FENCE7A ≠ 91 · stop↻replanDigest fencing is a client pattern, not an MCP protocol guarantee.
Pagination, invalidation, and digest fence. The diagram and visible semantic equivalent state the same conclusion.
  1. First traversal receives page 1, page 2, then a change event. The private candidate is tainted and rejected.
  2. The bounded retry reaches a terminal page chain and publishes digest 7A atomically.
  3. The model plans against digest 7A.
  4. Before execution, the current eligible catalog is digest 91.
  5. The mismatch blocks dispatch and requires replanning. The digest fence is a client design pattern.

Reading rule: text, symbols, patterns, and structure carry every conclusion; color is supplementary.

Test adversarial order, identity, and failure

Happy-path tests prove only that a cache can cache. The meaningful suite changes one coherence condition at a time. Start with a two-page catalog and assert that no partial page becomes visible. Repeat a cursor and assert rejection. Deliver listChanged between pages and assert the candidate is tainted. Advance a fake clock to the exact TTL boundary and require refresh.

Then switch authority. Keep the same server and principal label but remove an effective grant. The new request must miss the old partition. Switch to a second principal with identical allowed tools and still require a separate private entry. Test public scope separately so sharing happens only under the declared scope. No receipt should contain a bearer token or raw identity claim.

For subscription failure, drop the stream without a notification. The cache can remain fresh until its existing deadline, never beyond it. Reconnect, acknowledge a new listener, and ensure that reconnecting does not rewrite acquisition time. For digest fencing, plan under revision A, publish revision B, and assert that execution returns a catalog-changed result before any tool call.

Finally test deterministic reruns. Given the same normalized event script, the lab should emit the same transition trace, catalog digest, and receipt hash. Random wall-clock timestamps belong outside the hashed core. A robust MCP tool caching implementation is easier to trust when every hit and miss can be replayed as a finite state machine.

Ship a catalog-coherence receipt

A production receipt does not need the whole catalog. It needs enough evidence to reconstruct the decision: algorithm version, server partition alias, authorization partition digest, cache scope, acquisition start and completion, page count, TTL ceiling, listener state, invalidation generation, catalog digest, planning digest, and final disposition.

Redact descriptions and schemas unless a secured debugging workflow explicitly requires them. Record cursor hashes rather than opaque cursor values. Count tools, pages, refreshes, tainted acquisitions, digest mismatches, and scope rejections. Those metrics expose coherence trouble without copying sensitive capabilities into logs.

Document the limits. MCP tools/list caching cannot prove a server stayed unchanged between paginated responses. Notifications can be delayed or lost. TTL limits age; it does not certify correctness. The digest fence detects catalog drift known to the client, not unauthorized behavior behind an unchanged schema. Tool invocation still needs current server-side authorization and human review where consequence requires it.

Those limits make the system more useful, not less. They turn MCP tool caching from a vague performance shortcut into a bounded contract: reuse only inside an explicit partition, publish only complete acquisitions, invalidate on observed changes, expire regardless of listener optimism, and stop execution when planning evidence no longer matches.

In review language, MCP tools/list caching owns acquisition, MCP listChanged owns immediate observed invalidation, and MCP cache scope owns reuse boundaries. The MCP tool catalog is the executable value these controls protect. MCP tool caching is ready only when all four appear in one receipt, and MCP tool caching should fail closed whenever any one is unknown. That is the complete MCP tool caching acceptance rule, and MCP tool caching should not ship without it.

Runnable local artifact — The lab simulates client coherence decisions; it does not connect to an MCP server, prove server snapshot isolation, or make listChanged delivery reliable.

Plain text1 line
Normalize a finite event script, keep page candidates private, invalidate immediately on observed change, enforce TTL independently, and export a canonical digest-fence receipt.