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.
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:
| Ability meta.annotations key | MCP tool hint | Meaning |
|---|---|---|
readonly | readOnlyHint | If true, calling this tool does not modify anything on the site. |
destructive | destructiveHint | If true, calling this tool may cause damage or data loss that’s hard or impossible to undo. |
idempotent | idempotentHint | If 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.
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.
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
- Model Context Protocol specification
- MCP Adapter repository, GitHub
- Abilities API reference, developer.wordpress.org