MCP tool annotations: readOnlyHint and how clients use them
MCP tool annotations are optional hints such as readOnlyHint. What each one means, its default, and how ChatGPT, VS Code, and Copilot use them.

MCP tool annotations are optional hints a server attaches to each tool to describe how it behaves: readOnlyHint, destructiveHint, idempotentHint, openWorldHint, and a display title. Clients read them to decide things such as which calls need a person's approval, but they are hints, not guarantees: the spec says to treat them as untrusted unless they come from a trusted server.
The definitions come from the MCP specification's schema reference and Tools page, version 2025-11-25. Client behavior comes from each client's own docs, and the worked example is Sume's hosted MCP server, from its code and MCP tools and gates. All were read on 2026-09-27.
What does each annotation mean?
All four hints are booleans, and each has a default that applies when the server leaves it out:
- Read by those defaults, a tool that sends no annotations is a write that may be destructive and may reach external entities.
titleis a human-readable name. For display, the spec's order is the tool'stitle, thenannotations.title, thenname.
| Annotation | When true | Default |
|---|---|---|
readOnlyHint | The tool does not modify its environment | false |
destructiveHint | The tool may perform destructive updates; false means only additive updates. Meaningful only when readOnlyHint is false | true |
idempotentHint | Repeated calls with the same arguments have no additional effect. Meaningful only when readOnlyHint is false | false |
openWorldHint | The tool may interact with an open world of external entities, such as web search; a memory tool's world is closed | true |
Where do annotations go in a tool definition?
In the annotations object of each tool the server returns from tools/list, next to name, description, and inputSchema. Below is the spec's weather example, trimmed, with annotations added: a lookup that changes nothing and calls an outside service.
{
"name": "get_weather",
"title": "Weather Information Provider",
"description": "Get current weather information for a location",
"inputSchema": {
"type": "object",
"properties": {
"location": { "type": "string", "description": "City name or zip code" }
},
"required": ["location"]
},
"annotations": {
"readOnlyHint": true,
"openWorldHint": true
}
}Are MCP tool annotations enforced?
No. The spec calls every property in ToolAnnotations a hint that is not guaranteed to describe the tool faithfully. Clients should never make tool use decisions based on annotations from untrusted servers, and clients must consider annotations untrusted unless they come from trusted servers. Mastra's docs put the risk plainly: a malicious or buggy server can claim a tool is read-only when it isn't.
How do MCP clients use readOnlyHint?
Differently. Some ask before any call not marked read-only, one drops such tools, and some leave the policy to your code:
| Client | What its docs say |
|---|---|
| ChatGPT developer mode | Write actions need confirmation by default; it respects readOnlyHint, and tools without it are treated as write actions |
| VS Code | Shows a confirmation dialog for all tools not marked with readOnlyHint; read-only tools run without one |
| GitHub Copilot code review | Uses a tool only if annotations.readOnlyHint is true; excludes it when missing or false |
| AI SDK | Exposes annotations on tool metadata but does not turn them into an approval policy automatically |
| Mastra | Passes annotations to a requireToolApproval function; relax approval with them only for servers you trust |
What does Sume's MCP server set?
In current code, every tool on Sume's hosted server at https://mcp.sume.com/mcp carries all four hints. Sume's own gates sit on top of them: write and paid tools stay hidden until an OAuth session has mcp:write, and every write or paid call needs an idempotency_key. The values the code sets today:
- Read tools such as
tools_list,jobs_status, andjobs_wait:readOnlyHint: true,idempotentHint: true,destructiveHint: false. - Write and paid tools such as
generate_videoandvideo_trim:readOnlyHint: falseandidempotentHint: false. jobs_cancelis the only tool withdestructiveHint: true. Every tool setsopenWorldHint: false.tools_listalso returns a Sumesafetyblock per tool, with fields such asread_only,paid_generation, andrequires_idempotency_key, separate from the protocol annotations; Sume MCP tools list covers it.- So ChatGPT asks before a paid Sume tool but treats a status read as read-only; How to add an MCP server to ChatGPT shows that flow.
Sources
- Model Context Protocol: Schema reference (2025-11-25) (read 2026-09-27)
- Model Context Protocol: Tools (2025-11-25) (read 2026-09-27)
- OpenAI API: ChatGPT Developer mode (read 2026-09-27)
- VS Code: MCP developer guide (read 2026-09-27)
- GitHub Docs: Configure MCP servers for your repository (read 2026-09-27)
- AI SDK: MCP tools (read 2026-09-27)
- Mastra: MCPClient reference (read 2026-09-27)
- MCP tools and gates
- MCP OAuth and API keys
Related posts
More in Developers
- Python image generation API: generate and save AI images
Generate images from Python with Requests: POST a prompt to an image API, read the URLs from a 200 or poll the 202 job, then save each file.
- Speech to text API in JavaScript: audio to text in Node.js
Call a speech to text API from JavaScript on your server: send the audio file's URL with the Sume SDK in Node.js, wait for the job, read the text.
- Speech to text in Python: transcribe audio with timestamps
Speech to text in Python with Requests: send the audio URL, poll the job, then read the transcript and word timestamps. A script for Sume STT 1.0.
- CORS error calling the Sume API from a browser: the fix
Browsers block direct calls from your site to api.sume.com, and API keys must never ship in frontend code. Call Sume from your server and proxy it.
Written by Sume