MCP Tool Annotations: Every Hint and Its Default Value
Introduction
MCP tool annotations are optional metadata that a Model Context Protocol (MCP) server attaches to each of its tools to tell clients how the tool behaves.
Besides a display title, they're four boolean hints: readOnlyHint (the tool only reads data), destructiveHint (it may destroy data), idempotentHint (calling it twice has no extra effect) and openWorldHint (it reaches systems outside the server).
Clients use them to decide when to ask the user for confirmation and how to present each tool.
Anthropic introduced MCP in November 2024, and tool annotations arrived a few months later, in the 2025-03-26 revision of the specification. I first wrote this post in May 2025, while adding them to the Kubernetes MCP Server. Since then, the specification has gone through three more revisions (2025-06-18, 2025-11-25 and 2026-07-28), and as of October 2026 the latest one still defines the same five annotations with the same defaults. What changed is everything around them: most of the MCP clients I checked now act on them, and both Anthropic's connector directory and OpenAI's plugin review require them.
In this post, I'll walk through what tool annotations are, what each hint means and its default value, how clients use them, why you can't blindly trust them, and how to set them in your own server. The examples come from the Kubernetes MCP Server; I tested them with v0.0.67 in October 2026.
What are MCP Tool Annotations?
MCP tool annotations are metadata that you attach to your MCP tools to communicate their behavior to the MCP client. Think of them as labels that describe what your tool does and how it behaves, similar to how you might label physical tools in a workshop.
They travel with each tool in the server's tools/list response, next to its name, description and input schema.
This is the JSON a client receives for the Kubernetes MCP Server's helm_list tool (description, input schema and _meta left out):
{
"name": "helm_list",
"title": "Helm: List",
"annotations": {
"title": "Helm: List",
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": true
}
}The specification calls all of these properties hints, title included: they help client applications make informed decisions about tool usage, display appropriate user interfaces, and implement safety controls.
Note
Annotations are hints provided by the MCP server and are informational only. They do not enforce any behavior or restrictions on the tool itself. The specification says clients MUST consider them untrusted unless they come from a trusted server (more on this below).
Why are MCP Tool Annotations Important?
MCP tool annotations allow servers to express the behavior of their tools in a standardized way. By using annotations, tools become self-descriptive, which brings several benefits:
- Enhanced User Experience: A human-readable title lets clients show "Helm: List" instead of
helm_list. - Fewer, Better Confirmation Prompts: Clients can run read-only tools without asking and always ask before a destructive one, instead of prompting for every call.
- Server-Side Safety Modes: A server can use its own annotations to decide which tools to expose; the Kubernetes MCP Server's read-only mode is built on them.
- Directory Requirements: As of October 2026, Anthropic's Connectors Directory and OpenAI's plugin review both require annotations before they list a server.
- Consistency Across Tools: Annotations provide a consistent way to describe tool behavior, making it easier for developers to understand and use different tools across various MCP servers.
MCP Tool Annotations and Their Default Values
The MCP specification defines five annotations, all optional:
| Annotation | Default | Meaning |
|---|---|---|
| title | none | A human-readable name for the tool |
| readOnlyHint | false | The tool only reads, never modifies its environment |
| destructiveHint | true | The tool may delete or overwrite data (if not read-only) |
| idempotentHint | false | Repeating the call has no extra effect (if not read-only) |
| openWorldHint | true | The tool reaches external systems, not a closed domain |
The defaults assume the worst: per the specification, a tool without annotations may modify its environment, may destroy data, is not safe to retry, and talks to the outside world.
For a client that follows the defaults, leaving out a hint never makes a tool look safer than it is; it only makes the client ask for confirmation more often than needed.
Not every client does: VS Code, for example, only asks you to review an open-world tool's result when openWorldHint is explicitly true.
readOnlyHint (Read-Only Hint)
Default false.
Set it to true when the tool doesn't modify its environment, like helm_list.
It's the hint clients act on most: a read-only tool can run without a confirmation prompt.
destructiveHint (Destructive Hint)
Default true, and only meaningful when readOnlyHint is false.
true means the tool may perform destructive updates, like deleting a pod or uninstalling a Helm release; false means it only performs additive ones, like running a new pod or installing a release.
Clients that read it ask before running a destructive tool.
idempotentHint (Idempotent Hint)
Default false, and only meaningful when readOnlyHint is false.
true means calling the tool again with the same arguments has no additional effect, so a retry is safe: uninstalling the same Helm release twice leaves the cluster as the first call did.
openWorldHint (Open-World Hint)
Default true.
true means the tool may interact with an open world of external entities, like a web search or a Kubernetes cluster; false means it works on a closed domain, like a local memory store.
The title Annotation and the Top-Level title
The title annotation has a sibling.
Since the 2025-06-18 revision, a tool can also carry a top-level title field, and the specification sets the display-name order: title, then annotations.title, then name.
That's why the Kubernetes MCP Server sends the same title in both places: clients that predate the top-level field only know annotations.title, and whichever one a newer client checks first, it shows the same name.
How MCP Clients Use Tool Annotations
When I first wrote this post, annotations were mostly a promise.
As of October 2026, readOnlyHint is the annotation that matters: most clients use it to skip the confirmation prompt, and some, like Codex CLI and the Claude Agent SDK, to run read-only tool calls in parallel.
| Client | Annotations | What it does |
|---|---|---|
| VS Code (Copilot) | title, readOnlyHint, openWorldHint | Runs read-only tools without asking, asks for the rest, and can ask you to review open-world results. |
| Claude | title, readOnlyHint, destructiveHint | Read-only tools can skip the prompt; destructive ones always ask. |
| ChatGPT | readOnlyHint, destructiveHint, openWorldHint | Treats a tool without readOnlyHint as a write action that needs confirmation. |
| Codex CLI | readOnlyHint, destructiveHint, openWorldHint | By default, never asks for read-only tools and always for destructive ones; runs read-only calls in parallel. |
| Gemini CLI | readOnlyHint; any, through policies | In Plan Mode, lets you approve read-only tools and blocks the rest; its policies can allow, ask or deny by annotation. |
| Goose | readOnlyHint | In Smart Approve mode, runs read-only tools without asking, always asks for readOnlyHint: false, and lets the model judge tools without the hint. |
No client I checked acts on idempotentHint by default (a Gemini CLI policy you write yourself could match it), and Cursor, Windsurf and Zed don't document any use of annotations.
Set it anyway: it documents whether a retry is safe, and it costs one line.
Can You Trust MCP Tool Annotations?
Not blindly.
The specification is explicit: "clients MUST consider tool annotations to be untrusted unless they come from trusted servers".
A malicious or buggy server can mark a tool that deletes your data as readOnlyHint: true, and nothing in the protocol stops it.
In practice, the trust decision is the moment you install or enable a server.
Clients use annotations to relax their prompts for servers you chose to trust, and your own permission rules take precedence over the hints.
Real guarantees come from deterministic controls: the permissions of the credentials the server runs with, sandboxing, or network policies.
For a Kubernetes MCP server, that's the RBAC of the service account or user in its kubeconfig.
Read-only doesn't mean harmless either: a read-only tool can still put Secrets, or text an attacker controls, like a pod's logs, in front of the model.
The Kubernetes MCP Server's resources_get and resources_list read any kind, Secrets included, unless you deny it in its denied_resources setting.
The official MCP blog covered this in depth in Tool Annotations as Risk Vocabulary (March 2026). As of October 2026, new hints for sensitive data and untrusted output are proposed in SEP-1913, still a draft, and a Tool Annotations Interest Group is reviewing it and other proposals; none has been accepted.
Real-World Example: Kubernetes MCP Server
Let's look at how these annotations are applied in a real-world scenario using the Kubernetes MCP Server as an example.
The Kubernetes MCP Server provides a set of tools to interact with Kubernetes and OpenShift clusters, and every one of them declares its annotations.
This is the definition of the helm_list tool in v0.0.67 (input schema and RBAC requirements trimmed).
The server declares its tools with its own api.Tool type, which it converts to the official MCP Go SDK's mcp.Tool, and ptr.To comes from k8s.io/utils/ptr:
{Tool: api.Tool{
Name: "helm_list",
Description: "List all the Helm releases in the current or provided namespace (or in all namespaces if specified)",
InputSchema: &jsonschema.Schema{ /* namespace, all_namespaces */ },
Annotations: api.ToolAnnotations{
Title: "Helm: List",
ReadOnlyHint: ptr.To(true),
DestructiveHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: helmList},In this example, the tool helm_list is defined with several annotations:
- Title: "Helm: List" provides a clear and concise name for the tool to be presented to end users.
- Read-Only Hint:
trueindicates that this tool only reads data from the Kubernetes cluster without making any changes. - Destructive Hint: Marked as
falsesince this function lists Helm releases without modifying anything. Strictly, the specification only considers it for tools that are not read-only, but setting it makes the intent explicit. - Open World Hint:
trueindicates that this tool interacts with an external system (the Kubernetes cluster), which is important for understanding its scope and potential dependencies. That's a judgement call: OpenAI's plugin reference, for example, says a private workspace isn't open-world just because it's hosted externally.
The idempotentHint: false in the JSON above comes from the Go SDK, which always sends readOnlyHint and idempotentHint; for a read-only tool, clients ignore it.
Compare it with helm_uninstall, which declares DestructiveHint: ptr.To(true) and IdempotentHint: ptr.To(true): it deletes a release, and uninstalling the same release twice has no additional effect.
For more examples, check the toolsets of the Kubernetes MCP Server, where every tool declares its annotations.
Read-Only Mode and Destructive Tools
The Kubernetes MCP Server also uses its own annotations to decide which tools to expose. Two flags filter the tool list before any client sees it:
--read-onlyexposes only the tools annotated withreadOnlyHint: true.--disable-destructivehides the tools annotated withdestructiveHint: true.
npx -y kubernetes-mcp-server@latest --read-only --toolsets corenpx comes with Node.js, and the server uses your kubeconfig.
If it has more than one context, every tool gets a context argument, so the agent can reach all of them: give the server a kubeconfig with a single context (--kubeconfig), or add --disable-multi-cluster to keep it on the current one.
Like most local MCP servers, it talks over stdio, so you don't run it in a terminal: you add this command to your MCP client's configuration (on native Windows, some clients need cmd /c npx instead of npx).
I checked what each flag does with v0.0.67 and the core, config and helm toolsets enabled (--toolsets core,config,helm; Helm isn't enabled by default).
The exact number of tools depends on your cluster and configuration, but what each flag leaves out doesn't:
| Flags | Tools exposed | What's left out |
|---|---|---|
| none | 23 | nothing |
--disable-destructive | 17 | the six destructive tools, such as pods_delete, resources_delete and helm_uninstall |
--read-only | 15 | every tool that changes the cluster, including the additive ones: pods_run and helm_install |
Unlike a client trusting a remote server's hints, this is the server enforcing its own annotations, so a read-only session can't call a tool that writes.
It isn't a sandbox, though.
The config toolset, enabled by default, includes configuration_view, a read-only tool that returns your kubeconfig, credentials included.
An agent that can also run shell commands can pass them to kubectl and write anyway (#1415).
For a read-only session, leave that toolset out, as the command above does, and give the server a kubeconfig whose user only has the permissions you want the agent to have.
For finer control, the server's confirmation rules can ask the user before any tool with destructiveHint: true runs.
They ask through MCP elicitation, so with a client that doesn't support it, the call proceeds unless you set confirmation_fallback = "deny".
Setting Tool Annotations in Go, TypeScript, Python and Java
Every official MCP SDK can set annotations.
These snippets declare the same annotations as helm_list above, title included, each with the SDK's latest release as of October 2026.
The Go SDK also sends the default idempotentHint: false, which the others leave out; to a client, both mean the same.
With the official Go SDK (v1.8.0), DestructiveHint and OpenWorldHint are pointers, because their default is true and the SDK needs to tell "unset" from false.
Here, server is an *mcp.Server and helmList a typed tool handler:
destructive, openWorld := false, true
mcp.AddTool(server, &mcp.Tool{
Name: "helm_list",
Title: "Helm: List",
Description: "List all the Helm releases",
Annotations: &mcp.ToolAnnotations{
Title: "Helm: List",
ReadOnlyHint: true,
DestructiveHint: &destructive,
OpenWorldHint: &openWorld,
},
}, helmList)With the TypeScript SDK (v2.3.1), annotations go in the tool's configuration:
import { McpServer } from '@modelcontextprotocol/server';
const server = new McpServer({ name: 'kubernetes', version: '1.0.0' });
server.registerTool('helm_list', {
title: 'Helm: List',
description: 'List all the Helm releases',
annotations: { title: 'Helm: List', readOnlyHint: true, destructiveHint: false, openWorldHint: true }
}, helmList);With the Python SDK (v2.3.0), they're an argument of the tool decorator, with snake_case field names:
from mcp.server import MCPServer
from mcp.types import ToolAnnotations
mcp = MCPServer("kubernetes")
@mcp.tool(title="Helm: List",
annotations=ToolAnnotations(title="Helm: List", read_only_hint=True,
destructive_hint=False, open_world_hint=True))
def helm_list(namespace: str | None = None) -> str: ...On v1, the fields are camelCase (readOnlyHint=True): v1 accepts the snake_case names without an error but sends them as unknown keys, so clients see no hints.
With the Java SDK (v2.0.1), the tool builder from io.modelcontextprotocol.spec.McpSchema takes a ToolAnnotations instance:
var tool = McpSchema.Tool.builder("helm_list", Map.of("type", "object"))
.title("Helm: List")
.annotations(McpSchema.ToolAnnotations.builder().title("Helm: List")
.readOnlyHint(true).destructiveHint(false).openWorldHint(true).build())
.build();Best Practices for MCP Tool Annotations
When implementing tool annotations, consider the following guidelines:
- Be Accurate: Ensure that the annotations truthfully reflect the tool's behavior. Misleading annotations can lead to poor user experiences or safety issues, and connector and plugin directories reject servers whose annotations don't match their tools.
- Set the Hints Explicitly: Don't rely on the defaults. A spec-following client assumes the worst for a missing hint, but not every client or server handles a missing value the same way.
- Split Tools Whose Arguments Change Their Behavior: A tool that reads or writes depending on an argument can't be read-only. Two tools, one read-only and one not, let clients skip the prompt for the safe one.
- Think about Users: Use clear and descriptive titles that help users (as opposed to developers) understand the tool's purpose.
Set the top-level
title, and keepannotations.titlefor older clients. - Consider Safety Implications: Be conservative with destructive and read-only hints. When in doubt, err on the side of caution.
- Keep Annotations Up to Date: As tools evolve, ensure that their annotations are updated to reflect any changes in behavior or functionality.
The Kubernetes MCP Server keeps a snapshot of its
tools/listoutput in its tests, so any annotation change shows up in code review. - Use Annotations Consistently: Apply annotations uniformly across all tools in your MCP server to maintain a consistent user experience.
Conclusion
MCP tool annotations are a small part of the specification that most of the major MCP clients now rely on to decide when to ask before running a tool. Accurate annotations mean fewer confirmation prompts for safe tools, and a prompt before destructive ones in the clients that act on them.
In my Kubernetes MCP Server, annotations help users understand the difference between safe query operations and potentially destructive cluster modifications. They also let operators run the server in read-only mode, or without destructive tools, with a single flag.
As you build your own MCP servers, remember that annotations are about communication: they help bridge the gap between what your tool does technically and what users need to know to use it effectively. Just don't mistake them for a security boundary.
You Might Also Like
- Introduction to the Model Context Protocol (MCP): The Future of AI Integration
- Giving Superpowers to Small Language Models with Model Context Protocol (MCP)
- Goose AI Agent Tutorial: CLI, MCP, Ollama and Recipes
- Kubernetes MCP Server Joins the Containers Organization!
- Agent Client Protocol (ACP): The LSP for AI Coding Agents Explained
