Cache Kit
FreeNot checkedCorrect, leak-safe caching for the new MCP cache semantics (SEP-2549). Set ttlMs/cacheScope on the server, and honor them safely on the client/proxy so a privat
About
Correct, leak-safe caching for the new MCP cache semantics (SEP-2549). Set ttlMs/cacheScope on the server, and honor them safely on the client/proxy so a private result is never served across authorization contexts.
README
Part of the StudioMeyer MCP Stack — Built in Mallorca 🌴 · ⭐ if you use it
mcp-cache-kit
npm CI OpenSSF Scorecard License: MIT
Correct, leak-safe caching for the new MCP cache semantics (SEP-2549).
The MCP spec 2026-07-28 release candidate adds SEP-2549: tools/list, resources/read (and the other list results) now carry ttlMs and cacheScope so clients, gateways, and proxies can cache them — modeled on HTTP Cache-Control. It is brand-new and has essentially no dedicated tooling. Generic caches store results "somehow" and ignore cacheScope, which is a real security trap: a result marked cacheScope: "private" that gets cached and served across users is a cross-user data leak.
mcp-cache-kit is the small, correct layer for exactly this:
- Server side — set the fields right (
withCacheHints). - Client / proxy side — a cache that honors
ttlMsand never serves aprivateresult across authorization contexts (McpResultCache). - A guard — decide if any result may be cached for a given scope, with a clear reason (
cacheSafety/assertCacheSafe).
Zero runtime dependencies. TypeScript strict, ESM + CJS, Node 20+. The @modelcontextprotocol/sdk is an optional peer — the helpers also work on plain result objects, so you can use it without the SDK.
⚠️ The 2026-07-28 spec is a release candidate. Field names and semantics may still shift before final. This library models them conservatively and is intentionally tolerant of missing/malformed fields (it treats anything it cannot prove safe as uncacheable).
What SEP-2549 actually says
Verified against the spec source (schema/draft/schema.ts and docs/.../utilities/caching.mdx):
Cacheable results extend a CacheableResult shape with two top-level fields:
| Field | Type | Meaning |
|---|---|---|
ttlMs |
number (@minimum 0) |
Freshness window, like Cache-Control: max-age. 0 = immediately stale. Absent/negative → treat as 0. |
cacheScope |
"public" | "private" |
Like Cache-Control: public vs private. See below. |
"public"— the response has no user-specific data. Any client or intermediary MAY cache it and serve it across authorization contexts."private"— the response MAY be cached and reused only within the same authorization context. Caches MUST NOT be shared across authorization contexts (a different access token / user / session needs a different cache entry).
Applies to tools/list, resources/list, resources/templates/list, prompts/list, and resources/read.
The spec also warns: a
"public"result from an authenticated endpoint can still be shared between callers, and you MUST NOT rely oncacheScopealone to prevent unauthorized access. This library enforces the scope boundary for you, but you still own labeling scopes honestly and authenticating at the origin. See SECURITY.md.
The cross-user-leak trap
tools/list / resources/read
user A ───────────────────────────────► proxy (caches by request only)
│ stores result, ignores cacheScope
user B ───────── same request ──────────► proxy
│ returns A's cached result ← LEAK
If the cached result was cacheScope: "private" (A's inbox, A's tenant config, …), user B just received another user's data. mcp-cache-kit keys every entry by the request and the caller's scope identity, so a private entry for A is structurally unreachable for B.
Install
npm install mcp-cache-kit
# optional, only if you use the SDK result types directly:
npm install @modelcontextprotocol/sdk
Server side — set the hints
Attach the fields to a result. withCacheHints validates them (rejects negative/non-finite ttlMs and any cacheScope other than public/private) and returns a new object.
import { withCacheHints, CacheScope } from "mcp-cache-kit";
// tools/list rarely contains user data → public, cache for 5 minutes
server.setRequestHandler(ListToolsRequestSchema, async () =>
withCacheHints({ tools }, { ttlMs: 5 * 60_000, cacheScope: CacheScope.Public }),
);
// a per-user resource → private, cache for 1 minute
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
const contents = await loadForUser(req.params.uri);
return withCacheHints({ contents }, { ttlMs: 60_000, cacheScope: CacheScope.Private });
});
There are shorthands too: publicHints(ttlMs) and privateHints(ttlMs).
Client / proxy side — honor them safely
McpResultCache stores results keyed by (request, scope). Pass a scopeId that identifies the caller's authorization context (an access-token hash, user id, tenant id, or session id).
import { McpResultCache } from "mcp-cache-kit";
const cache = new McpResultCache({ maxEntries: 5_000 });
async function handleReadResource(req, ctx) {
const scopeId = ctx.tokenHash; // identifies the authorization context
// try the cache first
const hit = cache.get(req, { scopeId });
if (hit.hit) return hit.value;
// miss → fetch from the upstream MCP server, then offer it to the cache.
// set() stores it ONLY if the result is cache-safe for this scope.
const result = await upstream.request(req);
cache.set(req, result, { scopeId });
return result;
}
Or the one-liner:
const result = await cache.getOrLoad(req, () => upstream.request(req), { scopeId });
What the cache guarantees:
- TTL is honored — entries expire at
received + ttlMsand are removed on access (or viaprune()). privatenever leaks — aprivateentry stored for scope A is only ever returned to scope A. A differentscopeId, or noscopeId, gets a miss.publicis shared — stored under one shared key and returned to anyone.- Fail-safe —
set()silently refuses (and counts asrejected) anything it can't prove safe: missing/partial hints, badcacheScope, badttlMs,private-without-scopeId, andttlMs: 0(by default). - Never throws on the hot path — a request it cannot turn into a stable key (a circular
params, aBigInt, a hostile getter onmethod/params) is a fail-safe miss (get) / reject (set) with reason"unkeyable-request", never an exception that could take down a proxy.
Guard — cacheSafety / assertCacheSafe
Use these in a gateway when you want to make the decision yourself:
import { cacheSafety, assertCacheSafe } from "mcp-cache-kit";
const decision = cacheSafety(result, { scopeId });
if (decision.cacheable) {
// decision.scopeKey is where to store it; decision.hints.ttlMs is the TTL
myStore.put(decision.scopeKey, result, decision.hints.ttlMs);
} else {
// decision.reason: "missing-fields" | "invalid-ttl" | "invalid-scope"
// | "zero-ttl" | "private-without-scope" | "not-an-object"
log.debug(`not caching: ${decision.message}`);
}
// or throw if "must be cacheable here" is an invariant:
const { hints, scopeKey } = assertCacheSafe(result, { scopeId }); // throws CacheUnsafeError otherwise
Low-level helpers
All individually exported and tested:
parseCacheHints(result)→{ ok: true, hints } | { ok: false, reason, message }— never throws.validateCacheHints({ ttlMs, cacheScope })→ normalized hints (throwsTypeErroron bad input).isCacheScope(x),isValidTtlMs(x)— type guards.deriveScopeKey(cacheScope, scopeId?)— the scope-key rule (undefinedforprivatewithout ascopeId).deriveRequestKey({ method, params })/stableStringify(x)— deterministic request keys (param key order doesn't matter; circular structures are rendered with a"[Circular]"marker, never a stack overflow).- Constants:
CacheScope,CACHE_SCOPE_VALUES,PUBLIC_SCOPE_KEY.
Fail-safe philosophy
Caching the wrong thing across users is worse than a cache miss. So the default decision is always "do not cache" unless the result proves it is safe: both fields present, both valid, and — for private — a scopeId to bind it to. SEP-2549 is still an RC, so being strict here also protects you from upstream servers that emit partial or mislabeled hints.
Testing notes
McpResultCache takes an injectable clock (() => number), so you can test TTL behavior deterministically without real time — and it also works under vitest fake timers driving the default Date.now. Both styles are covered in the test suite, including the headline cross-user leak test.
Part of the StudioMeyer MCP toolkit
A small family of focused, production-grade tools for building and operating MCP servers — mix and match:
- mcp-armor — runtime defense sidecar: scans tool calls, verifies signed manifests, blocks known-bad CVEs
- mcp-gauntlet — pre-deploy
mcp-fuzz(schema-aware fuzzer) +mcp-storm(load tester) - mcp-otel — W3C Trace Context → OpenTelemetry bridge
- mcp-cache-kit (this one) — leak-safe SEP-2549 caching (
ttlMs+cacheScope) - skilldoctor — linter + security scanner for agent skill files
License
MIT © StudioMeyer 2026
Install Cache Kit in Claude Desktop, Claude Code & Cursor
unyly install cache-kitInstalls into Claude Desktop, Claude Code, Cursor & VS Code — handles npx, uvx and build-from-source repos for you.
First time? Get the CLI: curl -fsSL https://unyly.org/install | sh
Or configure manually
Run in your terminal:
claude mcp add cache-kit -- npx -y mcp-cache-kitFAQ
Is Cache Kit MCP free?
Yes, Cache Kit MCP is free — one-click install via Unyly at no cost.
Does Cache Kit need an API key?
No, Cache Kit runs without API keys or environment variables.
Is Cache Kit hosted or self-hosted?
Self-hosted: the server runs locally on your machine via the install command above.
How do I install Cache Kit in Claude Desktop, Claude Code or Cursor?
Open Cache Kit on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
GitHub
PRs, issues, code search, CI status
by GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
by mcpdotdirectCompare Cache Kit with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All development MCPs
