Annotations & Quality

Mapping Ability Annotations to MCP Hints

⏱ 13 min

Every ability you’ve registered in this course has carried a small meta.annotations array, readonly, destructive, idempotent, without much explanation of what happens to it downstream. This lesson closes that gap: the MCP Adapter translates these WordPress-native fields into the actual MCP tool hint fields a client reads, using a real, specific mapping worth understanding precisely.

What you'll learn in this lesson
The exact annotation mapping
readonly, destructive, and idempotent, and what each becomes in MCP terms.
Why these are hints, not enforcement
What actually stops a destructive action, and what merely warns a human about one.
title and openWorldHint
The two MCP tool annotation fields that don't map from any WordPress-side property.
Setting annotations honestly
Why an inaccurate annotation is worse than none at all.
Prerequisites

Any ability with a meta.annotations array, my-plugin/create-draft-post from earlier lessons works. No new WordPress setup needed, this lesson is entirely about how existing metadata is interpreted.

Step 1: The actual mapping

The MCP Adapter’s McpAnnotationMapper class converts WordPress-format annotation properties into the field names the MCP 2025-11-25 specification’s ToolAnnotations type expects:

WordPress ability annotation to MCP tool hint
Ability meta.annotations keyMCP tool hintMeaning
readonlyreadOnlyHintIf true, calling this tool does not modify anything on the site.
destructivedestructiveHintIf true, calling this tool may cause damage or data loss that’s hard or impossible to undo.
idempotentidempotentHintIf true, calling this tool again with the same arguments has no additional effect beyond the first call.

Two more MCP tool fields exist that don’t map from any WordPress ability property at all: title, a separate human-readable label distinct from your ability’s label, and openWorldHint, which signals whether the tool interacts with a bounded, closed set of data (like your own database) versus an open-ended external system. Both of these can still be set through meta.annotations directly using their MCP field names, the mapper accepts either the WordPress-style key or the MCP-style key and normalizes whichever one is present.

Step 2: Setting them on a real ability

// File: my-mcp-abilities.php (annotations for create-draft-post)
'meta' => array(
	'annotations' => array(
		'readonly'    => false, // it creates content
		'destructive' => false, // creating a draft isn't destructive
		'idempotent'  => false, // calling it twice creates two drafts, not one
	),
	'mcp'         => array( 'public' => true ),
),

Compare that with a hypothetical delete-post ability, where the honest answer to every field flips:

// File: my-mcp-abilities.php (annotations for a delete-post ability)
'meta' => array(
	'annotations' => array(
		'readonly'    => false,
		'destructive' => true,  // deleting a post is exactly the case this exists for
		'idempotent'  => true,  // deleting an already-deleted post has no further effect
	),
	'mcp'         => array( 'public' => true ),
),

Note destructive and idempotent aren’t opposites, an action can be both destructive and idempotent at once, deleting something is a good example: harmful the first time, but calling delete again on the same already-gone item changes nothing further.

Step 3: Why these are hints, and what that word is doing

The MCP specification is explicit that these are hints, not guarantees a server enforces and not permissions a client checks before allowing a call. A well-behaved MCP client UI uses destructiveHint: true to show a confirmation dialog before a human approves the call, or to treat the tool with extra caution in an autonomous agent loop. Nothing in the protocol or the adapter stops a tool from actually being destructive regardless of what its hints claim, and nothing stops a client from ignoring the hint entirely. The actual enforcement in your system is still your permission_callback, these annotations exist purely to shape how a client presents and reasons about the call, not to replace access control.

What actually gates a destructive action, versus what merely warns about it
permission_callback
Enforced. Blocks execution outright for callers who fail the check.
input_schema validation
Enforced. Rejects malformed arguments before execute_callback runs.
destructiveHint / readOnlyHint / idempotentHint
Not enforced. A well-behaved client uses them to warn a human or adjust its own caution, that's all.

Test it

Confirm the mapped hints appear correctly in a tool listing, over either transport:

# File: terminal
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
  | wp mcp-adapter serve --user=admin --server=my-plugin-content-server

Inspect the JSON response for the annotations object on my-plugin-create-draft-post, you should see destructiveHint: false and idempotentHint: false there, translated from the destructive and idempotent keys you set in PHP.

Marking something readonly or non-destructive out of habit

It’s tempting to default every new ability’s annotations to the safest-looking values, readonly: true, destructive: false, without actually checking whether that’s true. An inaccurate annotation is arguably worse than an honest one, because it actively misleads any client or human relying on it to decide how cautious to be. If an ability writes to the database in any way, readonly must be false. If in doubt about whether an action counts as destructive, default to true, a client being slightly more cautious than strictly necessary costs little; a client being less cautious than it should be because your annotation lied to it can cost a lot.

Recap

readonly, destructive, and idempotent in an ability’s meta.annotations map directly to MCP’s readOnlyHint, destructiveHint, and idempotentHint tool annotations, alongside title and openWorldHint, which have no WordPress-side equivalent and are set directly. These are hints for client UX, not an enforcement layer, that job still belongs entirely to permission_callback and schema validation. Set them honestly, every time, since a client’s caution is only as good as the metadata it’s reading. The next module moves from correctness to operations: testing, debugging, and eventually deploying the server you’ve built.

Resources & further reading

← Running Multiple MCP Servers on One Site Testing and Debugging Your MCP Server →