|
| 1 | +# Ability Meta Annotation Guidelines |
| 2 | + |
| 3 | +WordPress MCP abilities should set `meta.annotations` so clients can understand whether a tool reads data, mutates state, performs destructive work, or can be safely repeated. |
| 4 | + |
| 5 | +The plugin stores annotations in WordPress Abilities API format and maps them to MCP tool hints automatically. |
| 6 | + |
| 7 | +## Annotation Fields |
| 8 | + |
| 9 | +Use these WordPress ability annotation keys: |
| 10 | + |
| 11 | +| Annotation | Meaning | |
| 12 | +| --- | --- | |
| 13 | +| `readonly` | The ability does not modify the site, filesystem, database, external services, or runtime state. | |
| 14 | +| `destructive` | The ability may delete, uninstall, overwrite, deactivate, or otherwise remove or damage an existing resource. | |
| 15 | +| `idempotent` | Calling the ability repeatedly with the same arguments has no additional effect after the first successful call. | |
| 16 | + |
| 17 | +These map to MCP tool annotations as follows: |
| 18 | + |
| 19 | +| WordPress ability annotation | MCP tool hint | |
| 20 | +| --- | --- | |
| 21 | +| `readonly` | `readOnlyHint` | |
| 22 | +| `destructive` | `destructiveHint` | |
| 23 | +| `idempotent` | `idempotentHint` | |
| 24 | + |
| 25 | +## Registration Pattern |
| 26 | + |
| 27 | +Use the `$annotations` argument on `add_ability()` when the defaults are not specific enough. |
| 28 | + |
| 29 | +```php |
| 30 | +$this->add_ability( |
| 31 | + self::INTERNAL_PREFIX . 'option-save', |
| 32 | + 'Save Option', |
| 33 | + 'Create or update a WordPress option value by name', |
| 34 | + $schema, |
| 35 | + $callback, |
| 36 | + false, |
| 37 | + 'manage_options', |
| 38 | + array( 'idempotent' => true ) |
| 39 | +); |
| 40 | +``` |
| 41 | + |
| 42 | +Current signature: |
| 43 | + |
| 44 | +```php |
| 45 | +add_ability( |
| 46 | + $name, |
| 47 | + $label, |
| 48 | + $description, |
| 49 | + $input_schema, |
| 50 | + $callback, |
| 51 | + $read_only = true, |
| 52 | + $capability = 'edit_posts', |
| 53 | + $annotations = array(), |
| 54 | + $permission_callback = null |
| 55 | +) |
| 56 | +``` |
| 57 | + |
| 58 | +## Defaults |
| 59 | + |
| 60 | +`add_ability()` derives safe defaults from `$read_only`: |
| 61 | + |
| 62 | +| `$read_only` | `readonly` | `destructive` | `idempotent` | |
| 63 | +| --- | --- | --- | --- | |
| 64 | +| `true` | `true` | `false` | `true` | |
| 65 | +| `false` | `false` | `false` | `false` | |
| 66 | + |
| 67 | +Override only the fields that differ from these defaults. |
| 68 | + |
| 69 | +## Classification Rules |
| 70 | + |
| 71 | +Mark an ability as `readonly: true` when it only reads or computes data. |
| 72 | + |
| 73 | +Examples: |
| 74 | + |
| 75 | +- List, search, and get tools. |
| 76 | +- Site info and Site Health reads. |
| 77 | +- Error log reads, assuming they only read the log file. |
| 78 | +- REST route discovery and schema/detail tools. |
| 79 | + |
| 80 | +Mark an ability as `destructive: true` when it can remove or materially damage an existing resource. |
| 81 | + |
| 82 | +Examples: |
| 83 | + |
| 84 | +- `content-delete` |
| 85 | +- `taxonomy-term-delete` |
| 86 | +- `media-delete` |
| 87 | +- `user-delete` |
| 88 | +- `option-delete` |
| 89 | +- `comment-delete` |
| 90 | +- `plugin-uninstall` |
| 91 | +- `theme-delete` |
| 92 | + |
| 93 | +Mark an ability as `idempotent: true` when repeating the same call should converge to the same final state. |
| 94 | + |
| 95 | +Examples: |
| 96 | + |
| 97 | +- Updating a specific media item with the same fields. |
| 98 | +- Saving a specific option to the same value. |
| 99 | +- Saving general settings to the same values. |
| 100 | +- Updating a specific global styles record. |
| 101 | +- Activating an already-active plugin or theme. |
| 102 | +- Deactivating an already-inactive plugin. |
| 103 | +- Deleting a resource that is already deleted, when the intended end state is "absent". |
| 104 | + |
| 105 | +Leave `idempotent` as `false` for create-or-update tools when the same arguments can create another resource, depend on omitted IDs, generate timestamps, trigger side effects, or run arbitrary commands. |
| 106 | + |
| 107 | +Examples: |
| 108 | + |
| 109 | +- Content save when `id` is omitted. |
| 110 | +- Term save when `id` is omitted. |
| 111 | +- Media upload. |
| 112 | +- Plugin or theme install. |
| 113 | +- Generic REST execution. |
| 114 | +- WP-CLI command execution. |
| 115 | + |
| 116 | +## Practical Guidance |
| 117 | + |
| 118 | +- Do not mark a write tool as destructive just because it writes. Use `destructive` for removal or high-risk state changes. |
| 119 | +- Do not mark arbitrary command or REST runners as idempotent. Their behavior depends on the command or target route. |
| 120 | +- Prefer precise annotations over broad categories. For example, `plugin-activate` is writable and idempotent, but not destructive. |
| 121 | +- Keep annotations aligned with actual behavior. If a tool changes from update-only to create-or-update, revisit `idempotent`. |
| 122 | +- Treat annotations as client guidance, not authorization. Permissions must still be enforced with capabilities and dynamic permission callbacks. |
| 123 | + |
0 commit comments