Answers · MCP security
How do I cache MCP tool results safely?
Last updated
TL;DR: Cache only calls that read, and key every entry on everything that can change the answer: the server (its URL and the serverInfo.version it reported at initialize), the tool name, a hash of the tool definition you saw in tools/list, the canonicalized arguments, and who is asking (user, tenant or credential scope). Give entries a short TTL, never cache results with isError set, and never cache tools that change things: replaying a cached "sent" or "deleted" result means the action silently didn't happen. Don't let the server's hints decide this on their own. MCP tool annotations such as readOnlyHint and idempotentHint are hints; the specification says clients must treat them as untrusted unless they come from trusted servers and should never make tool use decisions based on annotations from untrusted servers. Keep your own allowlist of cacheable tools, and drop the cache when the server's tool list changes (notifications/tools/list_changed) or its version does. Caching the tool list is a separate, safer optimization: the OpenAI Agents SDK's cache_tools_list caches list_tools() only, and its docs say to enable it only if the tool definitions don't change often.
What should I check, step by step?
1. Decide per tool, from your own allowlist: cache read-only lookups (search, fetch, get); never cache tools that send, write, pay, delete or deploy.
- Treat annotations as input to that decision only for servers you trust. readOnlyHint defaults to false and destructiveHint to true, so a tool with no annotations should be treated as one that changes things.
- Build the key from: server URL, serverInfo.version, tool name, a hash of the tool's definition (description, inputSchema and outputSchema), the canonicalized arguments, and the caller's identity or tenant.
- Canonicalize arguments before hashing: RFC 8785 (JCS) sorts object keys recursively, keeps array order and serializes numbers one way, so {"a":1,"b":2} and {"b":2,"a":1} produce the same key. JCS does not normalize Unicode, so normalize strings yourself first if your inputs need it.
- Never cache results with isError: true; those are execution errors meant for the model to read and retry.
- Set a short TTL that matches how fast the underlying data changes. Tools that reach an open world (web search, live prices, inboxes) go stale fastest.
- Invalidate on notifications/tools/list_changed, on a new serverInfo.version, and when the tool definition hash changes.
- Keep the cache per user or per credential scope, so one person's results are never served to another.
- Mark cached results as cached (with their age) in what you hand the model or the user, so a stale answer can be recognized.
What the MCP specification gives you to work with
In the 2025-11-25 schema, ToolAnnotations are all hints: readOnlyHint (the tool does not modify its environment, default false), destructiveHint (default true), idempotentHint (calling repeatedly with the same arguments has no additional effect on its environment, default false) and openWorldHint (default true). The schema says they are not guaranteed to describe tool behavior faithfully.
- idempotentHint is about side effects, not about getting the same answer back. A read-only tool can return a different result a minute later; an idempotent write is still a write. Neither one makes a result safe to reuse indefinitely.
- A server that declares tools.listChanged SHOULD send notifications/tools/list_changed when its tools change, and the initialize result carries serverInfo with a required version string. Both are natural cache-invalidation signals.
- Tool execution errors come back in the result with isError: true, so the client can tell an error result from a successful one before storing it.
Why caching a side-effecting tool is dangerous
If a send_email or create_invoice call is answered from cache, the model sees a success message while nothing was sent or created. Retrying the same request is how agents recover from errors, so a cache in front of a write tool quietly turns retries into no-ops.
- Results also carry the caller's permissions. A cache keyed only on tool and arguments can hand one user's private search results or file contents to another user who sends the same arguments.
How do I ask Rightshold about this (free, no key)?
Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “This MCP server just shipped an update. Have its tool definitions changed since they were recorded, so I know whether to drop my cached results?”
{
"tool": "check_tool_integrity",
"arguments": {
"server": "<the server's Rightshold slug or id>"
}
}- The answer is unchanged, changed (with a plain diff of tools added, removed, description changed or schema changed) or unreachable, which is never reported as unchanged. A changed answer is a reason to invalidate cached results for the tools that changed. It works for servers registered on Rightshold.
- precall_check gives a one-read summary for any https MCP server: registry entry, live integrity probe, invisible-text scan of the definitions it serves now, security flags and tool-name collisions.
Rightshold does not see or store your tool calls, arguments or cached results. It compares the tool definitions a server serves now with a recorded fingerprint. Not legal advice.
What are the sources?
- Model Context Protocol specification (2025-11-25): Tools (listChanged, isError, annotations untrusted)
- Model Context Protocol schema reference (2025-11-25): ToolAnnotations, InitializeResult serverInfo, ToolListChangedNotification
- OpenAI Agents SDK (Python): MCP caching (cache_tools_list, invalidate_tools_cache)
- RFC 8785: JSON Canonicalization Scheme (JCS)
Related
- Are MCP tool annotations like readOnlyHint a security guarantee?
- How to cap MCP tool output size to prevent context flooding
- How to detect MCP tool description changes (rug pulls)
- How to pin MCP server versions and review changes before upgrading
Not legal advice. JSON · Markdown · All answers
Get a free API key in one call
No card and no email needed. 100 free credits; the license and policy checks stay free without a key.
curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_... # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
-H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'
