Skip to content

Commit 75bc24e

Browse files
committed
Add granular dynamic permission callbacks to MCP abilities
- Add `permission_callback` parameter to `add_ability()` for object-level checks - Add `check_ability_permission()` dispatcher that runs static cap then dynamic callback - Add per-tool callbacks: content, media, taxonomy, user, option, global styles, REST proxy, WP-CLI - Introduce custom capabilities `wp_forge_mcp_manage_options`, `wp_forge_mcp_read_error_log`, `wp_forge_mcp_run_wp_cli`; grant to admin role on activation and `admin_init` - Redact sensitive option values (secret, token, password, api_key, etc.) in all option responses - Block upload of disallowed file types via `wp_check_filetype_and_ext()` - Add `perm=readable` to WP_Query args for content/media search - Add runtime `read_post` guard inside `get_content_by_id()` and `get_content_by_slug()` - Block proxying the MCP transport route through the REST catalog tool - Add `docs/permissions.md` with least-privilege guidelines and capability selection rules
1 parent 2311a43 commit 75bc24e

12 files changed

Lines changed: 661 additions & 38 deletions

docs/permissions.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Ability Permission Guidelines
2+
3+
WordPress MCP abilities must use least-privilege permission checks. A static capability is acceptable only when the action does not depend on a specific object, taxonomy, user, route, or option name. When request parameters determine access, register a dynamic permission callback and also keep defensive checks near the runtime operation when practical.
4+
5+
## Registration Pattern
6+
7+
Use `add_ability()` with:
8+
9+
- A baseline capability that controls discovery and first-pass access.
10+
- A dynamic permission callback when the ability acts on a specific object or requested resource.
11+
- The narrowest WordPress capability that matches the action.
12+
13+
Examples:
14+
15+
```php
16+
$this->add_ability(
17+
self::INTERNAL_PREFIX . 'content-get',
18+
'Get Content',
19+
'Get a content item from any registered post type by ID or slug',
20+
$schema,
21+
$callback,
22+
true,
23+
'read',
24+
array(),
25+
array( $this, 'can_read_content_request' )
26+
);
27+
```
28+
29+
The dispatcher checks the static capability first and then invokes the dynamic callback with the tool input. Dynamic callbacks should return `true` or a `Response::error( ..., 403 )` payload.
30+
31+
## Capability Selection
32+
33+
- Posts and custom post types:
34+
- Read a specific item with `current_user_can( 'read_post', $post_id )`.
35+
- Create with the post type's `create_posts` capability.
36+
- Update with `current_user_can( 'edit_post', $post_id )`.
37+
- Delete with `current_user_can( 'delete_post', $post_id )`.
38+
- Publishing, private, future, or cross-author actions must also check the post type's `publish_posts`, `read_private_posts`, or `edit_others_posts` caps as appropriate.
39+
- Attachments:
40+
- Upload with `upload_files`.
41+
- Read, update, or delete a specific attachment with `read_post`, `edit_post`, or `delete_post` for that attachment ID.
42+
- Taxonomies:
43+
- Use the taxonomy object's capability map: `manage_terms`, `edit_terms`, `delete_terms`, and `assign_terms`.
44+
- Do not assume `manage_categories` for custom taxonomies.
45+
- Users:
46+
- List users with `list_users`.
47+
- Create users with `create_users`.
48+
- Edit users with `current_user_can( 'edit_user', $user_id )`.
49+
- Delete users with `current_user_can( 'delete_user', $user_id )`.
50+
- Role changes must check `promote_users` or `promote_user`.
51+
- Settings and options:
52+
- General WordPress settings use `manage_options`.
53+
- Arbitrary option management should use a plugin-specific capability and protect critical options.
54+
- Redact likely secrets from option read responses.
55+
- Plugins and themes:
56+
- Use core caps: `install_plugins`, `activate_plugins`, `delete_plugins`, `install_themes`, `switch_themes`, and `delete_themes`.
57+
- Site Health:
58+
- Use `view_site_health_checks`.
59+
- Error logs and command execution:
60+
- Use plugin-specific capabilities such as `wp_forge_mcp_read_error_log` and `wp_forge_mcp_run_wp_cli`.
61+
- Keep WP-CLI disabled by default and constrain commands with an allowlist.
62+
- REST proxy tools:
63+
- Never proxy the MCP transport route.
64+
- Let `rest_do_request()` enforce the target route's `permission_callback`.
65+
- Add route allowlists or denylists when exposing sensitive namespaces.
66+
67+
## Runtime Checks
68+
69+
Permission callbacks are the primary guard, but high-risk methods should also check permissions near the operation they perform. This is important for shared helper methods that may be reused by multiple abilities.
70+
71+
Examples:
72+
73+
- Before returning post content, check `read_post`.
74+
- Before mutating a post, check `edit_post`.
75+
- Before returning a media file body, check `read_post` for the attachment.
76+
- Before deleting anything, check the delete capability for the target object.
77+
78+
## Custom Capabilities
79+
80+
Use custom capabilities when an MCP ability is more sensitive than the closest core screen or when it should be delegated independently. Grant custom capabilities to administrators on activation, then let site owners assign them to dedicated automation roles if needed.
81+
82+
Current sensitive MCP capabilities:
83+
84+
- `wp_forge_mcp_manage_options`
85+
- `wp_forge_mcp_read_error_log`
86+
- `wp_forge_mcp_run_wp_cli`
87+
88+
## Nonces and Authentication
89+
90+
MCP ability calls run through the WordPress MCP/REST transport and must rely on authenticated WordPress users plus capability checks. Browser-admin actions still need normal WordPress nonce verification. Do not use nonces as a replacement for `current_user_can()`, and do not use capabilities as a replacement for nonce checks on browser-submitted admin forms.

0 commit comments

Comments
 (0)