Skip to content

Commit 2b4378e

Browse files
committed
Add docs index to AGENTS.md and meta-annotations guide
- Added "Current docs" index in AGENTS.md listing meta-annotations.md and permissions.md with one-line descriptions for progressive disclosure - Added docs/meta-annotations.md: guidelines for setting `readonly`, `destructive`, and `idempotent` on abilities, their MCP hint mappings, registration pattern, defaults, classification rules, and practical guidance
1 parent 75bc24e commit 2b4378e

2 files changed

Lines changed: 128 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ Do not load every file in `docs/` by default. Use progressive disclosure:
2727
- If a doc links to another doc that is directly relevant, follow that link; otherwise leave unrelated docs unread.
2828
- If the `docs/` folder does not exist, continue with the repository files and note that there are no docs to consult.
2929

30+
Current docs:
31+
32+
- [`docs/meta-annotations.md`](docs/meta-annotations.md): Guidelines for setting ability `meta.annotations` and MCP tool hints such as `readOnlyHint`, `destructiveHint`, and `idempotentHint`.
33+
- [`docs/permissions.md`](docs/permissions.md): Guidelines for least-privilege ability permissions, capability selection, dynamic permission callbacks, and runtime checks.
34+
3035
## Development Workflow
3136

3237
- Use Composer PSR-4 autoloading and keep PHP classes in `src/`.

docs/meta-annotations.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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

Comments
 (0)