Introduction

Tool annotations are metadata hints that describe a tool's behavior characteristics. They help clients and LLMs make informed decisions about when and how to use a tool, especially around safety and side effects.

Key Concepts

  • readOnlyHint: Indicates the tool only reads data and does not modify any state.
  • destructiveHint: Signals the tool may irreversibly modify or delete data.
  • idempotentHint: Tells clients that calling the tool multiple times with the same input produces the same result.
  • openWorldHint: Indicates the tool interacts with external systems outside the server's control.

Real World Context

Consider a database administration MCP server. A list-tables tool would be readOnlyHint: true and destructiveHint: false, while a drop-table tool would be destructiveHint: true. A client like Claude Desktop could use these annotations to show a confirmation dialog before executing destructive tools, protecting users from accidental data loss.

Deep Dive

Annotations are added to the registerTool config object under the annotations key. Each annotation is a boolean hint.

Here is a read-only tool with annotations:

typescript
server.registerTool('list-files', {
  description: 'List files in a directory',
  inputSchema: z.object({
    path: z.string().describe('Directory path to list')
  }),
  annotations: {
    readOnlyHint: true,
    destructiveHint: false,
    idempotentHint: true,
    openWorldHint: false
  }
}, async ({ path }): Promise<CallToolResult> => {
  const files = await fs.readdir(path);
  return { content: [{ type: 'text', text: files.join('\n') }] };
});

This tells the client that list-files only reads data, never destroys anything, returns the same result for the same input, and operates on the local filesystem rather than external services.

Contrast this with a destructive tool:

typescript
server.registerTool('delete-file', {
  description: 'Delete a file permanently',
  inputSchema: z.object({
    path: z.string().describe('File path to delete')
  }),
  annotations: {
    readOnlyHint: false,
    destructiveHint: true,
    idempotentHint: true,
    openWorldHint: false
  }
}, async ({ path }): Promise<CallToolResult> => {
  await fs.unlink(path);
  return { content: [{ type: 'text', text: `Deleted ${path}` }] };
});

The destructiveHint: true flag signals that this tool permanently modifies the filesystem. A well-designed client might require explicit user confirmation before executing it. Note that idempotentHint is still true here because deleting the same file twice has the same end result (the file is gone).

Here is a tool that interacts with an external API:

typescript
server.registerTool('send-notification', {
  description: 'Send a push notification via external service',
  inputSchema: z.object({
    userId: z.string(),
    message: z.string()
  }),
  annotations: {
    readOnlyHint: false,
    destructiveHint: false,
    idempotentHint: false,
    openWorldHint: true
  }
}, async ({ userId, message }): Promise<CallToolResult> => {
  await notificationService.send(userId, message);
  return { content: [{ type: 'text', text: 'Notification sent' }] };
});

This tool is not idempotent (sending the same notification twice results in two notifications) and uses openWorldHint: true because it calls an external service the server does not control.

Common Pitfalls

  • Marking everything as readOnly: Only use readOnlyHint: true when the tool genuinely has no side effects. Incorrect annotations erode client trust.
  • Confusing destructive with non-idempotent: A tool can be non-idempotent (sending duplicate emails) without being destructive. Destructive means data is permanently lost or corrupted.
  • Omitting annotations entirely: While annotations are optional, including them improves the safety and user experience of your tools.

Best Practices

  • Always set destructiveHint: true for tools that delete data, overwrite files, or make irreversible changes.
  • Set readOnlyHint: true for query and lookup tools so clients can auto-approve them.
  • Use openWorldHint: true whenever a tool calls external APIs, sends emails, or interacts with services outside the server process.
  • Remember these are hints, not enforced guarantees. They guide client behavior but do not restrict execution.

Summary

You learned about the four tool annotation hints: readOnlyHint, destructiveHint, idempotentHint, and openWorldHint. Annotations help clients make smart decisions about tool execution, such as auto-approving safe read-only tools or requiring confirmation for destructive operations.

Code Examples

typescript
server.registerTool('list-files', {
  description: 'List files in a directory',
  inputSchema: z.object({
    path: z.string().describe('Directory path to list')
  }),
  annotations: {
    readOnlyHint: true,
    destructiveHint: false,
    idempotentHint: true,
    openWorldHint: false
  }
}, async ({ path }): Promise<CallToolResult> => {
  const files = await fs.readdir(path);
  return { content: [{ type: 'text', text: files.join('\n') }] };
});
✓ Completed