npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mcp-cache-kit

v0.1.1

Published

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.

Downloads

346

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 ttlMs and never serves a private result 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 on cacheScope alone 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 + ttlMs and are removed on access (or via prune()).
  • private never leaks — a private entry stored for scope A is only ever returned to scope A. A different scopeId, or no scopeId, gets a miss.
  • public is shared — stored under one shared key and returned to anyone.
  • Fail-safeset() silently refuses (and counts as rejected) anything it can't prove safe: missing/partial hints, bad cacheScope, bad ttlMs, private-without-scopeId, and ttlMs: 0 (by default).
  • Never throws on the hot path — a request it cannot turn into a stable key (a circular params, a BigInt, a hostile getter on method/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 (throws TypeError on bad input).
  • isCacheScope(x), isValidTtlMs(x) — type guards.
  • deriveScopeKey(cacheScope, scopeId?) — the scope-key rule (undefined for private without a scopeId).
  • 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