mcp-cache-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/hints.ts","../src/safety.ts","../src/cache.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Core types for mcp-cache-kit, modeled on MCP SEP-2549.\n *\n * SEP-2549 (MCP spec, 2026-07-28 release candidate) adds two top-level fields to\n * cacheable results (`tools/list`, `resources/list`, `resources/templates/list`,\n * `prompts/list`, `resources/read`):\n *\n * - `ttlMs: number` — how long the client MAY treat the result as fresh,\n * analogous to HTTP `Cache-Control: max-age`. `@minimum 0`.\n * - `cacheScope: \"public\" | \"private\"` — analogous to HTTP\n * `Cache-Control: public` vs `private`.\n *\n * Source of truth (verified):\n * https://github.com/modelcontextprotocol/modelcontextprotocol (schema/draft/schema.ts,\n * docs/specification/draft/server/utilities/caching.mdx) and\n * https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/\n *\n * NOTE: the 2026-07-28 spec is a release candidate. These field names/semantics\n * may still shift before final. This library is intentionally tolerant of missing\n * or malformed fields and treats anything it cannot prove safe as uncacheable.\n */\n\n/**\n * The allowed values of `cacheScope` per SEP-2549.\n *\n * - `\"public\"` — the response does not contain user-specific data. Any client or\n * intermediary MAY cache it and serve it across authorization contexts.\n * - `\"private\"` — the response MAY be cached and reused only within the SAME\n * authorization context. Caches MUST NOT be shared across authorization contexts\n * (a different access token/user/session requires a different cache entry).\n */\nexport const CacheScope = {\n /** Shareable across authorization contexts. */\n Public: \"public\",\n /** Only reusable within the same authorization context. */\n Private: \"private\",\n} as const;\n\n/** Union of the valid `cacheScope` string values: `\"public\" | \"private\"`. */\nexport type CacheScope = (typeof CacheScope)[keyof typeof CacheScope];\n\n/** Immutable list of valid scope values, handy for validation/iteration. */\nexport const CACHE_SCOPE_VALUES: readonly CacheScope[] = Object.freeze([\n CacheScope.Public,\n CacheScope.Private,\n]);\n\n/**\n * The SEP-2549 cache hint fields as they appear (top-level) on a cacheable result.\n */\nexport interface CacheHints {\n /**\n * How long (ms) the client MAY treat the result as fresh. `>= 0`.\n * `0` means \"immediately stale\". See {@link CacheScope} for scope semantics.\n */\n ttlMs: number;\n /** Whether the result is safe to share across authorization contexts. */\n cacheScope: CacheScope;\n}\n\n/**\n * Minimal structural shape of an MCP result that MAY carry cache hints.\n *\n * Kept deliberately loose (`Record<string, unknown>`) so the helpers work on plain\n * result objects WITHOUT a hard dependency on `@modelcontextprotocol/sdk`. If you\n * do use the SDK, its `ListToolsResult` / `ReadResourceResult` (etc.) structurally\n * satisfy this type.\n */\nexport interface MaybeCacheableResult extends Record<string, unknown> {\n ttlMs?: unknown;\n cacheScope?: unknown;\n}\n\n/**\n * A result that carries valid, fully-typed SEP-2549 cache hints.\n * `T` is the underlying result type so callers keep their concrete shape.\n */\nexport type WithCacheHints<T extends object> = T & CacheHints;\n\n/** Options accepted by {@link withCacheHints}. */\nexport interface CacheHintsInput {\n /** Time-to-live in milliseconds. Must be a finite number `>= 0`. */\n ttlMs: number;\n /** One of {@link CacheScope}. */\n cacheScope: CacheScope;\n}\n\n/**\n * Result of parsing the cache hints off an unknown result object.\n *\n * `ok: true` only when BOTH fields are present and valid per spec. Otherwise\n * `ok: false` with a machine-readable `reason` and human-readable `message`.\n */\nexport type ParsedCacheHints =\n | { ok: true; hints: CacheHints }\n | { ok: false; reason: UncacheableReason; message: string };\n\n/** Machine-readable reasons a result is considered uncacheable. */\nexport type UncacheableReason =\n /** The result was null/undefined or not an object. */\n | \"not-an-object\"\n /** `ttlMs` or `cacheScope` was absent. Fail-safe: don't cache. */\n | \"missing-fields\"\n /** `ttlMs` was present but not a finite number `>= 0`. */\n | \"invalid-ttl\"\n /** `cacheScope` was present but not `\"public\" | \"private\"`. */\n | \"invalid-scope\"\n /** `ttlMs` was `0` — explicitly \"immediately stale\", so nothing to store. */\n | \"zero-ttl\"\n /** A `private` result was offered without a scope identity to key it by. */\n | \"private-without-scope\";\n\n/**\n * The decision returned by {@link cacheSafety} / used by the cache: may this result\n * be stored for the given scope identity, and if not, why.\n */\nexport type CacheDecision =\n | {\n cacheable: true;\n hints: CacheHints;\n /**\n * The scope key the entry MUST be stored under. For `public` results this is\n * a shared constant; for `private` results it is derived from the caller's\n * authorization-context identity.\n */\n scopeKey: string;\n }\n | { cacheable: false; reason: UncacheableReason; message: string };\n","/**\n * Server-side helpers: attach and read SEP-2549 cache hints on results.\n */\n\nimport {\n CACHE_SCOPE_VALUES,\n CacheScope,\n type CacheHints,\n type CacheHintsInput,\n type MaybeCacheableResult,\n type ParsedCacheHints,\n type WithCacheHints,\n} from \"./types.js\";\n\n/** Type guard for the `cacheScope` enum. */\nexport function isCacheScope(value: unknown): value is CacheScope {\n return (\n typeof value === \"string\" &&\n (CACHE_SCOPE_VALUES as readonly string[]).includes(value)\n );\n}\n\n/**\n * True if `ttlMs` is a valid SEP-2549 TTL: a finite, non-negative number.\n *\n * The spec annotates `ttlMs` with `@minimum 0` and treats negative/absent values\n * as `0`. We require a real finite number here (NaN / Infinity are rejected).\n */\nexport function isValidTtlMs(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value) && value >= 0;\n}\n\n/**\n * Validate a {@link CacheHintsInput}, throwing a descriptive `TypeError` on bad input.\n * Returns the normalized {@link CacheHints} (ttlMs floored to an integer ms).\n *\n * @throws {TypeError} if `ttlMs` is not a finite number `>= 0`, or `cacheScope`\n * is not `\"public\" | \"private\"`.\n */\nexport function validateCacheHints(input: CacheHintsInput): CacheHints {\n if (input === null || typeof input !== \"object\") {\n throw new TypeError(\n `mcp-cache-kit: cache hints must be an object with { ttlMs, cacheScope }, got ${describe(\n input,\n )}`,\n );\n }\n if (!isValidTtlMs(input.ttlMs)) {\n throw new TypeError(\n `mcp-cache-kit: ttlMs must be a finite number >= 0 (milliseconds), got ${describe(\n input.ttlMs,\n )}`,\n );\n }\n if (!isCacheScope(input.cacheScope)) {\n throw new TypeError(\n `mcp-cache-kit: cacheScope must be one of ${CACHE_SCOPE_VALUES.map(\n (v) => `\"${v}\"`,\n ).join(\" | \")}, got ${describe(input.cacheScope)}`,\n );\n }\n // Normalize to integer milliseconds — fractional ms is meaningless and keeps\n // the value JSON-clean for the wire.\n return { ttlMs: Math.floor(input.ttlMs), cacheScope: input.cacheScope };\n}\n\n/**\n * Server-side: attach SEP-2549 cache hints to a `tools/list` / `resources/read`\n * (etc.) result so clients and proxies can cache it correctly.\n *\n * Returns a NEW object (does not mutate the input) with `ttlMs` and `cacheScope`\n * set as top-level fields, exactly where the spec places them.\n *\n * @example\n * ```ts\n * server.setRequestHandler(ListToolsRequestSchema, () =>\n * withCacheHints({ tools }, { ttlMs: 60_000, cacheScope: CacheScope.Public }),\n * );\n * ```\n *\n * @throws {TypeError} via {@link validateCacheHints} on invalid hints.\n */\nexport function withCacheHints<T extends object>(\n result: T,\n hints: CacheHintsInput,\n): WithCacheHints<T> {\n if (result === null || typeof result !== \"object\") {\n throw new TypeError(\n `mcp-cache-kit: withCacheHints(result, ...) requires a result object, got ${describe(\n result,\n )}`,\n );\n }\n const valid = validateCacheHints(hints);\n return { ...result, ttlMs: valid.ttlMs, cacheScope: valid.cacheScope };\n}\n\n/**\n * Convenience: a `public` result fresh for `ttlMs`. Safe to share across users.\n */\nexport function publicHints(ttlMs: number): CacheHints {\n return validateCacheHints({ ttlMs, cacheScope: CacheScope.Public });\n}\n\n/**\n * Convenience: a `private` result fresh for `ttlMs`. Reusable only within the\n * same authorization context — the cache will refuse to share it across scopes.\n */\nexport function privateHints(ttlMs: number): CacheHints {\n return validateCacheHints({ ttlMs, cacheScope: CacheScope.Private });\n}\n\n/**\n * Client/proxy-side: read and validate the cache hints off an unknown result.\n *\n * Returns `{ ok: true, hints }` only when BOTH fields are present and valid.\n * Anything else returns `{ ok: false, reason, message }` — this is the fail-safe\n * primitive the cache builds on: if we cannot prove the hints, we don't cache.\n *\n * This never throws. It is safe to call on arbitrary JSON-RPC result payloads.\n */\nexport function parseCacheHints(result: unknown): ParsedCacheHints {\n if (result === null || typeof result !== \"object\") {\n return {\n ok: false,\n reason: \"not-an-object\",\n message: `result is not an object (got ${describe(result)})`,\n };\n }\n const r = result as MaybeCacheableResult;\n // Read the two fields once, defensively: a live JS object could carry a\n // throwing getter on ttlMs/cacheScope. We promise never to throw, so a hostile\n // getter is treated as \"cannot read hints\" → uncacheable (fail safe).\n let ttlVal: unknown;\n let scopeVal: unknown;\n try {\n ttlVal = \"ttlMs\" in r ? r.ttlMs : undefined;\n scopeVal = \"cacheScope\" in r ? r.cacheScope : undefined;\n } catch {\n return {\n ok: false,\n reason: \"not-an-object\",\n message: \"reading cache hints threw (hostile getter on the result?)\",\n };\n }\n const hasTtl = ttlVal !== undefined;\n const hasScope = scopeVal !== undefined;\n\n if (!hasTtl && !hasScope) {\n return {\n ok: false,\n reason: \"missing-fields\",\n message: \"result has no SEP-2549 cache hints (ttlMs / cacheScope absent)\",\n };\n }\n // Per the caching spec, absent ttlMs defaults to 0; but a partially-hinted\n // result (one field present, the other missing) is malformed — fail safe.\n if (!hasTtl || !hasScope) {\n return {\n ok: false,\n reason: \"missing-fields\",\n message:\n \"result has only one of ttlMs / cacheScope; both are required to be cacheable\",\n };\n }\n if (!isValidTtlMs(ttlVal)) {\n return {\n ok: false,\n reason: \"invalid-ttl\",\n message: `ttlMs is not a finite number >= 0 (got ${describe(ttlVal)})`,\n };\n }\n if (!isCacheScope(scopeVal)) {\n return {\n ok: false,\n reason: \"invalid-scope\",\n message: `cacheScope is not \"public\" | \"private\" (got ${describe(\n scopeVal,\n )})`,\n };\n }\n return { ok: true, hints: { ttlMs: Math.floor(ttlVal), cacheScope: scopeVal } };\n}\n\n/** Short, safe description of an unknown value for error messages. */\nfunction describe(value: unknown): string {\n if (value === null) return \"null\";\n if (typeof value === \"string\") return JSON.stringify(value);\n if (typeof value === \"number\" || typeof value === \"boolean\") return String(value);\n if (Array.isArray(value)) return \"an array\";\n return typeof value;\n}\n","/**\n * The safety layer: decide whether a result may be cached for a given\n * authorization-context identity, and derive the scope key it must be stored under.\n *\n * This is where the cross-user-leak trap is closed. SEP-2549 says a `private`\n * result \"MAY be cached and reused only within the same authorization context\".\n * We enforce that by KEYING private entries with the caller's scope identity, so a\n * `private` entry stored for user A is structurally unreachable for user B.\n */\n\nimport { parseCacheHints } from \"./hints.js\";\nimport {\n CacheScope,\n type CacheDecision,\n type CacheHints,\n} from \"./types.js\";\n\n/**\n * The single, shared scope key used for `public` results. Chosen to be a value\n * that cannot collide with any real scope identity (which is escaped, see below).\n */\nexport const PUBLIC_SCOPE_KEY = \"public\";\n\n/** Options for {@link cacheSafety} / {@link assertCacheSafe}. */\nexport interface CacheSafetyOptions {\n /**\n * Identity of the caller's authorization context — e.g. an access-token hash,\n * user id, tenant id, or session id. REQUIRED to cache `private` results.\n * Pass it for `public` results too if you have it; it will simply be ignored.\n */\n scopeId?: string;\n /**\n * If true, a `ttlMs` of `0` is reported as cacheable (with `zero-ttl` being a\n * non-fatal note). Default `false`: a 0 TTL means \"immediately stale\", so there\n * is nothing worth storing and we report it uncacheable. The cache uses the\n * default.\n */\n allowZeroTtl?: boolean;\n}\n\n/**\n * Derive the scope key an entry must be stored under.\n *\n * - `public` → {@link PUBLIC_SCOPE_KEY} (shared across all callers).\n * - `private` → a key derived from `scopeId`, namespaced so it can never collide\n * with the public bucket or with another scope id.\n *\n * Returns `undefined` for a `private` result when no `scopeId` is supplied —\n * the caller MUST treat that as \"do not cache\".\n */\nexport function deriveScopeKey(\n cacheScope: CacheScope,\n scopeId?: string,\n): string | undefined {\n if (cacheScope === CacheScope.Public) return PUBLIC_SCOPE_KEY;\n // private — fail closed on anything that is not a non-empty string. A JS caller\n // can pass a number/array/object (e.g. a numeric tenant PK straight from the DB),\n // and `(123).length` is `undefined`, which would void the length-prefix collision\n // defense below and leak a private entry across scopes. So we never coerce an\n // unexpected type into a cache key — we refuse to cache it.\n if (typeof scopeId !== \"string\" || scopeId === \"\") return undefined;\n // Namespace + length-prefix the id so distinct ids cannot be confused with one\n // another or with the literal public key (e.g. a scopeId of \"public\").\n return `private:${scopeId.length}:${scopeId}`;\n}\n\n/**\n * Decide whether `result` may be cached for the given scope identity.\n *\n * Returns a {@link CacheDecision}: when `cacheable: true` it includes the validated\n * hints and the `scopeKey` to store the entry under; when `cacheable: false` it\n * includes a machine-readable `reason` and a human-readable `message`.\n *\n * Fail-safe by construction: missing/invalid hints, an unrecognized scope, a\n * `private` result without a `scopeId`, and (by default) a `0` TTL all return\n * `cacheable: false`. Never throws.\n *\n * Use this as a guard anywhere in a proxy/gateway:\n * ```ts\n * const d = cacheSafety(result, { scopeId: tokenHash });\n * if (d.cacheable) store(key, d.scopeKey, result, d.hints.ttlMs);\n * ```\n */\nexport function cacheSafety(\n result: unknown,\n options: CacheSafetyOptions = {},\n): CacheDecision {\n const parsed = parseCacheHints(result);\n if (!parsed.ok) {\n return { cacheable: false, reason: parsed.reason, message: parsed.message };\n }\n const hints = parsed.hints;\n\n if (hints.ttlMs === 0 && options.allowZeroTtl !== true) {\n return {\n cacheable: false,\n reason: \"zero-ttl\",\n message: \"ttlMs is 0 (immediately stale); nothing to cache\",\n };\n }\n\n const scopeKey = deriveScopeKey(hints.cacheScope, options.scopeId);\n if (scopeKey === undefined) {\n return {\n cacheable: false,\n reason: \"private-without-scope\",\n message:\n 'cacheScope is \"private\" but no scopeId was provided; refusing to cache to avoid cross-context leaks',\n };\n }\n\n return { cacheable: true, hints, scopeKey };\n}\n\n/** Error thrown by {@link assertCacheSafe} when a result may not be cached. */\nexport class CacheUnsafeError extends Error {\n override readonly name = \"CacheUnsafeError\";\n /** Machine-readable reason, mirrors {@link CacheDecision}'s `reason`. */\n readonly reason: Extract<CacheDecision, { cacheable: false }>[\"reason\"];\n constructor(decision: Extract<CacheDecision, { cacheable: false }>) {\n super(`mcp-cache-kit: result is not cache-safe (${decision.reason}): ${decision.message}`);\n this.reason = decision.reason;\n }\n}\n\n/**\n * Assert that `result` may be cached for the given scope identity, throwing a\n * {@link CacheUnsafeError} otherwise. Returns the validated hints + scopeKey.\n *\n * Prefer {@link cacheSafety} when you want to branch without exceptions; use this\n * when \"must be cacheable here\" is an invariant you want to enforce loudly.\n */\nexport function assertCacheSafe(\n result: unknown,\n options: CacheSafetyOptions = {},\n): { hints: CacheHints; scopeKey: string } {\n const decision = cacheSafety(result, options);\n if (!decision.cacheable) {\n throw new CacheUnsafeError(decision);\n }\n return { hints: decision.hints, scopeKey: decision.scopeKey };\n}\n","/**\n * Client / proxy-side cache that honors SEP-2549 hints — and never leaks a\n * `private` result across authorization contexts.\n *\n * Safety model (the whole point of this package):\n * - Every stored entry is keyed by `requestKey` AND `scopeKey`.\n * - `public` results use a single shared scopeKey, so they ARE shared.\n * - `private` results use a scopeKey derived from the caller's `scopeId`, so a\n * lookup by a different caller derives a different key and structurally cannot\n * hit another caller's entry.\n * - Anything we cannot prove safe (missing/invalid hints, private-without-scope,\n * 0 TTL) is simply not stored. `get` of such a thing is always a miss.\n */\n\nimport { cacheSafety, deriveScopeKey } from \"./safety.js\";\nimport {\n CacheScope,\n type CacheDecision,\n type CacheHints,\n type UncacheableReason,\n} from \"./types.js\";\n\n/** Monotonic-ish clock returning epoch milliseconds. Injectable for tests. */\nexport type Clock = () => number;\n\n/** A request identity: either a precomputed string key, or the raw JSON-RPC request. */\nexport type RequestKeyInput = string | { method: string; params?: unknown };\n\n/** Options for {@link McpResultCache}. */\nexport interface McpResultCacheOptions {\n /** Max number of live entries. Oldest-inserted entries are evicted first. Default 1000. `0`/negative disables caching. */\n maxEntries?: number;\n /** Clock for TTL math. Default `Date.now`. Override with fake timers in tests. */\n clock?: Clock;\n /** Treat a `0` TTL as cacheable. Default `false` (0 = immediately stale). */\n allowZeroTtl?: boolean;\n}\n\n/** Per-lookup options. */\nexport interface LookupOptions {\n /**\n * Identity of the caller's authorization context (token hash / user / tenant /\n * session id). REQUIRED to read or write `private` entries; ignored for `public`.\n */\n scopeId?: string;\n}\n\n/** Outcome of {@link McpResultCache.set}. */\nexport type SetOutcome =\n | { stored: true; scopeKey: string; expiresAt: number; hints: CacheHints }\n | { stored: false; reason: UncacheableReason; message: string };\n\n/** Outcome of {@link McpResultCache.get}. */\nexport type GetOutcome<T> =\n | { hit: true; value: T; hints: CacheHints; expiresAt: number }\n | { hit: false; reason: GetMissReason };\n\n/** Why a {@link McpResultCache.get} missed. */\nexport type GetMissReason =\n /** No entry for this (request, scope). */\n | \"miss\"\n /** An entry existed but its TTL had elapsed (it has been evicted). */\n | \"expired\";\n\n/** Snapshot counters for observability. Read via {@link McpResultCache.stats}. */\nexport interface CacheStats {\n hits: number;\n misses: number;\n expired: number;\n stores: number;\n /** `set` calls rejected because the result was not cache-safe. */\n rejected: number;\n evictions: number;\n /** Current number of live (not-yet-pruned) entries. */\n size: number;\n}\n\ninterface Entry<T> {\n value: T;\n hints: CacheHints;\n /** Absolute epoch-ms expiry. */\n expiresAt: number;\n}\n\n/**\n * Build a stable cache key from a request. If given a string, it is used as-is.\n * If given `{ method, params }`, params are deterministically serialized (object\n * keys sorted recursively) so that semantically equal requests collide.\n */\nexport function deriveRequestKey(input: RequestKeyInput): string {\n if (typeof input === \"string\") return input;\n return `${input.method}\u0000${stableStringify(input.params)}`;\n}\n\n/** Deterministic JSON: object keys sorted recursively. Arrays keep order. */\nexport function stableStringify(value: unknown): string {\n return JSON.stringify(normalize(value));\n}\n\nfunction normalize(value: unknown): unknown {\n if (value === null || typeof value !== \"object\") return value;\n if (Array.isArray(value)) return value.map(normalize);\n const obj = value as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(obj).sort()) {\n out[key] = normalize(obj[key]);\n }\n return out;\n}\n\n/** Compose the internal storage key from request key + scope key. */\nfunction storageKey(requestKey: string, scopeKey: string): string {\n return `${scopeKey}\u0000${requestKey}`;\n}\n\n/**\n * A small, dependency-free, SEP-2549-aware result cache safe for use in MCP\n * clients, gateways, and proxies.\n */\nexport class McpResultCache {\n readonly #maxEntries: number;\n readonly #clock: Clock;\n readonly #allowZeroTtl: boolean;\n // Insertion-ordered (Map preserves order) — enables O(1) FIFO eviction.\n readonly #store = new Map<string, Entry<unknown>>();\n #stats: Omit<CacheStats, \"size\"> = {\n hits: 0,\n misses: 0,\n expired: 0,\n stores: 0,\n rejected: 0,\n evictions: 0,\n };\n\n constructor(options: McpResultCacheOptions = {}) {\n this.#maxEntries = options.maxEntries ?? 1000;\n this.#clock = options.clock ?? Date.now;\n this.#allowZeroTtl = options.allowZeroTtl ?? false;\n }\n\n /**\n * Store a result IF it is cache-safe for the given scope. Validates hints,\n * derives the scope key (refusing `private` without a `scopeId`), and stores\n * keyed by (request, scope). Returns whether it was stored and why not.\n *\n * Always safe to call on any result — non-cacheable results are silently\n * rejected (counted in {@link CacheStats.rejected}) rather than stored.\n */\n set<T extends object>(\n request: RequestKeyInput,\n result: T,\n options: LookupOptions = {},\n ): SetOutcome {\n if (this.#maxEntries <= 0) {\n return { stored: false, reason: \"missing-fields\", message: \"cache disabled (maxEntries <= 0)\" };\n }\n const decision: CacheDecision = cacheSafety(result, {\n ...(options.scopeId !== undefined ? { scopeId: options.scopeId } : {}),\n allowZeroTtl: this.#allowZeroTtl,\n });\n if (!decision.cacheable) {\n this.#stats.rejected++;\n return { stored: false, reason: decision.reason, message: decision.message };\n }\n\n const requestKey = deriveRequestKey(request);\n const key = storageKey(requestKey, decision.scopeKey);\n const expiresAt = this.#clock() + decision.hints.ttlMs;\n\n // Refresh insertion order on overwrite so re-stored hot entries aren't the\n // first to be evicted.\n this.#store.delete(key);\n this.#store.set(key, { value: result, hints: decision.hints, expiresAt });\n this.#stats.stores++;\n\n this.#evictIfNeeded();\n return { stored: true, scopeKey: decision.scopeKey, expiresAt, hints: decision.hints };\n }\n\n /**\n * Look up a result for the given request AND scope identity.\n *\n * A `private` entry is ONLY returned to the same `scopeId` that stored it: the\n * lookup derives the scope key from the caller's `scopeId`, so another caller's\n * lookup targets a different key and can never reach it. A `public` entry is\n * returned to anyone (it is stored under the shared public key).\n *\n * Expired entries are treated as a miss and removed lazily on access.\n */\n get<T>(request: RequestKeyInput, options: LookupOptions = {}): GetOutcome<T> {\n const requestKey = deriveRequestKey(request);\n\n // Prefer the caller's OWN private bucket (more specific) over the shared\n // public bucket for the same request, then fall back to public. We never try\n // another caller's private key — there is no way to construct it without their\n // scopeId, so private-first cannot leak.\n const candidateScopeKeys: string[] = [];\n const privateKey = deriveScopeKey(CacheScope.Private, options.scopeId);\n if (privateKey !== undefined) candidateScopeKeys.push(privateKey);\n candidateScopeKeys.push(deriveScopeKey(CacheScope.Public)!);\n\n for (const scopeKey of candidateScopeKeys) {\n const key = storageKey(requestKey, scopeKey);\n const entry = this.#store.get(key);\n if (entry === undefined) continue;\n\n if (this.#clock() >= entry.expiresAt) {\n this.#store.delete(key);\n this.#stats.expired++;\n // Keep scanning other candidate buckets — a fresh public entry might\n // still satisfy this lookup even if a private one expired.\n continue;\n }\n this.#stats.hits++;\n return {\n hit: true,\n value: entry.value as T,\n hints: entry.hints,\n expiresAt: entry.expiresAt,\n };\n }\n\n this.#stats.misses++;\n return { hit: false, reason: \"miss\" };\n }\n\n /**\n * Convenience wrapper: return the cached value, or compute + store it.\n *\n * If a fresh entry exists it is returned. Otherwise `loader()` runs, its result\n * is offered to {@link set} (stored only if cache-safe), and returned regardless.\n */\n async getOrLoad<T extends object>(\n request: RequestKeyInput,\n loader: () => Promise<T> | T,\n options: LookupOptions = {},\n ): Promise<T> {\n const cached = this.get<T>(request, options);\n if (cached.hit) return cached.value;\n const value = await loader();\n this.set(request, value, options);\n return value;\n }\n\n /** Remove all entries whose TTL has elapsed. Returns the count removed. */\n prune(): number {\n const now = this.#clock();\n let removed = 0;\n for (const [key, entry] of this.#store) {\n if (now >= entry.expiresAt) {\n this.#store.delete(key);\n removed++;\n }\n }\n this.#stats.expired += removed;\n return removed;\n }\n\n /** Drop everything. Does not reset stat counters. */\n clear(): void {\n this.#store.clear();\n }\n\n /** Current live entry count (without pruning). */\n get size(): number {\n return this.#store.size;\n }\n\n /** A snapshot of counters plus current size. */\n stats(): CacheStats {\n return { ...this.#stats, size: this.#store.size };\n }\n\n #evictIfNeeded(): void {\n while (this.#store.size > this.#maxEntries) {\n const oldest = this.#store.keys().next().value;\n if (oldest === undefined) break;\n this.#store.delete(oldest);\n this.#stats.evictions++;\n }\n }\n}\n"]}
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Core types for mcp-cache-kit, modeled on MCP SEP-2549.
3
+ *
4
+ * SEP-2549 (MCP spec, 2026-07-28 release candidate) adds two top-level fields to
5
+ * cacheable results (`tools/list`, `resources/list`, `resources/templates/list`,
6
+ * `prompts/list`, `resources/read`):
7
+ *
8
+ * - `ttlMs: number` — how long the client MAY treat the result as fresh,
9
+ * analogous to HTTP `Cache-Control: max-age`. `@minimum 0`.
10
+ * - `cacheScope: "public" | "private"` — analogous to HTTP
11
+ * `Cache-Control: public` vs `private`.
12
+ *
13
+ * Source of truth (verified):
14
+ * https://github.com/modelcontextprotocol/modelcontextprotocol (schema/draft/schema.ts,
15
+ * docs/specification/draft/server/utilities/caching.mdx) and
16
+ * https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/
17
+ *
18
+ * NOTE: the 2026-07-28 spec is a release candidate. These field names/semantics
19
+ * may still shift before final. This library is intentionally tolerant of missing
20
+ * or malformed fields and treats anything it cannot prove safe as uncacheable.
21
+ */
22
+ /**
23
+ * The allowed values of `cacheScope` per SEP-2549.
24
+ *
25
+ * - `"public"` — the response does not contain user-specific data. Any client or
26
+ * intermediary MAY cache it and serve it across authorization contexts.
27
+ * - `"private"` — the response MAY be cached and reused only within the SAME
28
+ * authorization context. Caches MUST NOT be shared across authorization contexts
29
+ * (a different access token/user/session requires a different cache entry).
30
+ */
31
+ declare const CacheScope: {
32
+ /** Shareable across authorization contexts. */
33
+ readonly Public: "public";
34
+ /** Only reusable within the same authorization context. */
35
+ readonly Private: "private";
36
+ };
37
+ /** Union of the valid `cacheScope` string values: `"public" | "private"`. */
38
+ type CacheScope = (typeof CacheScope)[keyof typeof CacheScope];
39
+ /** Immutable list of valid scope values, handy for validation/iteration. */
40
+ declare const CACHE_SCOPE_VALUES: readonly CacheScope[];
41
+ /**
42
+ * The SEP-2549 cache hint fields as they appear (top-level) on a cacheable result.
43
+ */
44
+ interface CacheHints {
45
+ /**
46
+ * How long (ms) the client MAY treat the result as fresh. `>= 0`.
47
+ * `0` means "immediately stale". See {@link CacheScope} for scope semantics.
48
+ */
49
+ ttlMs: number;
50
+ /** Whether the result is safe to share across authorization contexts. */
51
+ cacheScope: CacheScope;
52
+ }
53
+ /**
54
+ * Minimal structural shape of an MCP result that MAY carry cache hints.
55
+ *
56
+ * Kept deliberately loose (`Record<string, unknown>`) so the helpers work on plain
57
+ * result objects WITHOUT a hard dependency on `@modelcontextprotocol/sdk`. If you
58
+ * do use the SDK, its `ListToolsResult` / `ReadResourceResult` (etc.) structurally
59
+ * satisfy this type.
60
+ */
61
+ interface MaybeCacheableResult extends Record<string, unknown> {
62
+ ttlMs?: unknown;
63
+ cacheScope?: unknown;
64
+ }
65
+ /**
66
+ * A result that carries valid, fully-typed SEP-2549 cache hints.
67
+ * `T` is the underlying result type so callers keep their concrete shape.
68
+ */
69
+ type WithCacheHints<T extends object> = T & CacheHints;
70
+ /** Options accepted by {@link withCacheHints}. */
71
+ interface CacheHintsInput {
72
+ /** Time-to-live in milliseconds. Must be a finite number `>= 0`. */
73
+ ttlMs: number;
74
+ /** One of {@link CacheScope}. */
75
+ cacheScope: CacheScope;
76
+ }
77
+ /**
78
+ * Result of parsing the cache hints off an unknown result object.
79
+ *
80
+ * `ok: true` only when BOTH fields are present and valid per spec. Otherwise
81
+ * `ok: false` with a machine-readable `reason` and human-readable `message`.
82
+ */
83
+ type ParsedCacheHints = {
84
+ ok: true;
85
+ hints: CacheHints;
86
+ } | {
87
+ ok: false;
88
+ reason: UncacheableReason;
89
+ message: string;
90
+ };
91
+ /** Machine-readable reasons a result is considered uncacheable. */
92
+ type UncacheableReason =
93
+ /** The result was null/undefined or not an object. */
94
+ "not-an-object"
95
+ /** `ttlMs` or `cacheScope` was absent. Fail-safe: don't cache. */
96
+ | "missing-fields"
97
+ /** `ttlMs` was present but not a finite number `>= 0`. */
98
+ | "invalid-ttl"
99
+ /** `cacheScope` was present but not `"public" | "private"`. */
100
+ | "invalid-scope"
101
+ /** `ttlMs` was `0` — explicitly "immediately stale", so nothing to store. */
102
+ | "zero-ttl"
103
+ /** A `private` result was offered without a scope identity to key it by. */
104
+ | "private-without-scope";
105
+ /**
106
+ * The decision returned by {@link cacheSafety} / used by the cache: may this result
107
+ * be stored for the given scope identity, and if not, why.
108
+ */
109
+ type CacheDecision = {
110
+ cacheable: true;
111
+ hints: CacheHints;
112
+ /**
113
+ * The scope key the entry MUST be stored under. For `public` results this is
114
+ * a shared constant; for `private` results it is derived from the caller's
115
+ * authorization-context identity.
116
+ */
117
+ scopeKey: string;
118
+ } | {
119
+ cacheable: false;
120
+ reason: UncacheableReason;
121
+ message: string;
122
+ };
123
+
124
+ /**
125
+ * Server-side helpers: attach and read SEP-2549 cache hints on results.
126
+ */
127
+
128
+ /** Type guard for the `cacheScope` enum. */
129
+ declare function isCacheScope(value: unknown): value is CacheScope;
130
+ /**
131
+ * True if `ttlMs` is a valid SEP-2549 TTL: a finite, non-negative number.
132
+ *
133
+ * The spec annotates `ttlMs` with `@minimum 0` and treats negative/absent values
134
+ * as `0`. We require a real finite number here (NaN / Infinity are rejected).
135
+ */
136
+ declare function isValidTtlMs(value: unknown): value is number;
137
+ /**
138
+ * Validate a {@link CacheHintsInput}, throwing a descriptive `TypeError` on bad input.
139
+ * Returns the normalized {@link CacheHints} (ttlMs floored to an integer ms).
140
+ *
141
+ * @throws {TypeError} if `ttlMs` is not a finite number `>= 0`, or `cacheScope`
142
+ * is not `"public" | "private"`.
143
+ */
144
+ declare function validateCacheHints(input: CacheHintsInput): CacheHints;
145
+ /**
146
+ * Server-side: attach SEP-2549 cache hints to a `tools/list` / `resources/read`
147
+ * (etc.) result so clients and proxies can cache it correctly.
148
+ *
149
+ * Returns a NEW object (does not mutate the input) with `ttlMs` and `cacheScope`
150
+ * set as top-level fields, exactly where the spec places them.
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * server.setRequestHandler(ListToolsRequestSchema, () =>
155
+ * withCacheHints({ tools }, { ttlMs: 60_000, cacheScope: CacheScope.Public }),
156
+ * );
157
+ * ```
158
+ *
159
+ * @throws {TypeError} via {@link validateCacheHints} on invalid hints.
160
+ */
161
+ declare function withCacheHints<T extends object>(result: T, hints: CacheHintsInput): WithCacheHints<T>;
162
+ /**
163
+ * Convenience: a `public` result fresh for `ttlMs`. Safe to share across users.
164
+ */
165
+ declare function publicHints(ttlMs: number): CacheHints;
166
+ /**
167
+ * Convenience: a `private` result fresh for `ttlMs`. Reusable only within the
168
+ * same authorization context — the cache will refuse to share it across scopes.
169
+ */
170
+ declare function privateHints(ttlMs: number): CacheHints;
171
+ /**
172
+ * Client/proxy-side: read and validate the cache hints off an unknown result.
173
+ *
174
+ * Returns `{ ok: true, hints }` only when BOTH fields are present and valid.
175
+ * Anything else returns `{ ok: false, reason, message }` — this is the fail-safe
176
+ * primitive the cache builds on: if we cannot prove the hints, we don't cache.
177
+ *
178
+ * This never throws. It is safe to call on arbitrary JSON-RPC result payloads.
179
+ */
180
+ declare function parseCacheHints(result: unknown): ParsedCacheHints;
181
+
182
+ /**
183
+ * The safety layer: decide whether a result may be cached for a given
184
+ * authorization-context identity, and derive the scope key it must be stored under.
185
+ *
186
+ * This is where the cross-user-leak trap is closed. SEP-2549 says a `private`
187
+ * result "MAY be cached and reused only within the same authorization context".
188
+ * We enforce that by KEYING private entries with the caller's scope identity, so a
189
+ * `private` entry stored for user A is structurally unreachable for user B.
190
+ */
191
+
192
+ /**
193
+ * The single, shared scope key used for `public` results. Chosen to be a value
194
+ * that cannot collide with any real scope identity (which is escaped, see below).
195
+ */
196
+ declare const PUBLIC_SCOPE_KEY = "public";
197
+ /** Options for {@link cacheSafety} / {@link assertCacheSafe}. */
198
+ interface CacheSafetyOptions {
199
+ /**
200
+ * Identity of the caller's authorization context — e.g. an access-token hash,
201
+ * user id, tenant id, or session id. REQUIRED to cache `private` results.
202
+ * Pass it for `public` results too if you have it; it will simply be ignored.
203
+ */
204
+ scopeId?: string;
205
+ /**
206
+ * If true, a `ttlMs` of `0` is reported as cacheable (with `zero-ttl` being a
207
+ * non-fatal note). Default `false`: a 0 TTL means "immediately stale", so there
208
+ * is nothing worth storing and we report it uncacheable. The cache uses the
209
+ * default.
210
+ */
211
+ allowZeroTtl?: boolean;
212
+ }
213
+ /**
214
+ * Derive the scope key an entry must be stored under.
215
+ *
216
+ * - `public` → {@link PUBLIC_SCOPE_KEY} (shared across all callers).
217
+ * - `private` → a key derived from `scopeId`, namespaced so it can never collide
218
+ * with the public bucket or with another scope id.
219
+ *
220
+ * Returns `undefined` for a `private` result when no `scopeId` is supplied —
221
+ * the caller MUST treat that as "do not cache".
222
+ */
223
+ declare function deriveScopeKey(cacheScope: CacheScope, scopeId?: string): string | undefined;
224
+ /**
225
+ * Decide whether `result` may be cached for the given scope identity.
226
+ *
227
+ * Returns a {@link CacheDecision}: when `cacheable: true` it includes the validated
228
+ * hints and the `scopeKey` to store the entry under; when `cacheable: false` it
229
+ * includes a machine-readable `reason` and a human-readable `message`.
230
+ *
231
+ * Fail-safe by construction: missing/invalid hints, an unrecognized scope, a
232
+ * `private` result without a `scopeId`, and (by default) a `0` TTL all return
233
+ * `cacheable: false`. Never throws.
234
+ *
235
+ * Use this as a guard anywhere in a proxy/gateway:
236
+ * ```ts
237
+ * const d = cacheSafety(result, { scopeId: tokenHash });
238
+ * if (d.cacheable) store(key, d.scopeKey, result, d.hints.ttlMs);
239
+ * ```
240
+ */
241
+ declare function cacheSafety(result: unknown, options?: CacheSafetyOptions): CacheDecision;
242
+ /** Error thrown by {@link assertCacheSafe} when a result may not be cached. */
243
+ declare class CacheUnsafeError extends Error {
244
+ readonly name = "CacheUnsafeError";
245
+ /** Machine-readable reason, mirrors {@link CacheDecision}'s `reason`. */
246
+ readonly reason: Extract<CacheDecision, {
247
+ cacheable: false;
248
+ }>["reason"];
249
+ constructor(decision: Extract<CacheDecision, {
250
+ cacheable: false;
251
+ }>);
252
+ }
253
+ /**
254
+ * Assert that `result` may be cached for the given scope identity, throwing a
255
+ * {@link CacheUnsafeError} otherwise. Returns the validated hints + scopeKey.
256
+ *
257
+ * Prefer {@link cacheSafety} when you want to branch without exceptions; use this
258
+ * when "must be cacheable here" is an invariant you want to enforce loudly.
259
+ */
260
+ declare function assertCacheSafe(result: unknown, options?: CacheSafetyOptions): {
261
+ hints: CacheHints;
262
+ scopeKey: string;
263
+ };
264
+
265
+ /**
266
+ * Client / proxy-side cache that honors SEP-2549 hints — and never leaks a
267
+ * `private` result across authorization contexts.
268
+ *
269
+ * Safety model (the whole point of this package):
270
+ * - Every stored entry is keyed by `requestKey` AND `scopeKey`.
271
+ * - `public` results use a single shared scopeKey, so they ARE shared.
272
+ * - `private` results use a scopeKey derived from the caller's `scopeId`, so a
273
+ * lookup by a different caller derives a different key and structurally cannot
274
+ * hit another caller's entry.
275
+ * - Anything we cannot prove safe (missing/invalid hints, private-without-scope,
276
+ * 0 TTL) is simply not stored. `get` of such a thing is always a miss.
277
+ */
278
+
279
+ /** Monotonic-ish clock returning epoch milliseconds. Injectable for tests. */
280
+ type Clock = () => number;
281
+ /** A request identity: either a precomputed string key, or the raw JSON-RPC request. */
282
+ type RequestKeyInput = string | {
283
+ method: string;
284
+ params?: unknown;
285
+ };
286
+ /** Options for {@link McpResultCache}. */
287
+ interface McpResultCacheOptions {
288
+ /** Max number of live entries. Oldest-inserted entries are evicted first. Default 1000. `0`/negative disables caching. */
289
+ maxEntries?: number;
290
+ /** Clock for TTL math. Default `Date.now`. Override with fake timers in tests. */
291
+ clock?: Clock;
292
+ /** Treat a `0` TTL as cacheable. Default `false` (0 = immediately stale). */
293
+ allowZeroTtl?: boolean;
294
+ }
295
+ /** Per-lookup options. */
296
+ interface LookupOptions {
297
+ /**
298
+ * Identity of the caller's authorization context (token hash / user / tenant /
299
+ * session id). REQUIRED to read or write `private` entries; ignored for `public`.
300
+ */
301
+ scopeId?: string;
302
+ }
303
+ /** Outcome of {@link McpResultCache.set}. */
304
+ type SetOutcome = {
305
+ stored: true;
306
+ scopeKey: string;
307
+ expiresAt: number;
308
+ hints: CacheHints;
309
+ } | {
310
+ stored: false;
311
+ reason: UncacheableReason;
312
+ message: string;
313
+ };
314
+ /** Outcome of {@link McpResultCache.get}. */
315
+ type GetOutcome<T> = {
316
+ hit: true;
317
+ value: T;
318
+ hints: CacheHints;
319
+ expiresAt: number;
320
+ } | {
321
+ hit: false;
322
+ reason: GetMissReason;
323
+ };
324
+ /** Why a {@link McpResultCache.get} missed. */
325
+ type GetMissReason =
326
+ /** No entry for this (request, scope). */
327
+ "miss"
328
+ /** An entry existed but its TTL had elapsed (it has been evicted). */
329
+ | "expired";
330
+ /** Snapshot counters for observability. Read via {@link McpResultCache.stats}. */
331
+ interface CacheStats {
332
+ hits: number;
333
+ misses: number;
334
+ expired: number;
335
+ stores: number;
336
+ /** `set` calls rejected because the result was not cache-safe. */
337
+ rejected: number;
338
+ evictions: number;
339
+ /** Current number of live (not-yet-pruned) entries. */
340
+ size: number;
341
+ }
342
+ /**
343
+ * Build a stable cache key from a request. If given a string, it is used as-is.
344
+ * If given `{ method, params }`, params are deterministically serialized (object
345
+ * keys sorted recursively) so that semantically equal requests collide.
346
+ */
347
+ declare function deriveRequestKey(input: RequestKeyInput): string;
348
+ /** Deterministic JSON: object keys sorted recursively. Arrays keep order. */
349
+ declare function stableStringify(value: unknown): string;
350
+ /**
351
+ * A small, dependency-free, SEP-2549-aware result cache safe for use in MCP
352
+ * clients, gateways, and proxies.
353
+ */
354
+ declare class McpResultCache {
355
+ #private;
356
+ constructor(options?: McpResultCacheOptions);
357
+ /**
358
+ * Store a result IF it is cache-safe for the given scope. Validates hints,
359
+ * derives the scope key (refusing `private` without a `scopeId`), and stores
360
+ * keyed by (request, scope). Returns whether it was stored and why not.
361
+ *
362
+ * Always safe to call on any result — non-cacheable results are silently
363
+ * rejected (counted in {@link CacheStats.rejected}) rather than stored.
364
+ */
365
+ set<T extends object>(request: RequestKeyInput, result: T, options?: LookupOptions): SetOutcome;
366
+ /**
367
+ * Look up a result for the given request AND scope identity.
368
+ *
369
+ * A `private` entry is ONLY returned to the same `scopeId` that stored it: the
370
+ * lookup derives the scope key from the caller's `scopeId`, so another caller's
371
+ * lookup targets a different key and can never reach it. A `public` entry is
372
+ * returned to anyone (it is stored under the shared public key).
373
+ *
374
+ * Expired entries are treated as a miss and removed lazily on access.
375
+ */
376
+ get<T>(request: RequestKeyInput, options?: LookupOptions): GetOutcome<T>;
377
+ /**
378
+ * Convenience wrapper: return the cached value, or compute + store it.
379
+ *
380
+ * If a fresh entry exists it is returned. Otherwise `loader()` runs, its result
381
+ * is offered to {@link set} (stored only if cache-safe), and returned regardless.
382
+ */
383
+ getOrLoad<T extends object>(request: RequestKeyInput, loader: () => Promise<T> | T, options?: LookupOptions): Promise<T>;
384
+ /** Remove all entries whose TTL has elapsed. Returns the count removed. */
385
+ prune(): number;
386
+ /** Drop everything. Does not reset stat counters. */
387
+ clear(): void;
388
+ /** Current live entry count (without pruning). */
389
+ get size(): number;
390
+ /** A snapshot of counters plus current size. */
391
+ stats(): CacheStats;
392
+ }
393
+
394
+ export { CACHE_SCOPE_VALUES, type CacheDecision, type CacheHints, type CacheHintsInput, type CacheSafetyOptions, CacheScope, type CacheStats, CacheUnsafeError, type Clock, type GetMissReason, type GetOutcome, type LookupOptions, type MaybeCacheableResult, McpResultCache, type McpResultCacheOptions, PUBLIC_SCOPE_KEY, type ParsedCacheHints, type RequestKeyInput, type SetOutcome, type UncacheableReason, type WithCacheHints, assertCacheSafe, cacheSafety, deriveRequestKey, deriveScopeKey, isCacheScope, isValidTtlMs, parseCacheHints, privateHints, publicHints, stableStringify, validateCacheHints, withCacheHints };