# MCP Tool Annotations Explained: readOnlyHint, idempotentHint, and destructiveHint

> Learn about MCP tool annotations readOnlyHint idempotentHint and destructiveHint These metadata flags help LLMs safely invoke functions retry operations and request user confirmation

- Repository: [Model Context Protocol/servers](https://github.com/modelcontextprotocol/servers)
- Tags: deep-dive
- Published: 2026-03-01

---

**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()`:

```typescript
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`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts) (lines 220-316), read operations like `read_text_file`, `read_media_file`, and `read_multiple_files` all declare `readOnlyHint: true`:

```typescript
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`](https://github.com/modelcontextprotocol/servers/blob/main/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:

```typescript
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`](https://github.com/modelcontextprotocol/servers/blob/main/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:

```typescript
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:

```typescript
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: true`** identifies observation-only tools like `read_text_file` in [`src/filesystem/index.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts).
- **`idempotentHint: true`** marks safe-to-retry operations like `write_file` where repeated calls yield identical results.
- **`destructiveHint: true`** warns about state-changing operations like `edit_file` that 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`](https://github.com/modelcontextprotocol/servers/blob/main/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`](https://github.com/modelcontextprotocol/servers/blob/main/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`](https://github.com/modelcontextprotocol/servers/blob/main/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.