MCP Tool Annotations Explained: readOnlyHint, idempotentHint, and destructiveHint
TLDR: MCP tool annotations are declarative metadata flags—readOnlyHint, idempotentHint, and destructiveHint—that describe a tool's side effects to help LLMs decide when to invoke functions safely, retry operations, or request user confirmation.
Model Context Protocol (MCP) servers expose functions that LLMs invoke to interact with file systems, networks, and other host environments. In the modelcontextprotocol/servers repository, developers attach MCP tool annotations to tool definitions to declare behavioral constraints that help models make safer orchestration decisions.
What Are MCP Tool Annotations?
MCP tool annotations are optional metadata properties attached when registering a tool on an McpServer. These hints describe the side effects and safety characteristics of the underlying function without enforcing runtime behavior. The annotations object lives inside the tool definition passed to server.registerTool():
server.registerTool(
"tool_name",
{
title: "...",
description: "...",
inputSchema: {...},
annotations: { readOnlyHint: true, idempotentHint: false, destructiveHint: false }
},
handler
);
The MCP runtime surfaces these hints to LLMs and downstream UIs, allowing models to inspect a tool's risk profile before invocation.
The Three Core Hint Types
readOnlyHint
The readOnlyHint flag indicates that a tool only observes state and never mutates the host environment. When set to true, the model can invoke the tool freely without risking side effects.
In src/filesystem/index.ts (lines 220-316), read operations like read_text_file, read_media_file, and read_multiple_files all declare readOnlyHint: true:
server.registerTool(
"read_text_file",
{
title: "Read Text File",
description: "Read the contents of a text file",
inputSchema: {...},
annotations: { readOnlyHint: true }
},
readTextFileHandler
);
idempotentHint
The idempotentHint flag signals that re-invoking the tool with identical arguments yields the same result and does not accumulate state changes. This tells the model the operation is safe to retry on failure.
According to the source code in src/filesystem/index.ts at line 352, the write_file tool sets idempotentHint: true because writing the same content to a file repeatedly leaves the file in an unchanged state:
server.registerTool(
"write_file",
{
title: "Write File",
description: "Create or overwrite a file",
inputSchema: {...},
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true }
},
writeFileHandler
);
destructiveHint
The destructiveHint flag marks tools that alter persistent state through writes, deletes, or modifications. Models should treat these as risky operations that may require user confirmation.
Both write_file and edit_file in src/filesystem/index.ts carry destructiveHint: true. At line 382, the edit_file tool specifically sets idempotentHint: false because each edit changes the file content, making repeated calls non-idempotent:
server.registerTool(
"edit_file",
{
title: "Edit File",
description: "Make edits to a text file",
inputSchema: {...},
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true }
},
editFileHandler
);
How Annotations Flow Through the Runtime
These hints are metadata only—they do not enforce behavior at the SDK level. When a tool is registered via server.registerTool(), the MCP runtime stores the annotations object and exposes it to:
- LLM clients, which can inspect hints to decide whether to call a destructive tool
- UI layers, which can color-code or gate calls based on risk levels
- Instrumentation systems, which can log usage statistics distinguishing idempotent from non-idempotent operations
The annotations travel with the tool definition through the MCP protocol, allowing any compliant client to parse the safety characteristics before execution.
Practical Implementation Example
When building custom MCP servers, you can declare annotations to guide model behavior. Here is a complete example registering a safe, read-only tool:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export const registerEchoTool = (server: McpServer) => {
server.registerTool(
"echo",
{
title: "Echo",
description: "Returns the supplied string unchanged.",
inputSchema: { message: z.string() },
outputSchema: { reply: z.string() },
annotations: {
readOnlyHint: true,
idempotentHint: true,
destructiveHint: false
}
},
async (args) => ({
reply: args.message
})
);
};
This configuration tells the model that echo is safe to call repeatedly without side effects.
Summary
- MCP tool annotations are declarative metadata that describe a tool's side effects without enforcing runtime constraints.
readOnlyHint: trueidentifies observation-only tools likeread_text_fileinsrc/filesystem/index.ts.idempotentHint: truemarks safe-to-retry operations likewrite_filewhere repeated calls yield identical results.destructiveHint: truewarns about state-changing operations likeedit_filethat modify persistent data.- The MCP runtime surfaces these hints to LLMs and UI layers to enable safer orchestration and user confirmation flows.
Frequently Asked Questions
Do MCP tool annotations enforce runtime behavior?
No. The annotations are purely descriptive metadata. As implemented in modelcontextprotocol/servers, the hints are stored in the tool definition and exposed to clients, but the underlying handler function executes regardless of the hint values. The model or UI layer must interpret the hints to enforce safety policies.
Can a tool be both idempotent and destructive?
Yes. According to the source code in src/filesystem/index.ts, the write_file tool sets both idempotentHint: true and destructiveHint: true. Writing the same content to a file repeatedly is destructive (it overwrites data) but idempotent (the final state remains constant across multiple identical writes).
How do content annotations differ from tool annotations?
While tool annotations describe side effects, content annotations attach metadata to individual message fragments. The file src/everything/tools/get-annotated-message.ts demonstrates content-level annotations like priority and audience that control how specific text or image blocks render, separate from the tool-level hints like readOnlyHint.
Where are tool annotations defined in the filesystem server?
All built-in filesystem tool annotations are defined in src/filesystem/index.ts. Lines 220-316 contain read-only tools with readOnlyHint: true, while lines 352-354 define write_file and lines 382-384 define edit_file with their respective destructive and idempotent characteristics.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →