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.' );
 	}
 }