Skip to content

Commit 0e0f5bc

Browse files
committed
Add granular MCP tool annotations for destructive and idempotent hints
Refactor Abilities to separate core annotations (readonly, destructive, idempotent) from MCP protocol hints (readOnlyHint, destructiveHint, idempotentHint), and expose both via a new meta_annotations field. Add an optional $annotations parameter to add_ability() so callers can override defaults per-tool. Apply destructive=true and idempotent=true to delete operations across all tool files, and idempotent=true to update/activate/save operations where appropriate. Extend tests/run.php to assert correct annotation values on both MCP tool hints and registered ability meta.
1 parent efc8053 commit 0e0f5bc

11 files changed

Lines changed: 82 additions & 27 deletions

src/Abilities.php

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,7 @@ public function register_wordpress_abilities() {
177177
return function_exists( 'current_user_can' ) ? current_user_can( $capability ) : false;
178178
},
179179
'meta' => array(
180-
'annotations' => array(
181-
'readonly' => ! empty( $ability['annotations']['readOnlyHint'] ),
182-
'destructive' => empty( $ability['annotations']['readOnlyHint'] ),
183-
'idempotent' => ! empty( $ability['annotations']['readOnlyHint'] ),
184-
),
180+
'annotations' => $ability['meta_annotations'],
185181
'show_in_rest' => true,
186182
),
187183
)
@@ -259,16 +255,57 @@ public function tool_to_ability_name( $name ) {
259255
* @param callable $callback Callback.
260256
* @param bool $read_only Whether ability is read-only.
261257
* @param string $capability Required WordPress capability.
258+
* @param array<string,bool> $annotations Core ability annotation overrides.
262259
* @return void
263260
*/
264-
private function add_ability( $name, $label, $description, $input_schema, $callback, $read_only = true, $capability = 'edit_posts' ) {
261+
private function add_ability( $name, $label, $description, $input_schema, $callback, $read_only = true, $capability = 'edit_posts', $annotations = array() ) {
262+
$core_annotations = $this->core_ability_annotations( $read_only, $annotations );
263+
265264
$this->abilities[ $name ] = array(
266-
'label' => $label,
267-
'description' => $description,
268-
'input_schema' => $input_schema,
269-
'callback' => $callback,
270-
'annotations' => array( 'readOnlyHint' => (bool) $read_only ),
271-
'capability' => $capability,
265+
'label' => $label,
266+
'description' => $description,
267+
'input_schema' => $input_schema,
268+
'callback' => $callback,
269+
'annotations' => $this->mcp_tool_annotations( $core_annotations ),
270+
'meta_annotations' => $core_annotations,
271+
'capability' => $capability,
272+
);
273+
}
274+
275+
/**
276+
* Build WordPress Abilities API annotations for a tool.
277+
*
278+
* @param bool $read_only Whether ability is read-only.
279+
* @param array<string,bool> $overrides Annotation overrides.
280+
* @return array<string,bool>
281+
*/
282+
private function core_ability_annotations( $read_only, $overrides = array() ) {
283+
$annotations = array(
284+
'readonly' => (bool) $read_only,
285+
'destructive' => false,
286+
'idempotent' => (bool) $read_only,
287+
);
288+
289+
foreach ( array( 'readonly', 'destructive', 'idempotent' ) as $key ) {
290+
if ( array_key_exists( $key, $overrides ) ) {
291+
$annotations[ $key ] = (bool) $overrides[ $key ];
292+
}
293+
}
294+
295+
return $annotations;
296+
}
297+
298+
/**
299+
* Convert core ability annotations to MCP tool hints.
300+
*
301+
* @param array<string,bool> $annotations Core ability annotations.
302+
* @return array<string,bool>
303+
*/
304+
private function mcp_tool_annotations( $annotations ) {
305+
return array(
306+
'readOnlyHint' => ! empty( $annotations['readonly'] ),
307+
'destructiveHint' => ! empty( $annotations['destructive'] ),
308+
'idempotentHint' => ! empty( $annotations['idempotent'] ),
272309
);
273310
}
274311

src/Tools/CommentManagementTools.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ private function add_comment_abilities() {
5656

5757
$this->add_ability( self::INTERNAL_PREFIX . 'comment-delete', 'Delete Comment', 'Delete a WordPress comment by ID', $id_schema, function ( $params ) {
5858
return $this->delete_comment_tool( (int) $params['id'] );
59-
}, false, 'moderate_comments' );
59+
}, false, 'moderate_comments', array( 'destructive' => true, 'idempotent' => true ) );
6060
}
6161

6262
/**

src/Tools/ContentManagementTools.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,6 @@ private function add_content_abilities() {
145145
}, false );
146146
$this->add_ability( self::INTERNAL_PREFIX . 'content-delete', 'Delete Content', 'Delete content from any registered post type', $delete_content_schema, function ( $params ) {
147147
return $this->delete_content( $params );
148-
}, false );
148+
}, false, 'edit_posts', array( 'destructive' => true, 'idempotent' => true ) );
149149
}
150150
}

src/Tools/GlobalStylesTools.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ private function add_style_abilities() {
3636
}, true, 'edit_theme_options' );
3737
$this->add_ability( self::INTERNAL_PREFIX . 'global-styles-update', 'Update Global Styles', 'Update a global styles configuration', $update_schema, function ( $params ) {
3838
return $this->update_global_styles( (int) $params['id'], $params );
39-
}, false, 'edit_theme_options' );
39+
}, false, 'edit_theme_options', array( 'idempotent' => true ) );
4040
$this->add_ability( self::INTERNAL_PREFIX . 'global-styles-active-get', 'Get Active Global Styles', 'Get the currently active global styles for the current theme', $this->schema(), function () {
4141
return $this->get_active_global_styles();
4242
}, true, 'edit_theme_options' );

src/Tools/MediaTools.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ private function add_media_abilities() {
6868
}, false, 'upload_files' );
6969
$this->add_ability( self::INTERNAL_PREFIX . 'media-update', 'Update Media', 'Update a WordPress media item', $update_schema, function ( $params ) {
7070
return $this->update_media( (int) $params['id'], $params );
71-
}, false, 'upload_files' );
71+
}, false, 'upload_files', array( 'idempotent' => true ) );
7272
$this->add_ability( self::INTERNAL_PREFIX . 'media-delete', 'Delete Media', 'Delete a WordPress media item permanently', $id_schema, function ( $params ) {
7373
return $this->delete_content_item( (int) $params['id'], 'attachment' );
74-
}, false, 'upload_files' );
74+
}, false, 'upload_files', array( 'destructive' => true, 'idempotent' => true ) );
7575
$this->add_ability( self::INTERNAL_PREFIX . 'media-search', 'Search Media', 'Search WordPress media by title, caption, or description', $list_schema, function ( $params ) {
7676
return $this->search_content_items( 'attachment', array_merge( array( 'status' => 'inherit' ), $params ) );
7777
}, true, 'upload_files' );

src/Tools/OptionManagementTools.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ private function add_option_abilities() {
5353
array( 'option_name', 'value' )
5454
), function ( $params ) {
5555
return $this->update_option_tool( $params );
56-
}, false, 'manage_options' );
56+
}, false, 'manage_options', array( 'idempotent' => true ) );
5757

5858
$this->add_ability( self::INTERNAL_PREFIX . 'option-delete', 'Delete Option', 'Delete a WordPress option by name', $option_schema, function ( $params ) {
5959
return $this->delete_option_tool( $params['option_name'] );
60-
}, false, 'manage_options' );
60+
}, false, 'manage_options', array( 'destructive' => true, 'idempotent' => true ) );
6161
}
6262

6363
/**

src/Tools/PluginManagementTools.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,15 @@ private function add_plugin_abilities() {
4444

4545
$this->add_ability( self::INTERNAL_PREFIX . 'plugin-activate', 'Activate Plugin', 'Activate an installed WordPress plugin by plugin file path', $plugin_file_schema, function ( $params ) {
4646
return $this->activate_plugin_tool( $params['plugin_file'] );
47-
}, false, 'activate_plugins' );
47+
}, false, 'activate_plugins', array( 'idempotent' => true ) );
4848

4949
$this->add_ability( self::INTERNAL_PREFIX . 'plugin-deactivate', 'Deactivate Plugin', 'Deactivate an active WordPress plugin by plugin file path', $plugin_file_schema, function ( $params ) {
5050
return $this->deactivate_plugin_tool( $params['plugin_file'] );
51-
}, false, 'activate_plugins' );
51+
}, false, 'activate_plugins', array( 'idempotent' => true ) );
5252

5353
$this->add_ability( self::INTERNAL_PREFIX . 'plugin-uninstall', 'Uninstall Plugin', 'Deactivate and delete an installed WordPress plugin by plugin file path', $plugin_file_schema, function ( $params ) {
5454
return $this->uninstall_plugin( $params['plugin_file'] );
55-
}, false, 'delete_plugins' );
55+
}, false, 'delete_plugins', array( 'destructive' => true, 'idempotent' => true ) );
5656
}
5757

5858
/**

src/Tools/SiteManagementTools.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ private function add_site_abilities() {
5959
}, false, 'edit_users' );
6060
$this->add_ability( self::INTERNAL_PREFIX . 'user-delete', 'Delete User', 'Delete a WordPress user by ID', $user_get_schema, function ( $params ) {
6161
return $this->delete_user( (int) $params['id'] );
62-
}, false, 'delete_users' );
62+
}, false, 'delete_users', array( 'destructive' => true, 'idempotent' => true ) );
6363

6464
$settings_schema = $this->schema(
6565
array(
@@ -78,7 +78,7 @@ private function add_site_abilities() {
7878
}, true, 'manage_options' );
7979
$this->add_ability( self::INTERNAL_PREFIX . 'general-settings-save', 'Save General Settings', 'Save WordPress general site settings', $settings_schema, function ( $params ) {
8080
return $this->update_general_settings( $params );
81-
}, false, 'manage_options' );
81+
}, false, 'manage_options', array( 'idempotent' => true ) );
8282
$this->add_ability( self::INTERNAL_PREFIX . 'site-info-get', 'Get Site Info', 'Get detailed site information', $this->schema(), function () {
8383
return $this->get_site_info();
8484
} );

src/Tools/TaxonomyTools.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ private function add_taxonomy_abilities() {
7979

8080
$this->add_ability( self::INTERNAL_PREFIX . 'taxonomy-term-delete', 'Delete Taxonomy Term', 'Delete a term from a registered taxonomy', $term_get_schema, function ( $params ) {
8181
return $this->delete_term( $params['taxonomy'], (int) $params['id'] );
82-
}, false, 'manage_categories' );
82+
}, false, 'manage_categories', array( 'destructive' => true, 'idempotent' => true ) );
8383
}
8484

8585
/**

src/Tools/ThemeManagementTools.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ private function add_theme_abilities() {
4545

4646
$this->add_ability( self::INTERNAL_PREFIX . 'theme-activate', 'Activate Theme', 'Activate an installed WordPress theme by stylesheet directory name', $stylesheet_schema, function ( $params ) {
4747
return $this->activate_theme_tool( $params['stylesheet'] );
48-
}, false, 'switch_themes' );
48+
}, false, 'switch_themes', array( 'idempotent' => true ) );
4949

5050
$this->add_ability( self::INTERNAL_PREFIX . 'theme-delete', 'Delete Theme', 'Delete an installed WordPress theme by stylesheet directory name', $stylesheet_schema, function ( $params ) {
5151
return $this->delete_theme_tool( $params['stylesheet'] );
52-
}, false, 'delete_themes' );
52+
}, false, 'delete_themes', array( 'destructive' => true, 'idempotent' => true ) );
5353
}
5454

5555
/**

0 commit comments

Comments
 (0)