Commit 26d9ba5e4a6 for woocommerce
commit 26d9ba5e4a672b062098e3fdafc371d5a96159e9
Author: Darren Ethier <darren@roughsmootheng.in>
Date: Mon May 11 11:39:41 2026 -0400
[RSM-1334] Control deprecated Woo MCP ability exposure (#64425)
Control which WordPress abilities are exposed through the deprecated WooCommerce MCP endpoint by requiring explicit `expose_in_deprecated_woocommerce_mcp` metadata instead of namespace-based inclusion.
REST-derived WooCommerce abilities opt in automatically, custom abilities can opt in through metadata, and the existing `woocommerce_mcp_include_ability` filter remains available as the final override. Updates tests and MCP documentation to reflect the new exposure contract.
diff --git a/docs/features/mcp/README.md b/docs/features/mcp/README.md
index 2bec86721bc..35d53ec993a 100644
--- a/docs/features/mcp/README.md
+++ b/docs/features/mcp/README.md
@@ -100,7 +100,7 @@ WooCommerce Core
- Manages MCP server initialization and configuration
- Handles feature flag checking (`mcp_integration`)
-- Provides ability filtering and namespace management
+- Provides deprecated endpoint exposure filtering
**Abilities Registry** ([`AbilitiesRegistry.php`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php))
@@ -281,7 +281,26 @@ add_action( 'abilities_api_init', function() {
### Including Custom Abilities in WooCommerce MCP Server
-By default, only abilities with the `woocommerce/` namespace are included. To include abilities from other namespaces:
+REST-derived WooCommerce abilities include `expose_in_deprecated_woocommerce_mcp` metadata automatically. Custom abilities are not included by namespace alone; set this metadata to boolean `true` when registering the ability to include it in the deprecated WooCommerce MCP server by default:
+
+```php
+wp_register_ability(
+ 'your-plugin/custom-operation',
+ array(
+ 'label' => __( 'Custom Store Operation', 'your-plugin' ),
+ 'description' => __( 'Performs a custom store operation.', 'your-plugin' ),
+ 'execute_callback' => 'your_custom_ability_handler',
+ 'permission_callback' => function () {
+ return current_user_can( 'manage_woocommerce' );
+ },
+ 'meta' => array(
+ 'expose_in_deprecated_woocommerce_mcp' => true,
+ ),
+ )
+);
+```
+
+To override the default metadata decision at runtime, use the `woocommerce_mcp_include_ability` filter:
```php
add_filter( 'woocommerce_mcp_include_ability', function( $include, $ability_id ) {
@@ -322,7 +341,7 @@ The demo plugin creates a `woocommerce-demo/store-info` ability that retrieves s
## Ability Not Found
- Confirm abilities are registered during `abilities_api_init`
-- Check namespace inclusion using the `woocommerce_mcp_include_ability` filter
+- Check the ability's `expose_in_deprecated_woocommerce_mcp` metadata or override inclusion using the `woocommerce_mcp_include_ability` filter
- Verify ability callbacks are accessible
Check **WooCommerce → Status → Logs** for entries with source `woocommerce-mcp`.
diff --git a/plugins/woocommerce/src/Internal/Abilities/AbilitiesCategories.php b/plugins/woocommerce/src/Internal/Abilities/AbilitiesCategories.php
index 85a9d4002c5..495493fdbc0 100644
--- a/plugins/woocommerce/src/Internal/Abilities/AbilitiesCategories.php
+++ b/plugins/woocommerce/src/Internal/Abilities/AbilitiesCategories.php
@@ -40,12 +40,14 @@ class AbilitiesCategories {
return;
}
- wp_register_ability_category(
- 'woocommerce-rest',
- array(
- 'label' => __( 'WooCommerce REST API', 'woocommerce' ),
- 'description' => __( 'REST API operations for WooCommerce resources including products, orders, and other store data.', 'woocommerce' ),
- )
- );
+ if ( ! function_exists( 'wp_has_ability_category' ) || ! wp_has_ability_category( 'woocommerce-rest' ) ) {
+ wp_register_ability_category(
+ 'woocommerce-rest',
+ array(
+ 'label' => __( 'WooCommerce REST API', 'woocommerce' ),
+ 'description' => __( 'REST API operations for WooCommerce resources including products, orders, and other store data.', 'woocommerce' ),
+ )
+ );
+ }
}
}
diff --git a/plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php b/plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
index 17eaf8aacf9..381b5851f74 100644
--- a/plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
+++ b/plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
@@ -19,6 +19,11 @@ defined( 'ABSPATH' ) || exit;
*/
class RestAbilityFactory {
+ /**
+ * Metadata key that marks REST-derived abilities for deprecated WooCommerce MCP exposure.
+ */
+ public const EXPOSE_IN_DEPRECATED_MCP_META_KEY = 'expose_in_deprecated_woocommerce_mcp';
+
/**
* Register abilities for a REST controller based on configuration.
*
@@ -67,6 +72,7 @@ class RestAbilityFactory {
'ability_class' => RestAbility::class,
'meta' => array(
'show_in_rest' => true,
+ self::EXPOSE_IN_DEPRECATED_MCP_META_KEY => true,
),
);
diff --git a/plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php b/plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
index ee5224e52f0..71bfd01e7d9 100644
--- a/plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
+++ b/plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
@@ -9,6 +9,7 @@ namespace Automattic\WooCommerce\Internal\MCP;
use Automattic\WooCommerce\Utilities\FeaturesUtil;
use Automattic\WooCommerce\Internal\Abilities\AbilitiesRegistry;
+use Automattic\WooCommerce\Internal\Abilities\REST\RestAbilityFactory;
use Automattic\WooCommerce\Internal\MCP\Transport\WooCommerceRestTransport;
defined( 'ABSPATH' ) || exit;
@@ -153,8 +154,8 @@ class MCPAdapterProvider {
/**
* Get WooCommerce abilities for MCP server.
*
- * Filters abilities to include only those with 'woocommerce/' namespace by default,
- * with a filter to allow inclusion of abilities from other namespaces.
+ * Filters abilities to include only those explicitly exposed to the deprecated
+ * WooCommerce MCP endpoint, with a filter to override inclusion decisions.
*
* @return array Array of ability IDs for MCP server.
*/
@@ -163,12 +164,11 @@ class MCPAdapterProvider {
$abilities_registry = wc_get_container()->get( AbilitiesRegistry::class );
$all_abilities_ids = $abilities_registry->get_abilities_ids();
- // Filter abilities based on namespace and custom filter.
+ // Filter abilities based on deprecated endpoint exposure metadata and custom filter.
$mcp_abilities = array_filter(
$all_abilities_ids,
static function ( $ability_id ) {
- // Include WooCommerce abilities by default.
- $include = str_starts_with( $ability_id, 'woocommerce/' );
+ $include = self::should_include_ability_by_default( $ability_id );
// Allow filter to override inclusion decision.
/**
@@ -176,7 +176,12 @@ class MCPAdapterProvider {
*
* @since 10.3.0
*
- * @param bool $include Whether to include the ability.
+ * @param bool $include Whether to include the ability by default. True when the ability has
+ * `expose_in_deprecated_woocommerce_mcp => true` in its metadata
+ * (set automatically on REST-derived abilities by RestAbilityFactory).
+ * Migration note: this value no longer represents whether the ability uses
+ * the `woocommerce/` namespace.
+ * Return unchanged to keep the default, or return true/false to override.
* @param string $ability_id The ability ID.
*/
return apply_filters( 'woocommerce_mcp_include_ability', $include, $ability_id );
@@ -187,6 +192,34 @@ class MCPAdapterProvider {
return array_values( $mcp_abilities );
}
+ /**
+ * Check if an ability should be included in the deprecated WooCommerce MCP endpoint.
+ *
+ * REST-derived abilities can opt in to the deprecated WooCommerce MCP endpoint.
+ * Require explicit metadata so semantic/domain abilities can use WooCommerce
+ * namespaces without expanding the deprecated MCP tool list.
+ * This intentionally allows abilities from any namespace to opt in to the
+ * deprecated endpoint while keeping namespace and transport exposure separate.
+ *
+ * @param string $ability_id Ability ID.
+ * @return bool Whether to include the ability by default.
+ */
+ private static function should_include_ability_by_default( string $ability_id ): bool {
+ // Keep the pre-check to avoid triggering _doing_it_wrong() for stale or mocked registry IDs.
+ if ( function_exists( 'wp_get_ability' ) && function_exists( 'wp_has_ability' ) && wp_has_ability( $ability_id ) ) {
+ $ability = wp_get_ability( $ability_id );
+ if ( $ability ) {
+ // Strict boolean required: truthy values like 1 or "true" are intentionally excluded.
+ return true === $ability->get_meta_item(
+ RestAbilityFactory::EXPOSE_IN_DEPRECATED_MCP_META_KEY,
+ false
+ );
+ }
+ }
+
+ return false;
+ }
+
/**
* Temporarily disable MCP validation.
*
diff --git a/plugins/woocommerce/tests/php/src/Internal/Abilities/REST/RestAbilityFactoryTest.php b/plugins/woocommerce/tests/php/src/Internal/Abilities/REST/RestAbilityFactoryTest.php
index 0127134d910..0a5d66cee0e 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Abilities/REST/RestAbilityFactoryTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Abilities/REST/RestAbilityFactoryTest.php
@@ -18,6 +18,88 @@ class RestAbilityFactoryTest extends WC_Unit_Test_Case {
*/
private const VALID_JSON_SCHEMA_TYPES = array( 'string', 'number', 'integer', 'boolean', 'object', 'array', 'null' );
+ /**
+ * Ability IDs registered by these tests.
+ *
+ * @var array
+ */
+ private $registered_ability_ids = array();
+
+ /**
+ * Ability category IDs registered by these tests.
+ *
+ * @var array
+ */
+ private $registered_ability_category_ids = array();
+
+ /**
+ * Original value of $wp_actions['wp_abilities_api_init'] to restore in tearDown.
+ *
+ * @var int|null
+ */
+ private $original_wp_abilities_api_init_action_count;
+
+ /**
+ * Original value of $wp_actions['wp_abilities_api_categories_init'] to restore in tearDown.
+ *
+ * @var int|null
+ */
+ private $original_wp_abilities_api_categories_init_action_count;
+
+ /**
+ * Set up before each test.
+ */
+ public function setUp(): void {
+ global $wp_actions;
+
+ parent::setUp();
+
+ $this->original_wp_abilities_api_init_action_count = $wp_actions['wp_abilities_api_init'] ?? null;
+ $this->original_wp_abilities_api_categories_init_action_count = $wp_actions['wp_abilities_api_categories_init'] ?? null;
+
+ if ( ! function_exists( 'wp_register_ability' ) ) {
+ $abilities_bootstrap = WP_PLUGIN_DIR . '/woocommerce/vendor/wordpress/abilities-api/includes/bootstrap.php';
+ if ( file_exists( $abilities_bootstrap ) ) {
+ require_once $abilities_bootstrap;
+ }
+ }
+ }
+
+ /**
+ * Clean up after each test.
+ */
+ public function tearDown(): void {
+ global $wp_actions;
+
+ foreach ( $this->registered_ability_ids as $ability_id ) {
+ if ( function_exists( 'wp_unregister_ability' ) ) {
+ wp_unregister_ability( $ability_id );
+ }
+ }
+ $this->registered_ability_ids = array();
+
+ foreach ( $this->registered_ability_category_ids as $category_id ) {
+ if ( function_exists( 'wp_unregister_ability_category' ) ) {
+ wp_unregister_ability_category( $category_id );
+ }
+ }
+ $this->registered_ability_category_ids = array();
+
+ if ( null !== $this->original_wp_abilities_api_init_action_count ) {
+ $wp_actions['wp_abilities_api_init'] = $this->original_wp_abilities_api_init_action_count; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ } elseif ( isset( $wp_actions['wp_abilities_api_init'] ) ) {
+ unset( $wp_actions['wp_abilities_api_init'] ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ }
+
+ if ( null !== $this->original_wp_abilities_api_categories_init_action_count ) {
+ $wp_actions['wp_abilities_api_categories_init'] = $this->original_wp_abilities_api_categories_init_action_count; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ } elseif ( isset( $wp_actions['wp_abilities_api_categories_init'] ) ) {
+ unset( $wp_actions['wp_abilities_api_categories_init'] ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ }
+
+ parent::tearDown();
+ }
+
/**
* Helper to invoke the private sanitize_args_to_schema method.
*
@@ -47,6 +129,51 @@ class RestAbilityFactoryTest extends WC_Unit_Test_Case {
return $method->invoke( null, $controller, $operation );
}
+ /**
+ * Register the test ability category if the suite has not already registered it.
+ *
+ * @param string $category_id Ability category ID.
+ */
+ private function ensure_test_ability_category( string $category_id ): void {
+ if ( ! function_exists( 'wp_register_ability_category' ) || ! function_exists( 'wp_has_ability_category' ) ) {
+ return;
+ }
+
+ if ( wp_has_ability_category( $category_id ) ) {
+ return;
+ }
+
+ $category = null;
+ $callback = null;
+ $callback = function () use ( &$category, $category_id, &$callback ) {
+ remove_action( 'wp_abilities_api_categories_init', $callback );
+
+ if ( wp_has_ability_category( $category_id ) ) {
+ return;
+ }
+
+ $category = wp_register_ability_category(
+ $category_id,
+ array(
+ 'label' => 'WooCommerce REST API',
+ 'description' => 'REST API operations for WooCommerce resources.',
+ )
+ );
+ };
+
+ add_action( 'wp_abilities_api_categories_init', $callback );
+ do_action( 'wp_abilities_api_categories_init' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- Test bootstrap for Abilities API registration.
+ remove_action( 'wp_abilities_api_categories_init', $callback );
+
+ if ( null !== $category ) {
+ $this->assertNotWPError( $category, 'Test ability category should register successfully.' );
+ $this->assertNotNull( $category, 'Test ability category should register successfully.' );
+ $this->registered_ability_category_ids[] = $category_id;
+ }
+
+ $this->assertTrue( wp_has_ability_category( $category_id ), 'Test ability category should be available.' );
+ }
+
/**
* Recursively collect all 'type' values from a schema.
*
@@ -114,6 +241,47 @@ class RestAbilityFactoryTest extends WC_Unit_Test_Case {
};
}
+ /**
+ * @testdox Should mark REST-derived abilities for the deprecated WooCommerce MCP endpoint.
+ */
+ public function test_register_controller_abilities_marks_rest_abilities_for_deprecated_mcp(): void {
+ $this->assertTrue( function_exists( 'wp_register_ability' ), 'Abilities API should be available.' );
+ $this->assertTrue( class_exists( \WC_REST_Products_Controller::class ), 'Products REST controller should be available.' );
+ $this->ensure_test_ability_category( 'woocommerce-rest' );
+
+ $ability_id = 'woocommerce/rest-factory-metadata-test';
+ $config = array(
+ 'controller' => \WC_REST_Products_Controller::class,
+ 'route' => '/wc/v3/products',
+ 'abilities' => array(
+ array(
+ 'id' => $ability_id,
+ 'operation' => 'list',
+ 'label' => 'List REST factory test products',
+ 'description' => 'Retrieve REST factory test products.',
+ ),
+ ),
+ );
+
+ $callback = null;
+ $callback = static function () use ( $config, &$callback ) {
+ remove_action( 'wp_abilities_api_init', $callback );
+
+ RestAbilityFactory::register_controller_abilities( $config );
+ };
+
+ add_action( 'wp_abilities_api_init', $callback );
+ do_action( 'wp_abilities_api_init' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- Test bootstrap for Abilities API registration.
+ remove_action( 'wp_abilities_api_init', $callback );
+
+ $ability = wp_get_ability( $ability_id );
+
+ $this->assertNotNull( $ability, 'REST-derived test ability should register successfully.' );
+ $this->registered_ability_ids[] = $ability_id;
+ $this->assertTrue( $ability->get_meta_item( 'show_in_rest', false ), 'REST-derived abilities should remain exposed through the Abilities REST API.' );
+ $this->assertTrue( $ability->get_meta_item( RestAbilityFactory::EXPOSE_IN_DEPRECATED_MCP_META_KEY, false ), 'REST-derived abilities should opt in to the deprecated WooCommerce MCP endpoint.' );
+ }
+
// ── Bug 1: date-time type conversion (issue #62764) ──
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php b/plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
index 0493d0a9075..8f1a7f4cd2d 100644
--- a/plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
@@ -7,8 +7,9 @@ declare( strict_types=1 );
namespace Automattic\WooCommerce\Tests\Internal\MCP;
-use Automattic\WooCommerce\Internal\MCP\MCPAdapterProvider;
use Automattic\WooCommerce\Internal\Abilities\AbilitiesRegistry;
+use Automattic\WooCommerce\Internal\Abilities\REST\RestAbilityFactory;
+use Automattic\WooCommerce\Internal\MCP\MCPAdapterProvider;
use Automattic\WooCommerce\Utilities\FeaturesUtil;
/**
@@ -37,12 +38,45 @@ class MCPAdapterProviderTest extends \WC_Unit_Test_Case {
*/
private $original_abilities_registry;
+ /**
+ * Ability IDs registered by these tests.
+ *
+ * @var array
+ */
+ private $registered_ability_ids = array();
+
+ /**
+ * Ability category IDs registered by these tests.
+ *
+ * @var array
+ */
+ private $registered_ability_category_ids = array();
+
+ /**
+ * Original value of $wp_actions['wp_abilities_api_init'] to restore in tearDown.
+ *
+ * @var int|null
+ */
+ private $original_wp_abilities_api_init_action_count;
+
+ /**
+ * Original value of $wp_actions['wp_abilities_api_categories_init'] to restore in tearDown.
+ *
+ * @var int|null
+ */
+ private $original_wp_abilities_api_categories_init_action_count;
+
/**
* Set up before each test.
*/
public function setUp(): void {
+ global $wp_actions;
+
parent::setUp();
+ $this->original_wp_abilities_api_init_action_count = $wp_actions['wp_abilities_api_init'] ?? null;
+ $this->original_wp_abilities_api_categories_init_action_count = $wp_actions['wp_abilities_api_categories_init'] ?? null;
+
// Bootstrap the WordPress Abilities API for tests.
if ( ! function_exists( 'wp_register_ability' ) ) {
$abilities_bootstrap = WP_PLUGIN_DIR . '/woocommerce/vendor/wordpress/abilities-api/includes/bootstrap.php';
@@ -80,6 +114,8 @@ class MCPAdapterProviderTest extends \WC_Unit_Test_Case {
* Clean up after each test.
*/
public function tearDown(): void {
+ global $wp_actions;
+
// Restore original abilities registry if it was captured.
if ( $this->original_abilities_registry ) {
$container = wc_get_container();
@@ -92,6 +128,20 @@ class MCPAdapterProviderTest extends \WC_Unit_Test_Case {
remove_all_filters( 'woocommerce_mcp_allow_insecure_transport' );
remove_all_filters( 'mcp_validation_enabled' );
+ foreach ( $this->registered_ability_ids as $ability_id ) {
+ if ( function_exists( 'wp_unregister_ability' ) ) {
+ wp_unregister_ability( $ability_id );
+ }
+ }
+ $this->registered_ability_ids = array();
+
+ foreach ( $this->registered_ability_category_ids as $category_id ) {
+ if ( function_exists( 'wp_unregister_ability_category' ) ) {
+ wp_unregister_ability_category( $category_id );
+ }
+ }
+ $this->registered_ability_category_ids = array();
+
// Remove actions registered by the system under test.
remove_action( 'rest_api_init', array( $this->sut, 'maybe_initialize' ), 10 );
remove_action( 'mcp_adapter_init', array( $this->sut, 'initialize_mcp_server' ), 10 );
@@ -99,13 +149,25 @@ class MCPAdapterProviderTest extends \WC_Unit_Test_Case {
// Clean up feature flag options.
delete_option( 'woocommerce_feature_mcp_integration_enabled' );
+ if ( null !== $this->original_wp_abilities_api_init_action_count ) {
+ $wp_actions['wp_abilities_api_init'] = $this->original_wp_abilities_api_init_action_count; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ } elseif ( isset( $wp_actions['wp_abilities_api_init'] ) ) {
+ unset( $wp_actions['wp_abilities_api_init'] ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ }
+
+ if ( null !== $this->original_wp_abilities_api_categories_init_action_count ) {
+ $wp_actions['wp_abilities_api_categories_init'] = $this->original_wp_abilities_api_categories_init_action_count; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ } elseif ( isset( $wp_actions['wp_abilities_api_categories_init'] ) ) {
+ unset( $wp_actions['wp_abilities_api_categories_init'] ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ }
+
parent::tearDown();
}
/**
- * Test that maybe_initialize respects feature flag when disabled.
+ * @testdox Should not initialize when the MCP feature flag is disabled.
*/
- public function test_maybe_initialize_respects_feature_flag_disabled() {
+ public function test_maybe_initialize_respects_feature_flag_disabled(): void {
// Ensure MCP feature is disabled via option.
update_option( 'woocommerce_feature_mcp_integration_enabled', 'no' );
@@ -115,9 +177,9 @@ class MCPAdapterProviderTest extends \WC_Unit_Test_Case {
}
/**
- * Test that maybe_initialize respects feature flag when enabled.
+ * @testdox Should initialize when the MCP feature flag is enabled.
*/
- public function test_maybe_initialize_respects_feature_flag_enabled() {
+ public function test_maybe_initialize_respects_feature_flag_enabled(): void {
// Enable MCP feature via option.
update_option( 'woocommerce_feature_mcp_integration_enabled', 'yes' );
@@ -128,9 +190,9 @@ class MCPAdapterProviderTest extends \WC_Unit_Test_Case {
}
/**
- * Test that double initialization is prevented.
+ * @testdox Should prevent double initialization.
*/
- public function test_prevents_double_initialization() {
+ public function test_prevents_double_initialization(): void {
// Enable MCP feature via option.
update_option( 'woocommerce_feature_mcp_integration_enabled', 'yes' );
@@ -145,46 +207,120 @@ class MCPAdapterProviderTest extends \WC_Unit_Test_Case {
}
/**
- * Test ability filtering by namespace.
+ * @testdox Should include marked deprecated WooCommerce MCP abilities across namespaces.
*/
- public function test_get_woocommerce_mcp_abilities_filters_by_namespace() {
- // Mock abilities registry to return test abilities.
+ public function test_get_woocommerce_mcp_abilities_includes_marked_abilities_across_namespaces(): void {
+ $exposed_woocommerce_ability = 'woocommerce/test-deprecated-products-a';
+ $exposed_external_ability = 'custom-plugin/test-deprecated-orders-a';
+
+ $this->register_test_ability(
+ $exposed_woocommerce_ability,
+ array(
+ RestAbilityFactory::EXPOSE_IN_DEPRECATED_MCP_META_KEY => true,
+ )
+ );
+ $this->register_test_ability(
+ $exposed_external_ability,
+ array(
+ RestAbilityFactory::EXPOSE_IN_DEPRECATED_MCP_META_KEY => true,
+ )
+ );
+
$this->mock_abilities_registry
->method( 'get_abilities_ids' )
->willReturn(
array(
- 'woocommerce/products-list',
- 'woocommerce/orders-get',
- 'other-plugin/custom-action',
- 'another/namespace/action',
+ 'unregistered-plugin/not-available',
+ $exposed_woocommerce_ability,
+ $exposed_external_ability,
)
);
- // Use reflection to test the private method.
- $reflection = new \ReflectionClass( $this->sut );
- $method = $reflection->getMethod( 'get_woocommerce_mcp_abilities' );
- $method->setAccessible( true );
+ $result = $this->get_woocommerce_mcp_abilities();
+
+ $this->assertCount( 2, $result, 'Should only return abilities explicitly exposed in the deprecated WooCommerce MCP endpoint.' );
+ $this->assertContains( $exposed_woocommerce_ability, $result, 'Should include marked WooCommerce abilities.' );
+ $this->assertContains( $exposed_external_ability, $result, 'Should include marked abilities from other namespaces.' );
+ $this->assertSame( array( 0, 1 ), array_keys( $result ), 'Should re-index array after filtering.' );
+ }
+
+ /**
+ * @testdox Should exclude unmarked WooCommerce abilities from the deprecated MCP endpoint.
+ */
+ public function test_get_woocommerce_mcp_abilities_excludes_unmarked_woocommerce_abilities(): void {
+ $unmarked_ability = 'woocommerce/test-unmarked';
- $result = $method->invoke( $this->sut );
+ $this->register_test_ability(
+ $unmarked_ability,
+ array()
+ );
+
+ $this->mock_abilities_registry
+ ->method( 'get_abilities_ids' )
+ ->willReturn(
+ array(
+ $unmarked_ability,
+ )
+ );
- $expected = array(
- 'woocommerce/products-list',
- 'woocommerce/orders-get',
+ $result = $this->get_woocommerce_mcp_abilities();
+
+ $this->assertEmpty( $result, 'Should exclude unmarked WooCommerce abilities.' );
+ }
+
+ /**
+ * @testdox Should require strict boolean metadata for deprecated MCP endpoint exposure.
+ */
+ public function test_get_woocommerce_mcp_abilities_excludes_false_or_non_boolean_exposure_metadata(): void {
+ $opted_out_ability = 'woocommerce/test-opted-out';
+ $invalid_exposure_ability = 'woocommerce/test-invalid-exposure';
+
+ $this->register_test_ability(
+ $opted_out_ability,
+ array(
+ RestAbilityFactory::EXPOSE_IN_DEPRECATED_MCP_META_KEY => false,
+ )
);
+ $this->register_test_ability(
+ $invalid_exposure_ability,
+ array(
+ RestAbilityFactory::EXPOSE_IN_DEPRECATED_MCP_META_KEY => 'true',
+ )
+ );
+
+ $this->mock_abilities_registry
+ ->method( 'get_abilities_ids' )
+ ->willReturn(
+ array(
+ $opted_out_ability,
+ $invalid_exposure_ability,
+ )
+ );
+
+ $result = $this->get_woocommerce_mcp_abilities();
- $this->assertEquals( $expected, $result, 'Should only return woocommerce namespaced abilities' );
+ $this->assertEmpty( $result, 'Should exclude false and non-boolean exposure metadata.' );
}
/**
- * Test ability filtering with custom filter.
+ * @testdox Should respect custom filters for deprecated WooCommerce MCP ability inclusion.
*/
- public function test_get_woocommerce_mcp_abilities_respects_custom_filter() {
+ public function test_get_woocommerce_mcp_abilities_respects_custom_filter(): void {
+ $deprecated_ability = 'woocommerce/test-custom-filter-deprecated';
+
+ $this->register_test_ability(
+ $deprecated_ability,
+ array(
+ RestAbilityFactory::EXPOSE_IN_DEPRECATED_MCP_META_KEY => true,
+ )
+ );
+
// Mock abilities registry to return test abilities.
$this->mock_abilities_registry
->method( 'get_abilities_ids' )
->willReturn(
array(
- 'woocommerce/products-list',
+ $deprecated_ability,
'custom-plugin/special-action',
'other-plugin/normal-action',
)
@@ -194,6 +330,9 @@ class MCPAdapterProviderTest extends \WC_Unit_Test_Case {
add_filter(
'woocommerce_mcp_include_ability',
function ( $should_include, $ability_id ) {
+ if ( 'woocommerce/test-custom-filter-deprecated' === $ability_id ) {
+ return false;
+ }
if ( str_starts_with( $ability_id, 'custom-plugin/' ) ) {
return true;
}
@@ -203,34 +342,27 @@ class MCPAdapterProviderTest extends \WC_Unit_Test_Case {
2
);
- // Use reflection to test the private method.
- $reflection = new \ReflectionClass( $this->sut );
- $method = $reflection->getMethod( 'get_woocommerce_mcp_abilities' );
- $method->setAccessible( true );
-
- $result = $method->invoke( $this->sut );
-
- $expected = array(
- 'woocommerce/products-list',
- 'custom-plugin/special-action',
- );
+ $result = $this->get_woocommerce_mcp_abilities();
- $this->assertEquals( $expected, $result, 'Should respect custom filter for including abilities' );
+ $this->assertCount( 1, $result, 'Should only return abilities included by the custom filter.' );
+ $this->assertContains( 'custom-plugin/special-action', $result, 'Should include abilities opted in by filter.' );
+ $this->assertNotContains( $deprecated_ability, $result, 'Should exclude abilities opted out by filter.' );
+ $this->assertSame( array( 0 ), array_keys( $result ), 'Should re-index array after filter override.' );
}
/**
- * Test MCP validation disable workaround.
+ * @testdox Should disable MCP validation.
*/
- public function test_disable_mcp_validation_returns_false() {
+ public function test_disable_mcp_validation_returns_false(): void {
$result = MCPAdapterProvider::disable_mcp_validation();
$this->assertFalse( $result, 'disable_mcp_validation should always return false' );
}
/**
- * Test initialization state tracking.
+ * @testdox Should track initialization state.
*/
- public function test_is_initialized_tracks_state() {
+ public function test_is_initialized_tracks_state(): void {
$this->assertFalse( $this->sut->is_initialized(), 'Should start as not initialized' );
// Enable MCP feature via option.
@@ -241,29 +373,23 @@ class MCPAdapterProviderTest extends \WC_Unit_Test_Case {
}
/**
- * Test that abilities with empty array are handled correctly.
+ * @testdox Should handle an empty abilities array.
*/
- public function test_handles_empty_abilities_array() {
+ public function test_handles_empty_abilities_array(): void {
// Mock abilities registry to return empty array.
$this->mock_abilities_registry
->method( 'get_abilities_ids' )
->willReturn( array() );
- // Use reflection to test the private method.
- $reflection = new \ReflectionClass( $this->sut );
- $method = $reflection->getMethod( 'get_woocommerce_mcp_abilities' );
- $method->setAccessible( true );
+ $result = $this->get_woocommerce_mcp_abilities();
- $result = $method->invoke( $this->sut );
-
- $this->assertEquals( array(), $result, 'Should handle empty abilities array correctly' );
+ $this->assertEmpty( $result, 'Should handle empty abilities array correctly' );
}
/**
- * Test that non-woocommerce abilities are filtered out.
+ * @testdox Should filter out unregistered abilities.
*/
- public function test_filters_out_non_woocommerce_abilities() {
- // Mock abilities registry to return only non-woocommerce abilities.
+ public function test_filters_out_unregistered_abilities(): void {
$this->mock_abilities_registry
->method( 'get_abilities_ids' )
->willReturn(
@@ -274,48 +400,113 @@ class MCPAdapterProviderTest extends \WC_Unit_Test_Case {
)
);
- // Use reflection to test the private method.
+ $result = $this->get_woocommerce_mcp_abilities();
+
+ $this->assertEmpty( $result, 'Should filter out all unregistered abilities.' );
+ }
+
+ /**
+ * Get WooCommerce MCP abilities through the private provider method.
+ *
+ * @return array
+ */
+ private function get_woocommerce_mcp_abilities(): array {
$reflection = new \ReflectionClass( $this->sut );
$method = $reflection->getMethod( 'get_woocommerce_mcp_abilities' );
$method->setAccessible( true );
- $result = $method->invoke( $this->sut );
+ return $method->invoke( $this->sut );
+ }
+
+ /**
+ * Register a minimal ability for provider tests.
+ *
+ * @param string $ability_id Ability ID.
+ * @param array $meta Ability meta.
+ */
+ private function register_test_ability( string $ability_id, array $meta ): void {
+ $this->ensure_test_ability_category( 'woocommerce-rest' );
+
+ $ability = null;
+ $callback = null;
+ $callback = function () use ( &$ability, $ability_id, $meta, &$callback ) {
+ remove_action( 'wp_abilities_api_init', $callback );
- $this->assertEquals( array(), $result, 'Should filter out all non-woocommerce abilities' );
+ $ability = wp_register_ability(
+ $ability_id,
+ array(
+ 'label' => 'Test ability',
+ 'description' => 'Test ability.',
+ 'category' => 'woocommerce-rest',
+ 'input_schema' => array( 'type' => 'object' ),
+ 'output_schema' => array( 'type' => 'object' ),
+ 'execute_callback' => static function () {
+ return array();
+ },
+ 'permission_callback' => static function () {
+ return true;
+ },
+ 'meta' => array_merge(
+ array(
+ 'show_in_rest' => true,
+ ),
+ $meta
+ ),
+ )
+ );
+ };
+
+ add_action( 'wp_abilities_api_init', $callback );
+ do_action( 'wp_abilities_api_init' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- Test bootstrap for Abilities API registration.
+ remove_action( 'wp_abilities_api_init', $callback );
+
+ $this->assertNotWPError( $ability, 'Test ability should register successfully.' );
+ $this->assertNotNull( $ability, 'Test ability should register successfully.' );
+ $this->registered_ability_ids[] = $ability_id;
}
/**
- * Test array re-indexing after filtering.
+ * Register the test ability category if the suite has not already registered it.
+ *
+ * @param string $category_id Ability category ID.
*/
- public function test_reindexes_array_after_filtering() {
- // Mock abilities registry to return mixed abilities.
- $this->mock_abilities_registry
- ->method( 'get_abilities_ids' )
- ->willReturn(
+ private function ensure_test_ability_category( string $category_id ): void {
+ if ( ! function_exists( 'wp_register_ability_category' ) || ! function_exists( 'wp_has_ability_category' ) ) {
+ return;
+ }
+
+ if ( wp_has_ability_category( $category_id ) ) {
+ return;
+ }
+
+ $category = null;
+ $callback = null;
+ $callback = function () use ( &$category, $category_id, &$callback ) {
+ remove_action( 'wp_abilities_api_categories_init', $callback );
+
+ if ( wp_has_ability_category( $category_id ) ) {
+ return;
+ }
+
+ $category = wp_register_ability_category(
+ $category_id,
array(
- 'other-plugin/action-1',
- 'woocommerce/products-list',
- 'another-namespace/action-2',
- 'woocommerce/orders-get',
+ 'label' => 'WooCommerce REST API',
+ 'description' => 'REST API operations for WooCommerce resources.',
)
);
+ };
- // Use reflection to test the private method.
- $reflection = new \ReflectionClass( $this->sut );
- $method = $reflection->getMethod( 'get_woocommerce_mcp_abilities' );
- $method->setAccessible( true );
+ add_action( 'wp_abilities_api_categories_init', $callback );
+ do_action( 'wp_abilities_api_categories_init' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- Test bootstrap for Abilities API registration.
+ remove_action( 'wp_abilities_api_categories_init', $callback );
- $result = $method->invoke( $this->sut );
+ if ( null !== $category ) {
+ $this->assertNotWPError( $category, 'Test ability category should register successfully.' );
+ $this->assertNotNull( $category, 'Test ability category should register successfully.' );
+ $this->registered_ability_category_ids[] = $category_id;
+ }
- // Check that array is properly re-indexed (keys should be 0, 1).
- $this->assertEquals( array( 0, 1 ), array_keys( $result ), 'Should re-index array after filtering' );
- $this->assertEquals(
- array(
- 'woocommerce/products-list',
- 'woocommerce/orders-get',
- ),
- array_values( $result ),
- 'Should maintain correct values after re-indexing'
- );
+ $this->assertTrue( wp_has_ability_category( $category_id ), 'Test ability category should be available.' );
}
}