Caching makes your PostgreSQL MCP server fast. But if your cache key is just tool name + JSON arguments, you've quietly deleted your authorization boundary.
Two users can call the same tool with identical arguments while belonging to different tenants, roles, or environments. With a weak key, the second user gets the first user's cached results — including rows they were never allowed to see.
This isn't hypothetical. It's the default behavior of many naive MCP server implementations.
Key Takeaways
- MCP cache keys must include auth attributes like tenant and role.
- Separate six cache classes to prevent cross-scope leaks.
- Test with negative cross-tenant scenarios.
The Problem: Cache Keys That Ignore Identity
Most MCP servers cache by hashing the tool name and the serialized arguments. That works for a single-user local setup. But as soon as you deploy a PostgreSQL MCP server behind a gateway serving multiple tenants, that key is insufficient.
Consider two users: Alice in tenant A and Bob in tenant B. Both call query_database with {"sql": "SELECT * FROM orders"}. A naive cache stores the result under query_database:{"sql":"..."}. Bob's identical call hits the cache and receives Alice's orders.
Even worse: tool discovery metadata can reveal sensitive structure. The list of available tools, their schemas, and their descriptions may differ per role. Caching that metadata without the caller's role leaks internal API shapes.
The Fix: Separate Cache Classes and Enrich the Key
The source article recommends two moves: separate cache classes, then include every authorization-relevant attribute in the key.
1. Separate Six Cache Classes
Don't use one cache for everything. Split into:
- Tool discovery — the list of tools and their schemas
- Schema metadata — table/column definitions
- Policy decisions — whether a caller may perform an operation
- Prepared operation shapes — compiled SQL or query plans
- Database results — actual rows returned
- Resumable state handles — pagination or streaming state
Each class has different sensitivity and different invalidation requirements. Policy decisions change when roles change; database results change when data changes. Mixing them forces you to invalidate everything on any change.
2. Build an Authorization-Aware Key
For every cache entry, compute the key from:
- Principal (user ID)
- Tenant ID
- Execution role
- Environment (prod/staging/dev)
- Policy version
- Tool-catalog version
- Schema version
- Normalized arguments (canonical JSON, sorted keys)
- Source watermark (data lineage or timestamp)
Include all of these in the hash. If any one changes, the cache miss is correct.
Three Details That Matter in Practice

Tool discovery leaks structure
Even listing available tools can reveal sensitive internal schema. Cache discovery responses per role and tenant, not globally.
Single-flight coalescing must use the same key
If you coalesce concurrent identical requests (single-flight), the coalescing key must be the same authorization-aware key. Otherwise, two users' requests merge into one execution, and the second user receives the first user's authorized result.
Cache hits must preserve evidence
A cache hit must still apply freshness, truncation, redaction, and trace metadata. Don't return a cached result that skips row-level security or truncation rules applied on the original query.
The Negative Test
Warm the cache, then repeat identical arguments across:
- Another tenant
- Another role
- Another environment
- A revoked approval
- A changed schema
Measure the absence of cross-scope hits — not just the aggregate hit rate. A high hit rate with cross-tenant leaks is a security incident, not a performance win.
Try It Now
If you're building or maintaining a PostgreSQL MCP server, audit your cache key today:
- Find every cache.put and cache.get call.
- Check the key composition. Does it include tenant and role?
- Separate cache classes if you haven't.
- Add a negative test that simulates cross-tenant identical calls.
This applies whether you use Redis, in-memory maps, or PostgreSQL itself as your cache layer. The principle is the same: the cache key is part of your authorization model.
The full guide with implementation details is available at the source article.
Source: dev.to









