|
| 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