Motivation
Most MCP tool calls are reads — list documents, get schema, fetch records. When an agent calls list_invoices twice in 30 seconds with the same parameters, there's no reason to hit the backend again.
A cache middleware would memoize results by tool name + input hash, with configurable TTL per tool.
Proposal
As middleware
import { createCacheMiddleware } from "@casys/mcp-server";
server.use(createCacheMiddleware({
defaultTtlMs: 30_000,
perTool: {
"list_invoices": { ttlMs: 60_000 },
"create_invoice": { enabled: false }, // never cache writes
},
}));
Storage backends — pluggable interface
interface CacheStore {
get(key: string[]): Promise<unknown | null>;
set(key: string[], value: unknown, opts?: { expireIn?: number }): Promise<void>;
delete(key: string[]): Promise<void>;
}
Deno (default): native Deno.openKv()
Zero config, persistent (SQLite-backed), built-in expiry:
const kv = await Deno.openKv();
await kv.set(["cache", toolName, argsHash], result, { expireIn: ttlMs });
const cached = await kv.get(["cache", toolName, argsHash]);
Node: @deno/kv (optional) or in-memory Map
@deno/kv — same API as Deno KV, backed by SQLite on Node. Optional dependency. Gives persistent cache with zero behavior difference between runtimes.
- In-memory Map — zero-dependency fallback, good enough for single-process. TTL via setTimeout or lazy expiry on read.
- Custom — user provides their own
CacheStore implementation (Redis, etc.)
Auto-detection: if Deno.openKv exists → use it. Else if @deno/kv is installed → use it. Else → in-memory Map.
Cache key
Hash of toolName + JSON.stringify(sortedArgs) — deterministic, collision-free for same inputs.
Behavior
- Cache sits early in the pipeline (after auth, before circuit breaker) — cached responses skip the heavy path
- Write tools (
create_*, update_*, delete_*) disabled by default (opt-in only)
- Cache invalidation: manual
cache.clear(toolName) or automatic on related write tools
- Metrics: cache hit/miss counters exposed via Prometheus
Pipeline position
auth → cache → circuit-breaker → retry → backpressure → handler
Scope
Motivation
Most MCP tool calls are reads — list documents, get schema, fetch records. When an agent calls
list_invoicestwice in 30 seconds with the same parameters, there's no reason to hit the backend again.A cache middleware would memoize results by tool name + input hash, with configurable TTL per tool.
Proposal
As middleware
Storage backends — pluggable interface
Deno (default): native
Deno.openKv()Zero config, persistent (SQLite-backed), built-in expiry:
Node:
@deno/kv(optional) or in-memory Map@deno/kv— same API as Deno KV, backed by SQLite on Node. Optional dependency. Gives persistent cache with zero behavior difference between runtimes.CacheStoreimplementation (Redis, etc.)Auto-detection: if
Deno.openKvexists → use it. Else if@deno/kvis installed → use it. Else → in-memory Map.Cache key
Hash of
toolName + JSON.stringify(sortedArgs)— deterministic, collision-free for same inputs.Behavior
create_*,update_*,delete_*) disabled by default (opt-in only)cache.clear(toolName)or automatic on related write toolsPipeline position
Scope
createCacheMiddleware()with TTL configCacheStorepluggable interface@deno/kvbackend (optional on Node)