Skip to content

Conversation

@budzanowski
Copy link
Contributor

@budzanowski budzanowski commented Sep 12, 2025

Add Model Context Protocol (MCP) integration for WooCommerce

This PR adds native support for the Model Context Protocol (MCP), enabling AI assistants like Claude Desktop to interact directly with WooCommerce stores through a standardized protocol. MCP integration exposes core store operations (products, orders) as discoverable tools that AI clients can use with proper authentication and permissions.

Key changes: Integrates WordPress Abilities API with WooCommerce REST endpoints, adds MCP server with custom WooCommerce transport layer, implements secure API key authentication, and includes feature flag configuration (mcp_integration). The implementation follows a flexible architecture where abilities can be extended beyond REST bridging.

How to test:

  • Enable the WooCommerce MCP Experimental feature
image
  • Create WooCommerce REST API keys with appropriate permissions ( read or write - different keys for different scenarios )
image
  • Connect using Claude Code: claude mcp add woocommerce_mcp --env WP_API_URL=https://yoursite.com/wp-json/woocommerce/mcp --env CUSTOM_HEADERS='{"X-MCP-API-Key": "key:secret"}' -- npx -y @automattic/mcp-wordpress-remote@latest

  • Or configure Claude Desktop with the MCP server details ( check the README.md from this PR )

  • Test product/order operations through the MCP interface, some examples

    • using woocommerce_mcp check order id 56
    • using woocommerce_mcp add new product with "title", "description", and "price"
    • using woocommerce_mcp remove product id 60
image
  • create a read only key, redo the connection
  • using that key try to create a product
  • this should fail

Important notes:

Documentation: Complete technical documentation, setup instructions, and security considerations are included in docs/features/mcp/README.md.

@budzanowski budzanowski requested a review from a team as a code owner September 12, 2025 23:10
@budzanowski budzanowski requested review from prettyboymp and removed request for a team September 12, 2025 23:10
@github-actions github-actions bot added focus: monorepo infrastructure Issues and PRs related to monorepo tooling. plugin: woocommerce Issues related to the WooCommerce Core plugin. labels Sep 12, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Sep 12, 2025

Testing Guidelines

Hi @prettyboymp @peterfabian @woocommerce/developer-advocacy,

Apart from reviewing the code changes, please make sure to review the testing instructions (Guide) and verify that relevant tests (E2E, Unit, Integration, etc.) have been added or updated as needed.

Reminder: PR reviewers are required to document testing performed. This includes:

  • 🖼️ Screenshots or screen recordings.
  • 📝 List of functionality tested / steps followed.
  • 🌐 Site details (environment attributes such as hosting type, plugins, theme, store size, store age, and relevant settings).
  • 🔍 Any analysis performed, such as assessing potential impacts on environment attributes and other plugins, conducting performance profiling, or using LLM/AI-based analysis.

⚠️ Within the testing details you provide, please ensure that no sensitive information (such as API keys, passwords, user data, etc.) is included in this public issue.

@github-actions
Copy link
Contributor

github-actions bot commented Sep 12, 2025

Test using WordPress Playground

The changes in this pull request can be previewed and tested using a WordPress Playground instance.
WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Test this pull request with WordPress Playground.

Note that this URL is valid for 30 days from when this comment was last updated. You can update it by closing/reopening the PR or pushing a new commit.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 12, 2025

📝 Walkthrough

Walkthrough

Adds Model Context Protocol (MCP) integration: composer dependency for wordpress/mcp-adapter; DI wiring for AbilitiesRegistry and MCPAdapterProvider; new MCP provider, REST transport, Abilities REST bridge, RestAbilityFactory/RestAbility classes; mcp_integration feature flag; docs, changelog, and unit tests.

Changes

Cohort / File(s) Summary of Changes
Dependencies
plugins/woocommerce/composer.json
Adds VCS repo https://github.com/WordPress/mcp-adapter.git and runtime dependency wordpress/mcp-adapter: dev-trunk#7a2d22cff9....
DI wiring / init
plugins/woocommerce/includes/class-woocommerce.php
DI-resolves AbilitiesRegistry and MCPAdapterProvider during init_hooks() so services are instantiated on init.
Feature flag
plugins/woocommerce/src/Internal/Features/FeaturesController.php
Adds legacy feature mcp_integration and new private get_mcp_integration_description() producing base description plus conditional permalink warning and docs link.
Abilities registry
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php
New AbilitiesRegistry class; calls AbilitiesRestBridge::init() and exposes get_abilities_ids() that returns wp_get_abilities() IDs or an empty array if unavailable.
Abilities → REST bridge
plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php
New AbilitiesRestBridge with controller configs (Products, Orders); registers abilities on abilities_api_init and gates registration to MCP context via MCPAdapterProvider::is_mcp_request(). init() now final public static.
REST ability classes & factory
plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php, .../RestAbilityFactory.php
Adds RestAbility (skips output validation) and RestAbilityFactory to derive JSON schemas from REST controllers, register abilities via wp_register_ability, and execute operations by dispatching REST requests.
MCP adapter/provider
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
New MCPAdapterProvider that conditionally initializes MCP adapter when mcp_integration enabled, registers MCP server on adapter-ready event, temporarily disables MCP validation during server creation, collects abilities from AbilitiesRegistry, and exposes helpers (is_initialized, is_mcp_request, disable_mcp_validation).
MCP REST transport
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
New transport authenticating via X-MCP-API-Key: consumer_key:consumer_secret, enforces TLS by default (filterable), authenticates against woocommerce_api_keys, sets WP current user and permissions, and enforces read/write permissions per HTTP method.
Tests
plugins/woocommerce/tests/php/src/Internal/MCP/*Test.php
Adds PHPUnit tests for MCPAdapterProvider and WooCommerceRestTransport covering feature gating, initialization, ability filtering, API-key auth, TLS handling, permission checks, and state cleanup.
Docs & changelog
plugins/woocommerce/changelog/add-mcp-to-woocommerce, docs/features/mcp/*
Adds changelog entry and developer-preview documentation (README.md, _category_.json) describing MCP integration, auth, architecture, usage, and troubleshooting.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant WP as WordPress
  participant WC as WooCommerce (class-woocommerce)
  participant DI as DI Container
  participant AbReg as AbilitiesRegistry
  participant MCPProv as MCPAdapterProvider

  WP->>WC: init_hooks()
  WC->>DI: get(AbilitiesRegistry)
  WC->>DI: get(MCPAdapterProvider)
  Note right of MCPProv #DDEBF7: registers rest_api_init hooks for adapter/server init
  WP->>MCPProv: rest_api_init -> maybe_initialize()
  MCPProv->>AbReg: get_abilities_ids()
  MCPProv->>WP: load MCP adapter & register MCP server (if feature enabled)
Loading
sequenceDiagram
  autonumber
  participant Client as MCP Client
  participant MCP as WooCommerce MCP Server
  participant Transport as WooCommerceRestTransport
  participant WP as WordPress REST
  participant Ctrl as WC REST Controller

  Client->>MCP: Request (includes X-MCP-API-Key)
  MCP->>Transport: validate_request()
  Transport->>WP: DB lookup/authenticate API key
  WP-->>Transport: wp_set_current_user + permissions
  Transport->>WP: rest_do_request(...)
  WP->>Ctrl: dispatch controller
  Ctrl-->>WP: response
  WP-->>MCP: response forwarded
  MCP-->>Client: result
Loading
sequenceDiagram
  autonumber
  participant WP as WordPress
  participant Bridge as AbilitiesRestBridge
  participant Factory as RestAbilityFactory
  participant Ctrl as WC REST Controller

  WP->>Bridge: abilities_api_init
  alt MCP context (MCPAdapterProvider::is_mcp_request)
    Bridge->>Factory: register_controller_abilities(config)
    Factory->>Ctrl: instantiate & introspect schemas/params
    Factory->>WP: wp_register_ability(... ability_class=RestAbility)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title concisely and accurately describes the primary purpose of the changeset, namely adding support for the Model Context Protocol integration in WooCommerce, without extraneous details or file references. It clearly communicates the main functionality being introduced and aligns with the content and objectives of the pull request.
Description Check ✅ Passed The description directly outlines the MCP integration feature, summarizes key architectural changes, lists testing steps, and notes documentation, all of which align with the detailed file modifications and objectives of the pull request. It provides context for reviewers on how to validate the feature and highlights any important caveats, demonstrating relevance and coherence with the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add/mcp-to-woocommerce

📜 Recent review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f399ede and 6a85740.

📒 Files selected for processing (1)
  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
plugins/woocommerce/tests/**/*.php

📄 CodeRabbit inference engine (.cursor/rules/woo-phpunit.mdc)

plugins/woocommerce/tests/**/*.php: Ensure test classes define public setUp() and tearDown() methods (not protected)
All PHPUnit test methods must be public, not protected
Add declare(strict_types=1); at the top of each test file
Test classes should extend WC_Unit_Test_Case

Files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
plugins/woocommerce/tests/php/src/**/*.php

📄 CodeRabbit inference engine (plugins/woocommerce/CLAUDE.md)

Place PHP unit tests under tests/php/src/

Files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
plugins/woocommerce/tests/php/**

📄 CodeRabbit inference engine (plugins/woocommerce/CLAUDE.md)

Store PHP test data and fixtures under tests/php/

Files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
🧠 Learnings (5)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.140Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
📚 Learning: 2025-08-18T06:11:48.768Z
Learnt from: CR
PR: woocommerce/woocommerce#0
File: .cursor/rules/woo-phpunit.mdc:0-0
Timestamp: 2025-08-18T06:11:48.768Z
Learning: Applies to plugins/woocommerce/tests/**/*.php : Ensure test classes define public setUp() and tearDown() methods (not protected)

Applied to files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
📚 Learning: 2025-09-11T07:18:56.840Z
Learnt from: CR
PR: woocommerce/woocommerce#0
File: plugins/woocommerce/CLAUDE.md:0-0
Timestamp: 2025-09-11T07:18:56.840Z
Learning: Applies to plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/**/*.php : Place unit tests for payment extension suggestions in tests/php/src/Internal/Admin/Suggestions/

Applied to files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
📚 Learning: 2025-09-03T11:50:02.208Z
Learnt from: tpaksu
PR: woocommerce/woocommerce#60735
File: plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Fulfillments/class-wc-rest-fulfillments-v4-controller-tests.php:10-10
Timestamp: 2025-09-03T11:50:02.208Z
Learning: For REST API controller tests in plugins/woocommerce/tests/**/*.php, test classes should extend WC_REST_Unit_Test_Case instead of WC_Unit_Test_Case because REST routes need to be registered before tests run.

Applied to files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
📚 Learning: 2025-09-03T16:06:52.193Z
Learnt from: prettyboymp
PR: woocommerce/woocommerce#60684
File: plugins/woocommerce/src/Admin/API/Init.php:115-119
Timestamp: 2025-09-03T16:06:52.193Z
Learning: In WooCommerce Admin API Init class, checking for already-instantiated controllers (isset($this->$controller)) to prevent duplicate registration breaks tests because the singleton pattern makes it difficult to reset class instance state between tests. The did_filter() approach is preferred as it addresses the root cause (duplicate filter applications) rather than the symptom (duplicate registrations) and maintains better test isolation.

Applied to files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
🧬 Code graph analysis (1)
plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php (5)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (4)
  • MCPAdapterProvider (22-230)
  • maybe_initialize (60-74)
  • is_initialized (206-208)
  • disable_mcp_validation (197-199)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (1)
  • AbilitiesRegistry (18-49)
plugins/woocommerce/src/Utilities/FeaturesUtil.php (1)
  • FeaturesUtil (14-102)
plugins/woocommerce/tests/php/src/Internal/MCP/WooCommerceRestTransportTest.php (2)
  • setUp (27-45)
  • tearDown (50-58)
plugins/woocommerce/woocommerce.php (1)
  • wc_get_container (57-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (26)
  • GitHub Check: Blocks e2e tests 9/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 8/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 7/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 10/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 5/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 6/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 2/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 3/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 4/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest (HPOS:off) [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Blocks e2e tests 1/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core API tests - @woocommerce/plugin-woocommerce [api]
  • GitHub Check: Metrics - @woocommerce/plugin-woocommerce [performance]
  • GitHub Check: Core e2e tests 3/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest (HPOS:off) [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Core e2e tests 4/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 6/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 2/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 5/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.3] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Core e2e tests 1/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.3] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Lint - @woocommerce/plugin-woocommerce
  • GitHub Check: build

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (12)
plugins/woocommerce/composer.json (2)

35-39: Pin MCP Adapter source for reproducible builds.

Using a VCS repo + dev branch is fine for a draft, but please pin to a tag or commit before merge to ensure repeatable builds and avoid surprise breakages when the trunk moves.


56-58: Avoid requiring dev-trunk in core.

wordpress/mcp-adapter: dev-trunk will float. Prefer a tagged release or a specific commit reference (and update when cutting a WC release). Also confirm wordpress/abilities-api range is correct against production sites.

plugins/woocommerce/src/Internal/MCP/ApiKeyManager.php (1)

29-37: Avoid implicit key generation on read.

get_api_key() creates a key when missing; this is invoked from the feature description, which can generate a secret merely by viewing the page. Split into ensure_api_key() (explicit generation) and get_api_key() (read-only), or gate generation behind an explicit admin action.

plugins/woocommerce/includes/class-woocommerce.php (1)

358-360: Gate MCP services behind the feature flag to avoid unnecessary boot.

Avoid instantiating MCP services unless the feature is enabled.

Apply this diff:

-		$container->get( AbilitiesRegistry::class );
-		$container->get( MCPAdapterProvider::class );
+		if ( wc_get_container()->get( FeaturesController::class )->feature_is_enabled( 'mcp_integration' ) ) {
+			$container->get( AbilitiesRegistry::class );
+			$container->get( MCPAdapterProvider::class );
+		}
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1)

10-14: Remove unused import.

use WP_Error; is unused when instantiating errors with \WP_Error. Either drop the import or use the imported symbol consistently.

plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (2)

30-37: Consider gating ability initialization behind the feature flag.

If MCP is disabled, you may not want to register MCP-facing abilities at all. Optional, depending on intended reuse outside MCP.


44-63: Naming consistency.

Method mixes camelCase with snake_case methods in the same class. Prefer get_ability_ids() and keep getAbilitiesIDs() as a deprecated alias for BC.

-	public function getAbilitiesIDs(): array {
+	/** @deprecated Use get_ability_ids(). */
+	public function getAbilitiesIDs(): array {
+		return $this->get_ability_ids();
+	}
+
+	public function get_ability_ids(): array {
 		...
 	}
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (5)

35-37: Hook timing comment doesn’t match the hook; prefer plugins_loaded.

Use plugins_loaded to ensure dependencies are loaded before checking the adapter class.

-		// Hook into WordPress plugins loaded to ensure proper timing
-		add_action( 'init', array( $this, 'maybe_initialize' ) );
+		// Initialize after all plugins are loaded to ensure proper timing.
+		add_action( 'plugins_loaded', array( $this, 'maybe_initialize' ) );

89-101: Make server configuration overridable.

Expose a filter so site owners can adjust namespace/route if needed.

-		// Create MCP server
-		try {
-			$adapter->create_server(
-				'woocommerce-mcp',
-				'woocommerce',
-				'mcp',
-				__( 'WooCommerce MCP Server', 'woocommerce' ),
-				__( 'AI-accessible WooCommerce operations via MCP', 'woocommerce' ),
-				'1.0.0',
-				array( WooCommerceRestTransport::class ),
-				\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
-				\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
-				$abilities_ids,
-			);
+		$args = apply_filters(
+			'woocommerce_mcp_server_args',
+			array(
+				'id'            => 'woocommerce-mcp',
+				'namespace'     => 'woocommerce',
+				'route'         => 'mcp',
+				'name'          => __( 'WooCommerce MCP Server', 'woocommerce' ),
+				'description'   => __( 'AI-accessible WooCommerce operations via MCP', 'woocommerce' ),
+				'version'       => '1.0.0',
+				'transports'    => array( WooCommerceRestTransport::class ),
+				'error_handler' => \WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
+				'observability' => \WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
+				'abilities'     => $abilities_ids,
+			)
+		);
+		try {
+			$adapter->create_server(
+				$args['id'],
+				$args['namespace'],
+				$args['route'],
+				$args['name'],
+				$args['description'],
+				$args['version'],
+				$args['transports'],
+				$args['error_handler'],
+				$args['observability'],
+				$args['abilities']
+			);

61-69: Re-initialization safety when adapter loads late.

If the adapter class isn’t available at this point, there’s no retry. Optionally add a late hook (e.g., rest_api_init) to re-attempt initialization.


76-77: Param type documentation.

Add a docblock @param \WP\MCP\Core\McpAdapter $adapter for clarity since the method is public and untyped.


1-112: Security follow-up: avoid admin-context execution in transport.

Given WooCommerceRestTransport currently elevates to the first admin user after API-key validation (see Transport/WooCommerceRestTransport.php lines 22–89), prioritize implementing per-key user association and least-privilege execution before enabling this by default.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1601aa3 and 2d74b80.

⛔ Files ignored due to path filters (1)
  • plugins/woocommerce/composer.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • plugins/woocommerce/composer.json (2 hunks)
  • plugins/woocommerce/includes/class-woocommerce.php (2 hunks)
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (1 hunks)
  • plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php (1 hunks)
  • plugins/woocommerce/src/Internal/Features/FeaturesController.php (1 hunks)
  • plugins/woocommerce/src/Internal/MCP/ApiKeyManager.php (1 hunks)
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1 hunks)
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/includes/class-woocommerce.php
  • plugins/woocommerce/src/Internal/Features/FeaturesController.php
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
  • plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php
  • plugins/woocommerce/src/Internal/MCP/ApiKeyManager.php
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/includes/class-woocommerce.php
  • plugins/woocommerce/src/Internal/Features/FeaturesController.php
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
  • plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php
  • plugins/woocommerce/src/Internal/MCP/ApiKeyManager.php
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
🧬 Code graph analysis (7)
plugins/woocommerce/includes/class-woocommerce.php (2)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1)
  • MCPAdapterProvider (22-112)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (1)
  • AbilitiesRegistry (18-64)
plugins/woocommerce/src/Internal/Features/FeaturesController.php (1)
plugins/woocommerce/src/Internal/MCP/ApiKeyManager.php (2)
  • ApiKeyManager (17-89)
  • get_api_key (29-37)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
plugins/woocommerce/src/Internal/MCP/ApiKeyManager.php (2)
  • ApiKeyManager (17-89)
  • validate_api_key (66-69)
plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php (1)
  • check_permission (113-116)
plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php (4)
plugins/woocommerce/includes/class-woocommerce.php (1)
  • WooCommerce (45-1643)
plugins/woocommerce/woocommerce.php (1)
  • WC (46-48)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • get_woocommerce_currency (485-487)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1)
  • check_permission (31-79)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (2)
plugins/woocommerce/includes/class-woocommerce.php (2)
  • WooCommerce (45-1643)
  • init (868-900)
plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php (2)
  • StoreInfoAbility (18-117)
  • init (23-26)
plugins/woocommerce/src/Internal/MCP/ApiKeyManager.php (1)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1)
  • validate_api_key (87-89)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (5)
plugins/woocommerce/includes/class-woocommerce.php (3)
  • WooCommerce (45-1643)
  • __construct (251-256)
  • instance (160-165)
plugins/woocommerce/src/Utilities/FeaturesUtil.php (1)
  • FeaturesUtil (14-102)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (3)
  • AbilitiesRegistry (18-64)
  • __construct (23-25)
  • getAbilitiesIDs (44-63)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1)
  • WooCommerceRestTransport (23-90)
plugins/woocommerce/woocommerce.php (1)
  • wc_get_container (57-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (24)
  • GitHub Check: Blocks e2e tests 5/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 10/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core API tests - @woocommerce/plugin-woocommerce [api]
  • GitHub Check: Core e2e tests 6/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 5/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 8/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 9/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 7/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 6/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 1/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.3] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Blocks e2e tests 2/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 3/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.3] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Metrics - @woocommerce/plugin-woocommerce [performance]
  • GitHub Check: Blocks e2e tests 4/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 3/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 2/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 4/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 1/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Lint - @woocommerce/plugin-woocommerce
  • GitHub Check: build
🔇 Additional comments (1)
plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php (1)

113-116: Permission is fine; consider narrowing if more abilities ship.

Keeping it at view_woocommerce_reports is reasonable for read-only store info. Revisit if abilities start exposing data beyond reports.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (1)

38-62: Do not auto-expose every woocommerce/* ability to MCP. Use an allowlist + filter.

This recreates the previously flagged security issue: any extension can register a woocommerce/* ability and it becomes MCP‑exposed (executed under admin context via the transport). Ship with a minimal core allowlist and a filter for opt‑in.

Apply:

 	/**
 	 * Get WooCommerce ability IDs from the WordPress Abilities API.
 	 *
 	 * @return array Array of WooCommerce ability IDs.
 	 */
 	public function getAbilitiesIDs(): array {
-		// Check if the abilities API is available
-		if ( ! function_exists( 'wp_get_abilities' ) ) {
-			return array();
-		}
-
-		// Get all registered abilities
-		$all_abilities = wp_get_abilities();
-		
-		// Filter for WooCommerce-specific abilities
-		$woocommerce_abilities = array();
-		foreach ( array_keys( $all_abilities ) as $ability_id ) {
-			// Check if ability ID starts with 'woocommerce/'
-			if ( strpos( $ability_id, 'woocommerce/' ) === 0 ) {
-				$woocommerce_abilities[] = $ability_id;
-			}
-		}
-
-		return $woocommerce_abilities;
+		$all_ids = array();
+		if ( function_exists( 'wp_get_abilities' ) ) {
+			$all = wp_get_abilities();
+			$all_ids = is_array( $all ) ? array_keys( $all ) : array();
+		}
+		// Core allowlist (safe by default).
+		$default = array(
+			'woocommerce/store-info',
+		);
+		$allowed = array_values( array_intersect( $default, $all_ids ) );
+		/**
+		 * Filter the list of Woo abilities exposed to MCP.
+		 *
+		 * Extensions must explicitly opt in; no wildcard prefixing.
+		 *
+		 * @param string[] $allowed Allowlisted and registered ability IDs.
+		 * @param string[] $all_ids All registered ability IDs.
+		 */
+		return apply_filters( 'woocommerce_mcp_ability_ids', $allowed, $all_ids );
 	}
🧹 Nitpick comments (5)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1)

33-36: Silence the unused parameter warning.

Use the variable to appease linters.

Apply:

-	protected function validate_output( $output ) {
+	protected function validate_output( $output ) {
+		// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+		$output = $output;

(If you adopt the previous diff, this nit is covered.)

plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php (1)

99-111: Hook exists but consider early-return if Abilities API is missing.

Minor: add a function_exists guard to avoid scheduling a no-op action in older WP environments.

Apply:

 	public static function init(): void {
-		// Register abilities when Abilities API is ready
-		add_action( 'abilities_api_init', array( __CLASS__, 'register_abilities' ) );
+		if ( function_exists( 'wp_register_ability' ) ) {
+			add_action( 'abilities_api_init', array( __CLASS__, 'register_abilities' ) );
+		}
 	}
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (1)

30-36: Init only when Abilities API exists.

Small guard to avoid touching classes/hooks on older WP.

Apply:

 	private function init_abilities(): void {
-		// Initialize store info ability
-		StoreInfoAbility::init();
-
-		// Initialize REST bridge for REST endpoint abilities
-		AbilitiesRestBridge::init();
+		if ( function_exists( 'wp_register_ability' ) ) {
+			StoreInfoAbility::init();
+			AbilitiesRestBridge::init();
+		}
 	}
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (2)

146-177: Input schema isn’t proper JSON Schema; avoid passing WP REST arg arrays verbatim.

get_collection_params()/get_endpoint_args_for_item_schema() include callbacks and WP-specific keys. Either convert to JSON Schema or relax input validation for these abilities.

Option A (convert minimal fields): keep types/required/default and drop callbacks.
Option B (pragmatic): mark input schema as generic and rely on REST validation:

-					return array(
-						'type'       => 'object',
-						'properties' => $controller->get_collection_params(),
-					);
+					return array( 'type' => 'object' );

Same for create/update.


25-37: Guard against missing Abilities API.

Early-return if wp_register_ability is absent; you’ve done it inside, but avoid instantiating controllers unnecessarily.

Apply:

 	public static function register_controller_abilities( array $config ): void {
+		if ( ! function_exists( 'wp_register_ability' ) ) {
+			return;
+		}
 		$controller_class = $config['controller'];
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d74b80 and a733652.

📒 Files selected for processing (6)
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (1 hunks)
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php (1 hunks)
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1 hunks)
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1 hunks)
  • plugins/woocommerce/src/Internal/MCP/ApiKeyManager.php (1 hunks)
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugins/woocommerce/src/Internal/MCP/ApiKeyManager.php
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php
🧬 Code graph analysis (3)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1)
  • RestAbility (20-37)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php (1)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (2)
  • RestAbilityFactory (18-259)
  • register_controller_abilities (25-37)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (3)
plugins/woocommerce/includes/class-woocommerce.php (1)
  • init (868-900)
plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php (2)
  • StoreInfoAbility (18-117)
  • init (23-26)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php (2)
  • init (99-102)
  • AbilitiesRestBridge (20-112)
🪛 PHPMD (2.15.0)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php

254-254: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)


254-254: Avoid unused parameters such as '$operation'. (undefined)

(UnusedFormalParameter)


254-254: Avoid unused parameters such as '$input'. (undefined)

(UnusedFormalParameter)

plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php

33-33: Avoid unused parameters such as '$output'. (undefined)

(UnusedFormalParameter)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php (1)

101-108: Avoid loading all orders into memory; use wc_orders_count() (cached, HPOS-safe).

wc_get_orders with limit -1 is unbounded and can OOM on large stores.

-			// Orders using wc_get_orders for accurate counting
-			$completed_orders = wc_get_orders( array(
-				'status' => 'completed',
-				'limit'  => -1,
-				'return' => 'ids',
-			) );
-			$completed_count = count( $completed_orders );
+			// Orders: use cached counter
+			$completed_count = (int) wc_orders_count( 'completed' );
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)

113-116: Good: user context now matches the API key owner (no blanket admin).

This addresses the prior “impersonate first admin” risk noted in earlier reviews. Nice improvement.


24-32: Require a WP_REST_Request; avoid nullable parameter here.

validate_request() unconditionally expects a WP_REST_Request, but check_permission() accepts null. Passing null would fatal when calling $request->get_header(...). Align the signature and drop the default null. If the parent method is already typed, mirror it.

Apply this diff (verify compatibility with the parent RestTransport before merging):

-	public function check_permission( $request = null ) {
+	public function check_permission( \WP_REST_Request $request ) {
 		return $this->validate_request( $request );
 	}

If the parent is untyped, keep it untyped but still remove the nullable default:

-	public function check_permission( $request = null ) {
+	public function check_permission( $request ) {
 		return $this->validate_request( $request );
 	}
🧹 Nitpick comments (10)
plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php (6)

41-51: Localize property description in input schema.

Wrap the description with translation helpers.

-						'include_stats' => array(
-							'type'        => 'boolean',
-							'description' => 'Whether to include basic store statistics (product count, order count, etc.)',
-							'default'     => false,
-						),
+						'include_stats' => array(
+							'type'        => 'boolean',
+							'description' => __( 'Whether to include basic store statistics (product count, order count, etc.)', 'woocommerce' ),
+							'default'     => false,
+						),

86-94: Use home_url() for the public-facing store URL.

site_url() can point to wp-admin or a subdirectory; home_url() reflects the storefront address.

-			'store_url'           => get_site_url(),
+			'store_url'           => home_url(),

109-113: Clarify whether order_count is “total orders” or “completed orders only”.

If “total,” sum all wc-* statuses via wc_orders_count; if “completed,” rename the field or document it in schema.

Option (total orders + breakdown):

-			$order_count = $completed_count;
+			$statuses     = function_exists( 'wc_get_order_statuses' ) ? array_keys( wc_get_order_statuses() ) : array();
+			$total_orders = 0;
+			$order_breakdown = array( 'completed' => $completed_count );
+			foreach ( $statuses as $st ) {
+				// wc_orders_count accepts slugs without 'wc-' prefix.
+				$slug          = preg_replace( '/^wc-/', '', $st );
+				$count         = (int) wc_orders_count( $slug );
+				$total_orders += $count;
+				if ( ! isset( $order_breakdown[ $slug ] ) ) {
+					$order_breakdown[ $slug ] = $count;
+				}
+			}
+			$order_count = $total_orders;

And in schema (if following this path), list known keys or keep it generic as an object. See earlier schema diff.

Also applies to: 62-70


96-124: Type-safety and defaults for input.

Normalize include_stats to bool to avoid truthy surprises from non-bool inputs.

-		if ( ! empty( $input['include_stats'] ) ) {
+		$include_stats = ! empty( $input['include_stats'] ) && (bool) $input['include_stats'];
+		if ( $include_stats ) {

79-87: Ask for tests covering permission and stats toggles.

Please add unit/E2E tests for:

  • permission gate behavior,
  • include_stats true/false,
  • order counting on HPOS and posts table stores,
  • inclusion gating for admin_email (if adopted).

I can draft PHPUnit tests for the ability execution and schema conformance if helpful.

Also applies to: 126-137


101-113: Optional: small performance tidy-up.

product_count uses wp_count_posts()->publish which is fine; consider caching the result for the request (transient not needed) if used multiple times. Not blocking.

plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (4)

44-58: Localize and harden error messages; don’t leak header format details.

User‑visible strings should be translatable. Also avoid revealing exact header format in errors.

Apply this diff:

-			return new \WP_Error(
-				'missing_api_key',
-				'X-MCP-API-Key header required. Format: consumer_key:consumer_secret',
-				array( 'status' => 401 )
-			);
+			return new \WP_Error(
+				'missing_api_key',
+				__( 'API key required in the X-MCP-API-Key header.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);
 		}
 
 		if ( strpos( $api_key, ':' ) === false ) {
-			return new \WP_Error(
-				'invalid_api_key',
-				'X-MCP-API-Key must be in format consumer_key:consumer_secret',
-				array( 'status' => 401 )
-			);
+			return new \WP_Error(
+				'invalid_api_key',
+				__( 'Malformed API key.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);
 		}

45-49: Avoid credential enumeration in responses.

Returning distinct messages/codes for “invalid consumer key” vs “invalid consumer secret” enables enumeration. Prefer a single invalid_credentials code/message for both failure paths.

Apply this simplified diff if you agree:

-				'invalid_consumer_key',
-				__( 'Invalid credentials.', 'woocommerce' ),
+				'invalid_credentials',
+				__( 'Invalid credentials.', 'woocommerce' ),
@@
-				'invalid_consumer_secret',
-				__( 'Invalid credentials.', 'woocommerce' ),
+				'invalid_credentials',
+				__( 'Invalid credentials.', 'woocommerce' ),

Also applies to: 54-57, 98-101, 107-110


10-13: Unused imports.

use WP_REST_Request; and use WP_Error; aren’t used (you reference FQCNs). Either use the imports or drop them.


40-70: Tests needed for auth paths and headers.

Please add unit/integration tests covering:

  • Missing header → 401.
  • Malformed header → 401.
  • Valid key/secret → sets current user to the key owner.
  • Invalid key and invalid secret → 401 with the same error when enumeration mitigation is applied.
  • Permission mismatch → 403.
  • HTTPS enforcement toggle via woocommerce_mcp_allow_insecure_transport.

I can scaffold WP_UnitTestCase tests for these cases if helpful.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a733652 and 96fb9a1.

📒 Files selected for processing (4)
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (1 hunks)
  • plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php (1 hunks)
  • plugins/woocommerce/src/Internal/Features/FeaturesController.php (1 hunks)
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugins/woocommerce/src/Internal/Features/FeaturesController.php
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
  • plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
  • plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php
🧬 Code graph analysis (2)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_api_hash (1437-1439)
plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php (2)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • get_woocommerce_currency (485-487)
plugins/woocommerce/includes/wc-order-functions.php (1)
  • wc_get_orders (36-77)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Lint - @woocommerce/plugin-woocommerce
  • GitHub Check: build
🔇 Additional comments (4)
plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php (4)

20-27: OK: guarded registration with Abilities API.

Registration is deferred until abilities_api_init and guarded by function_exists.

If the Abilities API can be disabled at runtime, ensure StoreInfoAbility::init() is called only once (it is; add_action handles duplicates). No action needed.

Also applies to: 31-36


114-117: Customer count approach is fine.

count_users()['avail_roles']['customer'] is the right bucket; safe fallback handled.


1-18: File scaffolding looks good.

Strict types, namespace, ABSPATH guard, and docblocks are appropriate.


101-113: wc_orders_count is present in this repo — prefer it for counting orders.

Defined at plugins/woocommerce/includes/wc-order-functions.php:391 and referenced across tests and multiple files.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (2)

39-39: Avoid front‑end initialization; defer to REST or guard with REST_REQUEST.

Running on init adds overhead on non‑REST requests. Prefer rest_api_init or guard in maybe_initialize.

-		add_action( 'init', array( $this, 'maybe_initialize' ) );
+		add_action( 'init', array( $this, 'maybe_initialize' ) );
 	public function maybe_initialize(): void {
 		// Check if MCP integration feature is enabled
 		if ( ! FeaturesUtil::feature_is_enabled( 'mcp_integration' ) ) {
 			return;
 		}
+		// Only proceed for REST requests to avoid front-end overhead.
+		if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
+			return;
+		}

Alternatively, move the hook to rest_api_init:

-		add_action( 'init', array( $this, 'maybe_initialize' ) );
+		add_action( 'rest_api_init', array( $this, 'maybe_initialize' ) );

Please confirm timing with the MCP adapter’s mcp_adapter_init firing order.

Also applies to: 45-59


87-115: Harden server initialization: type‑check adapter and localize metadata.

Guard against unexpected $adapter and localize server name/description.

 	public function initialize_mcp_server( $adapter ): void {
 		// Get abilities from the registry
 		$abilities_registry = wc_get_container()->get( AbilitiesRegistry::class );
 		$abilities_ids = $abilities_registry->getAbilitiesIDs();

 		// Bail if no abilities are available
 		if ( empty( $abilities_ids ) ) {
 			return;
 		}
+		// Ensure adapter has the expected API.
+		if ( ! is_object( $adapter ) || ! method_exists( $adapter, 'create_server' ) ) {
+			return;
+		}

 		// Temporarily disable MCP validation during server creation
 		// Workaround for validator bug with union types (e.g., ["integer", "null"])
 		// TODO: Remove once mcp-adapter validator bug is fixed
 		add_filter( 'mcp_validation_enabled', '__return_false', 999 );

 		try {
 			// Create MCP server
 			$adapter->create_server(
 				'woocommerce-mcp',
 				'woocommerce',
 				'mcp',
-				'WooCommerce MCP Server',
-				'AI-accessible WooCommerce operations via MCP',
+				__( 'WooCommerce MCP Server', 'woocommerce' ),
+				__( 'AI-accessible WooCommerce operations via MCP', 'woocommerce' ),
 				'1.0.0',
 				array( WooCommerceRestTransport::class ),
 				\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
 				\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
 				$abilities_ids,
 			);
plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php (2)

44-53: Do not expose admin_email by default; gate behind explicit flag and stronger cap.

PII leakage risk. Add an input flag and require manage_options (or manage_woocommerce) before returning admin_email. Also localize the new description.

 				'input_schema'      => array(
 					'type'       => 'object',
 					'properties' => array(
 						'include_stats' => array(
 							'type'        => 'boolean',
-							'description' => 'Whether to include basic store statistics (product count, order count, etc.)',
+							'description' => __( 'Whether to include basic store statistics (product count, order count, etc.)', 'woocommerce' ),
 							'default'     => false,
 						),
+						'include_sensitive' => array(
+							'type'        => 'boolean',
+							'description' => __( 'Include sensitive fields (e.g., admin_email). Requires manage_options.', 'woocommerce' ),
+							'default'     => false,
+						),
 					),
 				),
 		$result = array(
 			'store_name'          => get_bloginfo( 'name' ),
-			'store_url'           => get_site_url(),
-			'admin_email'         => get_bloginfo( 'admin_email' ),
+			'store_url'           => get_site_url(),
 			'woocommerce_version' => WC()->version,
 			'wordpress_version'   => get_bloginfo( 'version' ),
 			'currency'            => get_woocommerce_currency(),
 			'country'             => WC()->countries->get_base_country(),
 		);
+
+		// Optionally include PII if explicitly requested and caller has elevated caps.
+		if ( ! empty( $input['include_sensitive'] ) && current_user_can( 'manage_options' ) ) {
+			$result['admin_email'] = (string) get_bloginfo( 'admin_email' );
+		}

Also applies to: 59-59, 88-96


64-71: Schema/result mismatch: stats.order_breakdown is returned but not declared.

Declare order_breakdown in the output schema to avoid validation/runtime mismatches. Keys match the returned breakdown.

 						'stats'               => array(
 							'type'       => 'object',
 							'properties' => array(
 								'product_count'  => array( 'type' => 'integer' ),
 								'order_count'    => array( 'type' => 'integer' ),
 								'customer_count' => array( 'type' => 'integer' ),
+								'order_breakdown' => array(
+									'type'       => 'object',
+									'properties' => array(
+										'completed'  => array( 'type' => 'integer' ),
+										'processing' => array( 'type' => 'integer' ),
+										'pending'    => array( 'type' => 'integer' ),
+										'on-hold'    => array( 'type' => 'integer' ),
+										'cancelled'  => array( 'type' => 'integer' ),
+										'refunded'   => array( 'type' => 'integer' ),
+										'failed'     => array( 'type' => 'integer' ),
+									),
+								),
 							),
 						),

Also applies to: 112-121, 128-133

🧹 Nitpick comments (4)
plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php (4)

49-51: Localize schema description strings.

Wrap the input_schema description in translation functions for i18n.

-							'description' => 'Whether to include basic store statistics (product count, order count, etc.)',
+							'description' => __( 'Whether to include basic store statistics (product count, order count, etc.)', 'woocommerce' ),

103-123: Future‑proof order_count by summing all current wc- statuses.*

If new statuses are added (extensions), they won’t be reflected. Sum over wc_get_order_statuses for order_count while retaining the explicit breakdown you already return.

-			$order_count = $completed_count + $processing_count + $pending_count + $on_hold_count + $cancelled_count + $refunded_count + $failed_count;
+			$order_count = 0;
+			if ( function_exists( 'wc_get_order_statuses' ) ) {
+				foreach ( array_keys( wc_get_order_statuses() ) as $status ) {
+					$order_count += (int) wc_orders_count( $status );
+				}
+			} else {
+				$order_count = $completed_count + $processing_count + $pending_count + $on_hold_count + $cancelled_count + $refunded_count + $failed_count;
+			}

90-90: Consider home_url() for public-facing store URL.

home_url() better represents the storefront URL when WordPress and site URLs differ.

-			'store_url'           => get_site_url(),
+			'store_url'           => home_url(),

75-77: Add tests to cover schema shape, PII gating, and permission checks.

Please add unit/E2E tests ensuring:

  • order_breakdown schema matches returned keys
  • admin_email only returns when include_sensitive=true and user has manage_options
  • permission_callback enforces access

I can scaffold tests for Abilities API execution and permission gating if helpful.

Also applies to: 144-147

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3312031 and 5731c58.

📒 Files selected for processing (2)
  • plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php (1 hunks)
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
  • plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
  • plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php
🧬 Code graph analysis (2)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (5)
plugins/woocommerce/includes/class-woocommerce.php (1)
  • __construct (251-256)
plugins/woocommerce/src/Utilities/FeaturesUtil.php (1)
  • FeaturesUtil (14-102)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (3)
  • AbilitiesRegistry (18-50)
  • __construct (23-25)
  • getAbilitiesIDs (40-49)
plugins/woocommerce/woocommerce.php (1)
  • wc_get_container (57-59)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_get_logger (2122-2152)
plugins/woocommerce/src/Internal/Abilities/StoreInfoAbility.php (4)
plugins/woocommerce/includes/class-woocommerce.php (1)
  • WooCommerce (45-1643)
plugins/woocommerce/woocommerce.php (1)
  • WC (46-48)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • get_woocommerce_currency (485-487)
plugins/woocommerce/includes/wc-order-functions.php (1)
  • wc_orders_count (391-419)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (2)

60-66: Enforce permissions in ability permission_callback (don’t return true).

Gate at the ability level; map to REST permissions and pass route for context.

-					'execute_callback'    => function( $input ) use ( $controller, $ability_config, $route ) {
-						return self::execute_operation( $controller, $ability_config['operation'], $input, $route );
-					},
-					'permission_callback' => function( $input ) use ( $controller, $ability_config ) {
-						return self::check_permission( $controller, $ability_config['operation'], $input );
-					},
+					'execute_callback'    => function( $input ) use ( $ability_config, $route ) {
+						return self::execute_operation( $ability_config['operation'], $input, $route );
+					},
+					'permission_callback' => function( $input ) use ( $controller, $ability_config, $route ) {
+						return self::check_permission( $controller, $ability_config['operation'], $input, $route );
+					},

240-244: Implement real permission checks (map to controller permission methods; fallback to REST probe).

Return boolean or WP_Error.

-	private static function check_permission( $controller, string $operation, array $input ): bool {
-		// We return true here and let the controller handle permissions
-		// The REST controller will check permissions when its methods are called
-		return true;
-	}
+	private static function check_permission( $controller, string $operation, array $input, string $route ) {
+		$perm_map = array(
+			'list'   => 'get_items_permissions_check',
+			'get'    => 'get_item_permissions_check',
+			'create' => 'create_item_permissions_check',
+			'update' => 'update_item_permissions_check',
+			'delete' => 'delete_item_permissions_check',
+		);
+		$method_map = array(
+			'list'   => 'GET',
+			'get'    => 'GET',
+			'create' => 'POST',
+			'update' => 'PUT',
+			'delete' => 'DELETE',
+		);
+		$req_method   = $method_map[ $operation ] ?? 'GET';
+		$request_route = rtrim( $route, '/' );
+		if ( isset( $input['id'] ) && in_array( $operation, array( 'get', 'update', 'delete' ), true ) ) {
+			$request_route .= '/' . absint( $input['id'] );
+		}
+		// Prefer controller permission method if present.
+		$perm_fn = $perm_map[ $operation ] ?? null;
+		if ( $perm_fn && method_exists( $controller, $perm_fn ) ) {
+			$request = new \WP_REST_Request( $req_method, $request_route );
+			if ( isset( $input['id'] ) ) {
+				$request->set_param( 'id', absint( $input['id'] ) );
+			}
+			$allowed = $controller->{$perm_fn}( $request );
+			if ( true === $allowed ) {
+				return true;
+			}
+			if ( is_wp_error( $allowed ) ) {
+				return $allowed;
+			}
+			return new \WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to perform this action.', 'woocommerce' ), array( 'status' => 403 ) );
+		}
+		// Fallback probe through REST.
+		$probe = new \WP_REST_Request( $req_method, $request_route );
+		$response = rest_do_request( $probe );
+		if ( is_wp_error( $response ) ) {
+			return $response;
+		}
+		$status = $response instanceof \WP_REST_Response ? $response->get_status() : 200;
+		return $status < 400;
+	}
🧹 Nitpick comments (13)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (13)

190-200: Handle unknown operations explicitly.

Avoid defaulting to GET; surface a clear error.

-		$method = $method_map[ $operation ] ?? 'GET';
+		$method = $method_map[ $operation ] ?? null;
+		if ( null === $method ) {
+			return new \WP_Error(
+				'woocommerce_mcp_unknown_operation',
+				sprintf( 'Unknown operation "%s".', $operation ),
+				array( 'status' => 400 )
+			);
+		}

201-207: Normalize route and sanitize id.

Prevent double slashes and use absint.

-		$request_route = $route;
+		$request_route = '/' . ltrim( rtrim( $route, '/' ), '/' );
 		if ( isset( $input['id'] ) && in_array( $operation, array( 'get', 'update', 'delete' ), true ) ) {
-			$request_route .= '/' . intval( $input['id'] );
+			$request_route .= '/' . absint( $input['id'] );
 			unset( $input['id'] );
 		}

208-213: Send body for mutating requests.

Some controllers expect body params; keep params for GET.

-		$request = new \WP_REST_Request( $method, $request_route );
-		foreach ( $input as $key => $value ) {
-			$request->set_param( $key, $value );
-		}
+		$request = new \WP_REST_Request( $method, $request_route );
+		if ( in_array( $method, array( 'POST', 'PUT', 'PATCH' ), true ) ) {
+			$request->set_body_params( $input );
+		} else {
+			foreach ( $input as $key => $value ) {
+				$request->set_param( $key, $value );
+			}
+		}

221-226: Return pagination metadata for list operations.

Expose X-WP-Total and X-WP-TotalPages.

-		// For list operations, wrap in data object to match schema
-		if ( 'list' === $operation ) {
-			return array( 'data' => $data );
-		}
+		// For list operations, wrap in data object and include pagination meta.
+		if ( 'list' === $operation ) {
+			$headers = $response instanceof \WP_REST_Response ? $response->get_headers() : array();
+			$meta    = array();
+			if ( isset( $headers['X-WP-Total'] ) ) {
+				$meta['total'] = (int) $headers['X-WP-Total'];
+			}
+			if ( isset( $headers['X-WP-TotalPages'] ) ) {
+				$meta['total_pages'] = (int) $headers['X-WP-TotalPages'];
+			}
+			return array( 'data' => $data, 'meta' => $meta );
+		}

147-162: Align list output schema with meta.

Add meta.total and meta.total_pages.

-				return array(
-					'type'       => 'object',
-					'properties' => array(
-						'data' => array(
-							'type'  => 'array',
-							'items' => $schema,
-						),
-					),
-				);
+				return array(
+					'type'       => 'object',
+					'properties' => array(
+						'data' => array(
+							'type'  => 'array',
+							'items' => $schema,
+						),
+						'meta' => array(
+							'type'       => 'object',
+							'properties' => array(
+								'total'       => array( 'type' => 'integer', 'minimum' => 0 ),
+								'total_pages' => array( 'type' => 'integer', 'minimum' => 0 ),
+							),
+						),
+					),
+				);

86-91: Normalize WP REST arg defs into valid JSON Schema (capture required).

WP param defs embed “required” per-field; JSON Schema expects top-level required[]. Convert args before returning schema.

-					return array(
-						'type'       => 'object',
-						'properties' => $controller->get_collection_params(),
-					);
+					return self::convert_wp_args_to_json_schema( $controller->get_collection_params() );
-					return array(
-						'type'       => 'object',
-						'properties' => $args,
-					);
+					return self::convert_wp_args_to_json_schema( $args );
-					$args       = $controller->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE );
-					$args['id'] = array(
+					$args       = $controller->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE );
+					$args['id'] = array(
 						'type'        => 'integer',
 						'description' => __( 'Unique identifier for the resource', 'woocommerce' ),
+						'minimum'     => 1,
 					);
-					return array(
-						'type'       => 'object',
-						'properties' => $args,
-						'required'   => array( 'id' ),
-					);
+					$schema = self::convert_wp_args_to_json_schema( $args );
+					$schema['required'] = array_values( array_unique( array_merge( $schema['required'] ?? array(), array( 'id' ) ) ) );
+					return $schema;

Add helper:

+	/**
+	 * Convert WP REST arg definitions to JSON Schema (object) and extract required fields.
+	 *
+	 * @param array $args WP-style arg defs.
+	 * @return array JSON Schema with properties + required[].
+	 */
+	private static function convert_wp_args_to_json_schema( array $args ): array {
+		$required   = array();
+		$properties = array();
+		foreach ( $args as $name => $def ) {
+			$prop = array();
+			foreach ( array( 'type', 'items', 'enum', 'format', 'properties', 'description', 'minimum', 'maximum', 'minItems', 'maxItems', 'default' ) as $k ) {
+				if ( isset( $def[ $k ] ) ) {
+					$prop[ $k ] = $def[ $k ];
+				}
+			}
+			if ( isset( $def['sanitize_callback'] ) && 'absint' === $def['sanitize_callback'] ) {
+				$prop['minimum'] = $prop['minimum'] ?? 0;
+			}
+			$properties[ $name ] = $prop ?: array( 'type' => 'string' );
+			if ( ! empty( $def['required'] ) ) {
+				$required[] = $name;
+			}
+		}
+		$schema = array(
+			'type'       => 'object',
+			'properties' => $properties,
+		);
+		if ( $required ) {
+			$schema['required'] = array_values( array_unique( $required ) );
+		}
+		return $schema;
+	}

Also applies to: 99-102, 105-118


121-134: Tighten ID schema for get/delete.

Add minimum 1.

 				return array(
 					'type'       => 'object',
 					'properties' => array(
 						'id' => array(
 							'type'        => 'integer',
 							'description' => __( 'Unique identifier for the resource', 'woocommerce' ),
+							'minimum'     => 1,
 						),
 					),
 					'required'   => array( 'id' ),
 				);

47-51: Don’t fail silently when Abilities API is missing.

Log a warning (Woo logger) to aid debugging.

-		if ( ! function_exists( 'wp_register_ability' ) ) {
-			return;
-		}
+		if ( ! function_exists( 'wp_register_ability' ) ) {
+			if ( function_exists( 'wc_get_logger' ) ) {
+				wc_get_logger()->warning(
+					sprintf( 'Abilities API not available; skipping ability "%s".', $ability_config['id'] ?? '(unknown)' ),
+					array( 'source' => 'rest-ability-factory' )
+				);
+			} else {
+				error_log( sprintf( '[woocommerce] Abilities API not available; skipping ability "%s".', $ability_config['id'] ?? '(unknown)' ) );
+			}
+			return;
+		}

70-72: Use WooCommerce logger instead of error_log.

Consistent logging with context.

-			error_log( "Failed to register ability {$ability_config['id']}: " . $e->getMessage() );
+			if ( function_exists( 'wc_get_logger' ) ) {
+				wc_get_logger()->error(
+					sprintf( 'Failed to register ability %s: %s', $ability_config['id'] ?? '(unknown)', $e->getMessage() ),
+					array( 'source' => 'rest-ability-factory' )
+				);
+			} else {
+				error_log( sprintf( '[woocommerce] Failed to register ability %s: %s', $ability_config['id'] ?? '(unknown)', $e->getMessage() ) );
+			}

25-37: Validate config keys and guard against missing entries.

Harden against absent abilities/route keys; log and continue.

 	public static function register_controller_abilities( array $config ): void {
-		$controller_class = $config['controller'];
+		if ( empty( $config['controller'] ) || empty( $config['abilities'] ) || empty( $config['route'] ) ) {
+			if ( function_exists( 'wc_get_logger' ) ) {
+				wc_get_logger()->warning( 'Invalid REST ability config: missing controller/abilities/route.', array( 'source' => 'rest-ability-factory' ) );
+			}
+			return;
+		}
+		$controller_class = $config['controller'];

147-179: Optional: mark delete output required fields.

Ensure deleted/previous presence.

 			} elseif ( 'delete' === $operation ) {
 				// For delete operations, return simple confirmation
 				return array(
 					'type'       => 'object',
 					'properties' => array(
 						'deleted' => array( 'type' => 'boolean' ),
 						'previous' => $schema,
 					),
+					'required' => array( 'deleted' ),
 				);

190-229: Nit: drop unused $controller param from execute_operation (cleans PHPMD warning).

Signature not using it after above changes.

-	private static function execute_operation( $controller, string $operation, array $input, string $route ) {
+	private static function execute_operation( string $operation, array $input, string $route ) {

60-62: Nit: align execute_callback with updated signature.

Remove unused $controller capture.

-					'execute_callback'    => function( $input ) use ( $controller, $ability_config, $route ) {
-						return self::execute_operation( $controller, $ability_config['operation'], $input, $route );
-					},
+					'execute_callback'    => function( $input ) use ( $ability_config, $route ) {
+						return self::execute_operation( $ability_config['operation'], $input, $route );
+					},
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5731c58 and ff63ada.

📒 Files selected for processing (2)
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php (1 hunks)
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
🧠 Learnings (2)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
📚 Learning: 2025-09-03T11:50:02.208Z
Learnt from: tpaksu
PR: woocommerce/woocommerce#60735
File: plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Fulfillments/class-wc-rest-fulfillments-v4-controller-tests.php:10-10
Timestamp: 2025-09-03T11:50:02.208Z
Learning: For REST API controller tests in plugins/woocommerce/tests/**/*.php, test classes should extend WC_REST_Unit_Test_Case instead of WC_Unit_Test_Case because REST routes need to be registered before tests run.

Applied to files:

  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
🧬 Code graph analysis (1)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1)
  • RestAbility (20-37)
🪛 PHPMD (2.15.0)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php

190-190: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)


240-240: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)


240-240: Avoid unused parameters such as '$operation'. (undefined)

(UnusedFormalParameter)


240-240: Avoid unused parameters such as '$input'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (1)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1)

52-69: LGTM — rest_do_request dispatch verified; routes match registered endpoints.

RestAbilityFactory::execute_operation dispatches WP_REST_Request to the routes configured in AbilitiesRestBridge ('/wc/v3/products' and '/wc/v3/orders' — plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php) and the corresponding controllers register those endpoints under namespace 'wc/v3' (plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php, class-wc-rest-orders-controller.php).

Copy link
Contributor

@peterfabian peterfabian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see this is being initialized on all requests, even for favicon or cron requests. Is it possible to limit this?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (5)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (5)

57-60: Require HTTPS for credentialed MCP calls (dev override allowed).

Prevent API key leakage over plaintext HTTP; allow opt‑in for local/dev via a filter.

 	public function validate_request( \WP_REST_Request $request ) {
+		// Require TLS by default; allow explicit opt-in for non-SSL (e.g., local dev).
+		if ( ! is_ssl() && ! apply_filters( 'woocommerce_mcp_allow_insecure_transport', false, $request ) ) {
+			return new \WP_Error(
+				'insecure_transport',
+				__( 'HTTPS is required for MCP requests.', 'woocommerce' ),
+				array( 'status' => 403 )
+			);
+		}
 		// Get X-MCP-API-Key header

99-101: Do not sanitize secrets; trim only before hashing.

sanitize_text_field() mutates values and will break valid keys.

-		// Hash the consumer key as WooCommerce does
-		$hashed_consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
+		// Hash the consumer key as WooCommerce does (trim only; do not mutate secrets).
+		$hashed_consumer_key = wc_api_hash( trim( (string) $consumer_key ) );

112-119: Unify auth errors (i18n), trim secret, and keep messages generic.

Avoid leaking which credential failed; keep timing-safe compare and translate strings.

-			return new \WP_Error(
-				'invalid_consumer_key',
-				'Consumer key is invalid.',
-				array( 'status' => 401 )
-			);
+			return new \WP_Error(
+				'invalid_consumer_key',
+				__( 'Invalid credentials.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);
@@
-		if ( ! hash_equals( $user_data->consumer_secret, $consumer_secret ) ) {
-			return new \WP_Error(
-				'invalid_consumer_secret',
-				'Consumer secret is invalid.',
-				array( 'status' => 401 )
-			);
+		if ( ! hash_equals( $user_data->consumer_secret, trim( (string) $consumer_secret ) ) ) {
+			return new \WP_Error(
+				'invalid_consumer_secret',
+				__( 'Invalid credentials.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);

Also applies to: 121-128


130-137: Enforce allowed key permissions and ensure user exists before switching context.

Prevents orphaned keys and too‑permissive scopes.

-		// Store permissions for tool-level checking
-		self::$current_mcp_permissions = $user_data->permissions;
-
-		// Set the current user
-		wp_set_current_user( $user_data->user_id );
+		// Store permissions for tool-level checking.
+		self::$current_mcp_permissions = $user_data->permissions;
+
+		// Enforce minimal permissions for MCP (filterable).
+		$allowed_permissions = apply_filters( 'woocommerce_mcp_allowed_key_permissions', array( 'read', 'read_write' ) );
+		if ( ! in_array( self::$current_mcp_permissions, $allowed_permissions, true ) ) {
+			return new \WP_Error(
+				'insufficient_api_key_permissions',
+				__( 'The API key does not grant required permissions for MCP.', 'woocommerce' ),
+				array( 'status' => 403 )
+			);
+		}
+
+		// Ensure the user exists before switching context.
+		$user = get_user_by( 'id', (int) $user_data->user_id );
+		if ( ! $user ) {
+			return new \WP_Error(
+				'mcp_user_not_found',
+				__( 'The user associated with this API key no longer exists.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);
+		}
+		wp_set_current_user( $user->ID );

44-49: Fix nullable param: avoid fatal in strict mode and match parent signature.

validate_request() requires \WP_REST_Request; passing null from check_permission() can fatally error. Type the param and drop the default.

-	 * @param WP_REST_Request|null $request The REST request object.
+	 * @param \WP_REST_Request $request The REST request object.
@@
-	public function check_permission( $request = null ) {
+	public function check_permission( \WP_REST_Request $request ) {
 		return $this->validate_request( $request );
 	}
🧹 Nitpick comments (9)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (4)

61-67: Localize error messages (i18n) and align phrasing.

User‑facing strings must be translatable.

-			return new \WP_Error(
-				'missing_api_key',
-				'X-MCP-API-Key header required. Format: consumer_key:consumer_secret',
-				array( 'status' => 401 )
-			);
+			return new \WP_Error(
+				'missing_api_key',
+				__( 'API key required in X-MCP-API-Key header. Format: consumer_key:consumer_secret', 'woocommerce' ),
+				array( 'status' => 401 )
+			);
@@
-			return new \WP_Error(
-				'invalid_api_key',
-				'X-MCP-API-Key must be in format consumer_key:consumer_secret',
-				array( 'status' => 401 )
-			);
+			return new \WP_Error(
+				'invalid_api_key',
+				__( 'X-MCP-API-Key must be in format consumer_key:consumer_secret.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);

Also applies to: 69-75


96-96: Strengthen types on private auth method.

Add scalar types for clarity and static analysis.

-	private function authenticate( $consumer_key, $consumer_secret ) {
+	private function authenticate( string $consumer_key, string $consumer_secret ) {

24-39: Avoid duplicate filter registration across multiple instances.

Make the filter hook idempotent to prevent duplicate callbacks.

 	/**
 	 * Current MCP user's API key permissions.
@@
 	private static $current_mcp_permissions = null;
+
+	/**
+	 * Whether the permission filter has been registered (avoid duplicate callbacks).
+	 *
+	 * @var bool
+	 */
+	private static $permission_filter_hooked = false;
@@
-		// Register permission filter callback
-		add_filter( 'woocommerce_check_rest_ability_permissions_for_method', array( $this, 'check_ability_permission' ), 10, 3 );
+		// Register permission filter callback once.
+		if ( ! self::$permission_filter_hooked ) {
+			add_filter( 'woocommerce_check_rest_ability_permissions_for_method', array( $this, 'check_ability_permission' ), 10, 3 );
+			self::$permission_filter_hooked = true;
+		}

156-178: Silence unused param warning and defer on unknown methods.

Rename the unused param and return $allowed for unknown HTTP verbs.

-	public function check_ability_permission( $allowed, $method, $controller ) {
+	public function check_ability_permission( $allowed, $method, $_controller ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
@@
-			default:
-				return false;
+			default:
+				// Unknown HTTP method; defer to prior filters/default.
+				return $allowed;
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (5)

10-10: Remove unused import.

WooCommerceRestTransport is not referenced in this file.

-use Automattic\WooCommerce\Internal\MCP\Transport\WooCommerceRestTransport;

27-39: Guard config keys and normalize the route to avoid notices/misroutes.

Ensure controller, route, and abilities are present/typed; normalize the route once.

-	public static function register_controller_abilities( array $config ): void {
-		$controller_class = $config['controller'];
+	public static function register_controller_abilities( array $config ): void {
+		$controller_class = $config['controller'] ?? null;
+		$route            = isset( $config['route'] ) ? '/' . ltrim( (string) $config['route'], '/' ) : null;
+		$abilities        = isset( $config['abilities'] ) && is_array( $config['abilities'] ) ? $config['abilities'] : array();
 
-		if ( ! class_exists( $controller_class ) ) {
+		if ( ! $controller_class || ! class_exists( $controller_class ) || ! $route ) {
 			return;
 		}
 
 		$controller = new $controller_class();
 
-		foreach ( $config['abilities'] as $ability_config ) {
-			self::register_single_ability( $controller, $ability_config, $config['route'] );
+		foreach ( $abilities as $ability_config ) {
+			self::register_single_ability( $controller, $ability_config, $route );
 		}
 	}

62-67: Drop unused $controller in execute_operation and normalize route in one place.

Removes PHPMD warning and makes routing resilient if a route without leading slash slips in.

-					'execute_callback'    => function( $input ) use ( $controller, $ability_config, $route ) {
-						return self::execute_operation( $controller, $ability_config['operation'], $input, $route );
+					'execute_callback'    => function( $input ) use ( $ability_config, $route ) {
+						return self::execute_operation( $ability_config['operation'], $input, $route );
 					},
-	private static function execute_operation( $controller, string $operation, array $input, string $route ) {
+	private static function execute_operation( string $operation, array $input, string $route ) {
 		$method = self::get_http_method_for_operation( $operation );
 
-		// Build final route - add ID for single item operations
-		$request_route = $route;
+		// Build final route - add ID for single item operations
+		$request_route = '/' . ltrim( $route, '/' );
 		if ( isset( $input['id'] ) && in_array( $operation, array( 'get', 'update', 'delete' ), true ) ) {
 			$request_route .= '/' . intval( $input['id'] );
 			unset( $input['id'] );
 		}

Also applies to: 192-201


71-74: Use WooCommerce logger instead of error_log.

Aligns with WC logging and allows filtering by source.

-		} catch ( \Throwable $e ) {
-			// Log the error for debugging but don't break the registration of other abilities
-			error_log( "Failed to register ability {$ability_config['id']}: " . $e->getMessage() );
-		}
+		} catch ( \Throwable $e ) {
+			$logger = function_exists( 'wc_get_logger' ) ? wc_get_logger() : null;
+			if ( $logger ) {
+				$logger->error(
+					sprintf( 'MCP: Failed to register ability %s: %s', $ability_config['id'], $e->getMessage() ),
+					array( 'source' => 'mcp-abilities' )
+				);
+			}
+		}

250-262: Return WP_Error on denial for clearer failures; suppress PHPMD unused param.

Improves DX by surfacing 403 with a message, and silences PHPMD for $input.

-	 * @return bool Whether permission is granted.
+	 * @return bool|\WP_Error Whether permission is granted.
 	 */
-	private static function check_permission( $controller, string $operation, array $input ): bool {
+	// @SuppressWarnings(PHPMD.UnusedFormalParameter)
+	private static function check_permission( $controller, string $operation, array $input ) {
 		// Get HTTP method for the operation
 		$method = self::get_http_method_for_operation( $operation );
 
 		/**
 		 * Filter to check REST ability permissions for HTTP method.
 		 *
 		 * @param bool   $allowed    Whether the operation is allowed. Default false.
 		 * @param string $method     HTTP method (GET, POST, PUT, DELETE).
 		 * @param object $controller REST controller instance.
 		 */
-		return apply_filters( 'woocommerce_check_rest_ability_permissions_for_method', false, $method, $controller );
+		$allowed = apply_filters( 'woocommerce_check_rest_ability_permissions_for_method', false, $method, $controller );
+		if ( $allowed ) {
+			return true;
+		}
+		return new \WP_Error(
+			'rest_forbidden',
+			__( 'You are not allowed to perform this operation via MCP.', 'woocommerce' ),
+			array( 'status' => 403 )
+		);
 	}
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff63ada and 4a80ebf.

📒 Files selected for processing (2)
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1 hunks)
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
🧠 Learnings (2)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
📚 Learning: 2025-09-17T16:16:48.098Z
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.

Applied to files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
🧬 Code graph analysis (2)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1)
  • check_permission (250-262)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_api_hash (1437-1439)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (2)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
  • WooCommerceRestTransport (22-180)
  • check_permission (47-49)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1)
  • RestAbility (20-37)
🪛 PHPMD (2.15.0)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php

156-156: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)

plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php

192-192: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)


250-250: Avoid unused parameters such as '$input'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (2)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1)

133-136: LGTM: set current user to API key owner (no admin impersonation).

This resolves the prior concern about blanket admin context on valid requests.

plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1)

27-39: Verify configured routes start with “/” and match controller namespace/rest_base. Automated verification failed (script error: /bin/bash: -c: line 11: syntax error near unexpected token `('). Manually confirm ability configs under plugins/woocommerce define 'route' values that begin with '/' and that each route corresponds to its controller's rest_base (see register_controller_abilities in plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php).

@budzanowski
Copy link
Contributor Author

@peterfabian

I can see this is being initialized on all requests, even for favicon or cron requests. Is it possible to limit this?

Initialization moved to rest_api_init

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1)

88-116: Type‑check adapter and localize server strings.

Avoid relying on exceptions for flow control and ensure UI strings are translatable.

Apply this diff:

 public function initialize_mcp_server( $adapter ): void {
+		// Ensure adapter exposes the expected API.
+		if ( ! is_object( $adapter ) || ! method_exists( $adapter, 'create_server' ) ) {
+			return;
+		}
@@
-			$adapter->create_server(
+			$adapter->create_server(
 				'woocommerce-mcp',
 				'woocommerce',
 				'mcp',
-				'WooCommerce MCP Server',
-				'AI-accessible WooCommerce operations via MCP',
+				__( 'WooCommerce MCP Server', 'woocommerce' ),
+				__( 'AI-accessible WooCommerce operations via MCP', 'woocommerce' ),
 				'1.0.0',
 				array( WooCommerceRestTransport::class ),
 				\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
 				\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
 				$abilities_ids,
 			);
🧹 Nitpick comments (2)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (2)

98-101: Temporarily disabling validation: verify blast radius and track upstream.

Disabling mcp_validation_enabled globally (even briefly) could affect other MCP servers created in the same request. If the filter supports context args, gate it by server id; otherwise, keep as is but link the upstream issue in the TODO so we know when to remove.

Could you add a reference to the upstream validator bug (issue URL/ID) in the TODO and confirm whether the mcp_validation_enabled filter receives arguments that would allow scoping?

Also applies to: 124-127


117-123: Enrich error log context.

Include exception class/code and useful context to speed up debugging.

Apply this diff:

-			if ( function_exists( 'wc_get_logger' ) ) {
-				wc_get_logger()->error(
-					'MCP server initialization failed: ' . $e->getMessage(),
-					array( 'source' => 'woocommerce-mcp' )
-				);
-			}
+			if ( function_exists( 'wc_get_logger' ) ) {
+				wc_get_logger()->error(
+					sprintf( 'MCP server initialization failed: [%s:%s] %s', get_class( $e ), (string) $e->getCode(), $e->getMessage() ),
+					array(
+						'source'          => 'woocommerce-mcp',
+						'server_id'       => 'woocommerce-mcp',
+						'abilities_count' => is_array( $abilities_ids ) ? count( $abilities_ids ) : 0,
+					)
+				);
+			}
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a80ebf and 9a0fe8d.

📒 Files selected for processing (1)
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
🧠 Learnings (1)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
🧬 Code graph analysis (1)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (7)
plugins/woocommerce/includes/class-woocommerce.php (3)
  • WooCommerce (45-1643)
  • __construct (251-256)
  • instance (160-165)
plugins/woocommerce/src/Utilities/FeaturesUtil.php (1)
  • FeaturesUtil (14-102)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (3)
  • AbilitiesRegistry (18-50)
  • __construct (23-25)
  • getAbilitiesIDs (40-49)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
  • WooCommerceRestTransport (22-180)
  • __construct (34-39)
plugins/woocommerce/src/Internal/Features/FeaturesController.php (2)
  • __construct (131-167)
  • feature_is_enabled (718-730)
plugins/woocommerce/woocommerce.php (1)
  • wc_get_container (57-59)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_get_logger (2122-2152)
🔇 Additional comments (5)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (5)

47-60: Good gating and idempotency.

Feature flag check + per‑request initialized guard are appropriate.


66-73: Adapter boot approach is sound.

class_exists guard + instance bootstrap is safe in REST context.


93-97: Bail on empty abilities: good defensive check.

Prevents spinning up a server with no abilities.


124-127: Nice use of finally for cleanup.

Guarantees validation is re‑enabled even on failure.


40-41: Adapter uses default priority 10 — original assumption is incorrect.

WP\MCP\Core\McpAdapter registers its rest_api_init callback at priority 10 and dispatches mcp_adapter_init from that callback; our maybe_initialize also hooks at priority 10, so execution order is determined by hook registration / plugin load order. Make the order explicit (adjust priorities or verify load order) if maybe_initialize depends on mcp_adapter_init.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1)

62-67: Route‑level permission enforced. LGTM.

Permission is checked via filter before execution; addresses earlier feedback.

🧹 Nitpick comments (6)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (6)

10-10: Remove unused import.

WooCommerceRestTransport isn’t referenced in this file.

- use Automattic\WooCommerce\Internal\MCP\Transport\WooCommerceRestTransport;

27-39: Validate config and normalize route upfront.

Harden against missing keys and malformed routes; avoid notices and 404s.

 public static function register_controller_abilities( array $config ): void {
-	$controller_class = $config['controller'];
+	if ( empty( $config['controller'] ) || empty( $config['abilities'] ) || empty( $config['route'] ) || ! is_array( $config['abilities'] ) ) {
+		return;
+	}
+	$controller_class = $config['controller'];
 
 	if ( ! class_exists( $controller_class ) ) {
 		return;
 	}
 
 	$controller = new $controller_class();
 
-	foreach ( $config['abilities'] as $ability_config ) {
-		self::register_single_ability( $controller, $ability_config, $config['route'] );
+	$route = '/' . ltrim( (string) $config['route'], '/' );
+	foreach ( $config['abilities'] as $ability_config ) {
+		self::register_single_ability( $controller, $ability_config, $route );
 	}
 }

55-75: Prefer WooCommerce logger over error_log.

Use wc_get_logger() with a context; avoids noisy PHP error logs and integrates with WC logging.

-		} catch ( \Throwable $e ) {
-			// Log the error for debugging but don't break the registration of other abilities
-			error_log( "Failed to register ability {$ability_config['id']}: " . $e->getMessage() );
-		}
+		} catch ( \Throwable $e ) {
+			if ( function_exists( 'wc_get_logger' ) ) {
+				wc_get_logger()->error(
+					sprintf( 'Failed to register ability %s: %s', $ability_config['id'], $e->getMessage() ),
+					array( 'source' => 'rest-ability-factory' )
+				);
+			}
+		}

156-215: Harden arg sanitation against unexpected shapes.

Guard non‑array args; only copy array enums; avoid leaking unknown nested keys.

 private static function sanitize_args_to_schema( array $args ): array {
 	$properties = array();
 	$required   = array();
 
 	foreach ( $args as $key => $arg ) {
-		$property = array();
+		if ( ! is_array( $arg ) ) {
+			continue;
+		}
+		$property = array();
 
 		// Copy valid JSON Schema fields
 		if ( isset( $arg['type'] ) ) {
-			$property['type'] = $arg['type'];
+			$property['type'] = $arg['type'];
 		}
 		if ( isset( $arg['description'] ) ) {
 			$property['description'] = $arg['description'];
 		}
 		if ( isset( $arg['default'] ) ) {
 			$property['default'] = $arg['default'];
 		}
-		if ( isset( $arg['enum'] ) ) {
-			$property['enum'] = array_values( $arg['enum'] );
+		if ( isset( $arg['enum'] ) && is_array( $arg['enum'] ) ) {
+			$property['enum'] = array_values( $arg['enum'] );
 		}
-		if ( isset( $arg['items'] ) ) {
-			$property['items'] = $arg['items'];
+		if ( isset( $arg['items'] ) && is_array( $arg['items'] ) ) {
+			$property['items'] = $arg['items'];
 		}
 		if ( isset( $arg['minimum'] ) ) {
 			$property['minimum'] = $arg['minimum'];
 		}
 		if ( isset( $arg['maximum'] ) ) {
 			$property['maximum'] = $arg['maximum'];
 		}
 		if ( isset( $arg['format'] ) ) {
 			$property['format'] = $arg['format'];
 		}
-		if ( isset( $arg['properties'] ) ) {
-			$property['properties'] = $arg['properties'];
+		if ( isset( $arg['properties'] ) && is_array( $arg['properties'] ) ) {
+			$property['properties'] = $arg['properties'];
 		}
 
 		// Convert readonly to readOnly (JSON Schema format)
 		if ( isset( $arg['readonly'] ) && $arg['readonly'] ) {
 			$property['readOnly'] = true;
 		}
 
 		// Collect required fields
 		if ( isset( $arg['required'] ) && $arg['required'] === true ) {
 			$required[] = $key;
 		}
 
 		$properties[ $key ] = $property;
 	}
 
 	$schema = array(
 		'type'       => 'object',
 		'properties' => $properties,
 	);
 
 	if ( ! empty( $required ) ) {
-		$schema['required'] = array_unique( $required );
+		$schema['required'] = array_values( array_unique( $required ) );
 	}
 
 	return $schema;
 }

267-297: Remove unused parameters and normalize route when executing.

Silences PHPMD warnings and avoids malformed routes.

-					'execute_callback'    => function( $input ) use ( $controller, $ability_config, $route ) {
-						return self::execute_operation( $controller, $ability_config['operation'], $input, $route );
+					'execute_callback'    => function( $input ) use ( $ability_config, $route ) {
+						return self::execute_operation( $ability_config['operation'], $input, $route );
 					},
-					'permission_callback' => function( $input ) use ( $controller, $ability_config ) {
-						return self::check_permission( $controller, $ability_config['operation'], $input );
+					'permission_callback' => function( $input ) use ( $controller, $ability_config ) {
+						return self::check_permission( $controller, $ability_config['operation'] );
 					},
-	private static function execute_operation( $controller, string $operation, array $input, string $route ) {
+	private static function execute_operation( string $operation, array $input, string $route ) {
 		$method = self::get_http_method_for_operation( $operation );
 
 		// Build final route - add ID for single item operations
-		$request_route = $route;
+		$request_route = '/' . ltrim( $route, '/' );
 		if ( isset( $input['id'] ) && in_array( $operation, array( 'get', 'update', 'delete' ), true ) ) {
 			$request_route .= '/' . intval( $input['id'] );
 			unset( $input['id'] );
 		}
-	private static function check_permission( $controller, string $operation, array $input ): bool {
+	private static function check_permission( $controller, string $operation ): bool {
 		// Get HTTP method for the operation
 		$method = self::get_http_method_for_operation( $operation );

Also applies to: 63-67, 325-337


278-281: Use query params for GET to avoid mixing transport layers.

Minor: for GET, prefer set_query_params for clarity.

-		foreach ( $input as $key => $value ) {
-			$request->set_param( $key, $value );
-		}
+		if ( 'GET' === $method ) {
+			$request->set_query_params( $input );
+		} else {
+			foreach ( $input as $key => $value ) {
+				$request->set_param( $key, $value );
+			}
+		}
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bfbbd3e and fee7d26.

📒 Files selected for processing (1)
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
🧠 Learnings (1)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
🧬 Code graph analysis (1)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (2)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
  • WooCommerceRestTransport (22-180)
  • check_permission (47-49)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1)
  • RestAbility (20-37)
🪛 PHPMD (2.15.0)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php

267-267: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)


325-325: Avoid unused parameters such as '$input'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (4)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (4)

84-142: Input schema derivation looks sound.

Ops map correctly; update includes required id; sensible fallback.


224-256: Output schema mapping is reasonable.

List wrapping and delete confirmation shape make sense; fallback is safe.


328-336: Confirm filter ordering so abilities aren’t always denied.

apply_filters(..., false, ...) means deny by default; ensure WooCommerceRestTransport is constructed (and its filter registered) before these ability checks execute in MCP context.


34-34: Confirm zero‑arg controllers.
Automated checks couldn't find the controller class definitions in this repo (they appear to be WooCommerce core classes). Verify every controller registered via plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php can be instantiated with new $controller_class() (instantiation at plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php:34); if any require constructor args, replace with a factory or inject dependencies.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (5)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (5)

70-76: Localize error message for missing API key.

The error message for missing API key is not localized, while other error messages in this file are using the __() function.

Apply this diff to localize the error message:

 		if ( empty( $api_key ) ) {
 			return new \WP_Error(
 				'missing_api_key',
-				'X-MCP-API-Key header required. Format: consumer_key:consumer_secret',
+				__( 'X-MCP-API-Key header required. Format: consumer_key:consumer_secret', 'woocommerce' ),
 				array( 'status' => 401 )
 			);
 		}

121-137: Localize and unify error messages for invalid credentials.

The error messages expose whether the consumer key or secret is invalid, which could aid attackers. Consider using a generic message for both cases. Also, these messages should be localized.

Apply this diff to unify and localize error messages:

 		// Check if user data was found
 		if ( empty( $user_data ) ) {
 			return new \WP_Error(
 				'invalid_consumer_key',
-				'Consumer key is invalid.',
+				__( 'Invalid credentials.', 'woocommerce' ),
 				array( 'status' => 401 )
 			);
 		}
 
 		// Validate consumer secret using hash_equals for timing attack protection
 		if ( ! hash_equals( $user_data->consumer_secret, $consumer_secret ) ) {
 			return new \WP_Error(
 				'invalid_consumer_secret',
-				'Consumer secret is invalid.',
+				__( 'Invalid credentials.', 'woocommerce' ),
 				array( 'status' => 401 )
 			);
 		}

131-131: Apply trimming to consumer_secret for consistency.

The consumer_secret should also be trimmed (but not sanitized) before comparison to ensure consistency with how the consumer_key is handled.

Apply this diff:

 		// Validate consumer secret using hash_equals for timing attack protection
-		if ( ! hash_equals( $user_data->consumer_secret, $consumer_secret ) ) {
+		if ( ! hash_equals( $user_data->consumer_secret, trim( (string) $consumer_secret ) ) ) {

108-109: Avoid sanitizing API keys as it may alter their values.

Using sanitize_text_field() on the consumer key can potentially alter the key value (e.g., stripping special characters), which would cause valid keys to fail authentication. API keys should only be trimmed to remove whitespace.

Apply this diff to only trim the consumer key:

 		// Hash the consumer key as WooCommerce does
-		$hashed_consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
+		$hashed_consumer_key = wc_api_hash( trim( (string) $consumer_key ) );

139-145: Validate API key permissions and ensure user exists before setting context.

The code should verify that:

  1. The API key has appropriate permissions for MCP (at least 'read')
  2. The user associated with the API key still exists in the system

Apply this diff to add permission and user validation:

 		// Store permissions for tool-level checking
 		self::$current_mcp_permissions = $user_data->permissions;
 
+		// Enforce minimal permissions for MCP (filterable).
+		$allowed_permissions = apply_filters( 'woocommerce_mcp_allowed_key_permissions', array( 'read', 'read_write' ) );
+		if ( ! in_array( $user_data->permissions, $allowed_permissions, true ) ) {
+			return new \WP_Error(
+				'insufficient_api_key_permissions',
+				__( 'The API key does not grant required permissions for MCP.', 'woocommerce' ),
+				array( 'status' => 403 )
+			);
+		}
+
+		// Ensure the user exists before switching context.
+		$user = get_user_by( 'id', (int) $user_data->user_id );
+		if ( ! $user ) {
+			return new \WP_Error(
+				'mcp_user_not_found',
+				__( 'The user associated with this API key no longer exists.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);
+		}
+
 		// Set the current user
-		wp_set_current_user( $user_data->user_id );
+		wp_set_current_user( $user->ID );
🧹 Nitpick comments (3)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (3)

47-49: Consider adding proper parameter typing to check_permission method.

The method accepts $request = null but then passes it directly to validate_request which expects a typed \WP_REST_Request parameter. This could cause a runtime error if null is passed.

Apply this diff to add proper typing:

-	public function check_permission( $request = null ) {
+	public function check_permission( \WP_REST_Request $request ) {
 		return $this->validate_request( $request );
 	}

78-84: Localize error message for invalid API key format.

The error message for invalid API key format is not localized.

Apply this diff to localize the error message:

 		if ( strpos( $api_key, ':' ) === false ) {
 			return new \WP_Error(
 				'invalid_api_key',
-				'X-MCP-API-Key must be in format consumer_key:consumer_secret',
+				__( 'X-MCP-API-Key must be in format consumer_key:consumer_secret', 'woocommerce' ),
 				array( 'status' => 401 )
 			);
 		}

165-187: Consider using the $controller parameter or documenting why it's unused.

The static analysis tool flagged that $controller parameter is unused. While this may be intentional (as permissions are based purely on HTTP method and API key permissions), you might want to use it for more granular permission checks in the future.

Consider either:

  1. Adding a comment explaining why $controller is intentionally unused
  2. Using it for more granular permission checks based on the specific REST controller

Example documentation:

/**
 * Check REST ability permissions for HTTP method.
 *
 * @param bool   $allowed    Whether the operation is allowed. Default false.
 * @param string $method     HTTP method (GET, POST, PUT, DELETE).
 * @param object $controller REST controller instance (unused - permissions based on method only).
 * @return bool Whether permission is granted.
 */
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fee7d26 and b06a0fb.

📒 Files selected for processing (1)
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
🧠 Learnings (2)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
📚 Learning: 2025-09-17T16:16:48.098Z
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.

Applied to files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
🧬 Code graph analysis (1)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1)
  • check_permission (325-337)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_api_hash (1437-1439)
🪛 PHPMD (2.15.0)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php

165-165: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (1)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1)

58-65: LGTM! Proper TLS enforcement with development override.

The implementation correctly requires HTTPS by default while providing a filter for local development environments.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (5)

24-31: Add a scoped callback property to avoid clobbering third‑party filters.

We add/remove a global __return_false callback later; keeping our own callable avoids removing someone else’s identical callback.

Apply this diff:

 class MCPAdapterProvider {

 	/**
 	 * Whether MCP adapter is initialized.
 	 *
 	 * @var bool
 	 */
 	private bool $initialized = false;
+
+	/**
+	 * Scoped callback used to temporarily disable MCP validation.
+	 * @var callable|null
+	 */
+	private $validation_disable_cb = null;

65-73: Log when MCP adapter class is unavailable.

Currently this fails silently; logging helps diagnose missing deps/config.

Apply this diff:

 	private function initialize_mcp_adapter(): void {
 		// Check if MCP adapter class exists (should be autoloaded by WooCommerce's composer)
 		if ( ! class_exists( 'WP\MCP\Core\McpAdapter' ) ) {
-			return;
+			if ( function_exists( 'wc_get_logger' ) ) {
+				wc_get_logger()->warning(
+					'MCP adapter class not found. Skipping MCP initialization.',
+					array( 'source' => 'woocommerce-mcp' )
+				);
+			}
+			return;
 		}

97-101: Don’t remove other plugins’ mcp_validation_enabled filters.

Using __return_false risks removing someone else’s identical callback at the same priority. Use a scoped closure stored on the instance.

Apply this diff:

-		// Temporarily disable MCP validation during server creation
-		// Workaround for validator bug with union types (e.g., ["integer", "null"])
-		// TODO: Remove once mcp-adapter validator bug is fixed
-		add_filter( 'mcp_validation_enabled', '__return_false', 999 );
+		// Temporarily disable MCP validation during server creation (workaround for union-types bug).
+		// Use an instance-scoped callback to avoid removing third-party filters.
+		$this->validation_disable_cb = static function (): bool {
+			return false;
+		};
+		add_filter( 'mcp_validation_enabled', $this->validation_disable_cb, 999 );
@@
-			// Re-enable MCP validation immediately after server creation
-			remove_filter( 'mcp_validation_enabled', '__return_false', 999 );
+			// Re-enable MCP validation immediately after server creation
+			if ( null !== $this->validation_disable_cb ) {
+				remove_filter( 'mcp_validation_enabled', $this->validation_disable_cb, 999 );
+				$this->validation_disable_cb = null;
+			}

Also applies to: 124-126


102-116: Type-check the adapter and localize server labels.

  • Add a lightweight type/method check (defensive; avoids hard errors if the hook is misused).
  • Wrap name/description with __() for i18n.

Apply this diff:

-	public function initialize_mcp_server( $adapter ): void {
+	public function initialize_mcp_server( $adapter ): void {
 		// Get filtered abilities for MCP server
 		$abilities_ids = $this->get_woocommerce_mcp_abilities();
@@
-		try {
+		try {
+			// Ensure adapter has the expected API.
+			if ( ! is_object( $adapter ) || ! method_exists( $adapter, 'create_server' ) ) {
+				if ( function_exists( 'wc_get_logger' ) ) {
+					wc_get_logger()->error(
+						'MCP server initialization failed: invalid adapter instance (missing create_server).',
+						array( 'source' => 'woocommerce-mcp' )
+					);
+				}
+				return;
+			}
 			// Create MCP server
 			$adapter->create_server(
 				'woocommerce-mcp',
 				'woocommerce',
 				'mcp',
-				'WooCommerce MCP Server',
-				'AI-accessible WooCommerce operations via MCP',
+				__( 'WooCommerce MCP Server', 'woocommerce' ),
+				__( 'AI-accessible WooCommerce operations via MCP', 'woocommerce' ),
 				'1.0.0',
 				array( WooCommerceRestTransport::class ),
 				\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
 				\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
 				$abilities_ids,
 			);

1-166: Add tests for key flows.

  • Feature flag gating: ensure no init when disabled; init when enabled on REST only.
  • Validation filter scoping: assert filter state is restored after exceptions.
  • Abilities filtering: only woocommerce/ by default; custom include via filter.
  • Logging: assert error is logged on invalid adapter.

I can scaffold PHPUnit tests targeting these behaviors if helpful.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7afb192 and 3bd7780.

📒 Files selected for processing (1)
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
🧠 Learnings (1)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
🧬 Code graph analysis (1)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (5)
plugins/woocommerce/includes/class-woocommerce.php (2)
  • WooCommerce (45-1643)
  • __construct (251-256)
plugins/woocommerce/src/Utilities/FeaturesUtil.php (1)
  • FeaturesUtil (14-102)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (3)
  • AbilitiesRegistry (18-49)
  • __construct (23-25)
  • getAbilitiesIDs (39-48)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_get_logger (2122-2152)
plugins/woocommerce/woocommerce.php (1)
  • wc_get_container (57-59)
🔇 Additional comments (8)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (8)

6-15: Good: strict types + ABSPATH guard.

Solid baseline for safety and WP compatibility.


10-13: Imports look correct and minimal.

Only what’s needed is imported; nice.


34-41: Initializing on rest_api_init is the right call.

Prevents unnecessary boot on non‑REST hits and addresses the earlier review thread.


46-60: Feature flag + single‑shot guard: LGTM.

Gates the integration and prevents duplicate init within the request.


78-81: Hook wiring is appropriate.

Ties server setup to adapter readiness.


88-96: Bailing on empty abilities is correct.

Prevents creating a server with no abilities.


163-165: Public probe method is helpful.

Useful for integration tests and diagnostics.


137-156: No action required — str_starts_with is safe here

WooCommerce requires PHP 7.4+ and WordPress provides polyfills for PHP 8 string helpers (str_starts_with/str_contains via wp-includes/compat.php), so the namespace filter can remain as written.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1)

103-106: Prefer runtime/feature detection instead of a version check for removing this workaround

composer.json requires wordpress/mcp-adapter: "dev-trunk" (composer.lock also lists version "dev-trunk" / commit 95981e865bde0424a177dacb6fb06be03cc76320), so there is no stable package version to reliably check. Replace the TODO/version-check suggestion with a runtime approach: if the adapter exposes a VERSION constant or get_version() accessor, use that; otherwise add a small validator compatibility test at runtime (e.g., attempt a minimal union-type validation) and only disable mcp validation when that test fails. Keep the existing try/finally that re-enables validation.
Location: plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (around the add_filter at ~lines 103–106).

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bd7780 and 6c83b84.

📒 Files selected for processing (1)
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
🧠 Learnings (1)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
🧬 Code graph analysis (1)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (6)
plugins/woocommerce/includes/class-woocommerce.php (3)
  • WooCommerce (45-1643)
  • __construct (251-256)
  • instance (160-165)
plugins/woocommerce/src/Utilities/FeaturesUtil.php (1)
  • FeaturesUtil (14-102)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (3)
  • AbilitiesRegistry (18-49)
  • __construct (23-25)
  • getAbilitiesIDs (39-48)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
  • WooCommerceRestTransport (22-189)
  • __construct (34-39)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_get_logger (2122-2152)
plugins/woocommerce/woocommerce.php (1)
  • wc_get_container (57-59)
🪛 PHPMD (2.15.0)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php

172-174: Avoid unused private methods such as 'disable_mcp_validation'. (undefined)

(UnusedPrivateMethod)

🔇 Additional comments (7)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (7)

34-41: LGTM! Well-structured hook registration with performance optimization.

The constructor correctly hooks into rest_api_init with priority 10 to ensure MCP initialization only occurs on REST API requests, preventing unnecessary overhead on other requests like favicon or cron jobs. The priority ordering ensures proper sequencing with the MCP adapter's own initialization.


46-60: LGTM! Proper feature flag gating and initialization flow.

The initialization logic correctly:

  • Guards against feature flag disabled state
  • Prevents double initialization
  • Follows a clean initialization sequence

65-79: LGTM! Robust class existence checking and error handling.

The method properly validates the MCP adapter class exists before attempting to use it, with appropriate logging when the class is not found. The singleton pattern usage is correct for triggering the adapter's initialization.


94-133: LGTM! Comprehensive server initialization with proper error handling.

The server initialization method includes:

  • Early return for empty abilities
  • Temporary validation disable workaround for known MCP adapter bug
  • Proper exception handling with logging
  • Clean filter removal in finally block
  • Localized strings for UI elements

The implementation addresses all the concerns from the previous review.


143-162: LGTM! Well-designed ability filtering with extensibility.

The method provides a clean separation of concerns:

  • Gets all abilities from the registry
  • Filters by namespace (woocommerce/) by default
  • Allows override via the woocommerce_mcp_include_ability filter
  • Re-indexes the array properly

172-174: Method is used as a filter callback.

The static analysis tool incorrectly flagged this as unused. The disable_mcp_validation method is used as a callback for the mcp_validation_enabled filter on Line 106.


181-183: LGTM! Simple state accessor method.

Provides a clean way to check initialization state externally.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c83b84 and 6e86925.

📒 Files selected for processing (1)
  • plugins/woocommerce/changelog/add-mcp-to-woocommerce (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
📚 Learning: 2025-09-17T16:16:48.098Z
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.

Applied to files:

  • plugins/woocommerce/changelog/add-mcp-to-woocommerce

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (2)

103-107: Document the temporary workaround with specific details.

The comment mentions a "validator bug with union types" but lacks specifics about the issue or expected resolution timeline.

-		// Temporarily disable MCP validation during server creation
-		// Workaround for validator bug with union types (e.g., ["integer", "null"])
-		// TODO: Remove once mcp-adapter validator bug is fixed
+		// Temporarily disable MCP validation during server creation
+		// Workaround for MCP adapter validator incorrectly rejecting valid union types
+		// like ["integer", "null"] in JSON Schema validation
+		// TODO: Remove once wordpress/mcp-adapter issue is resolved

148-162: Consider caching filtered abilities for performance.

The abilities filtering runs every time the server is initialized. For better performance, consider caching the filtered results if abilities don't change frequently during a request cycle.

+	/**
+	 * Cached MCP abilities.
+	 *
+	 * @var array|null
+	 */
+	private ?array $cached_mcp_abilities = null;
+
 	/**
 	 * 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.
 	 *
 	 * @return array Array of ability IDs for MCP server.
 	 */
 	private function get_woocommerce_mcp_abilities(): array {
+		// Return cached abilities if available
+		if ( $this->cached_mcp_abilities !== null ) {
+			return $this->cached_mcp_abilities;
+		}
+
 		// Get all abilities from the registry
 		$abilities_registry = wc_get_container()->get( AbilitiesRegistry::class );
 		$all_abilities_ids = $abilities_registry->getAbilitiesIDs();
 
 		// Filter abilities based on namespace and custom filter
 		$mcp_abilities = array_filter(
 			$all_abilities_ids,
 			function( $ability_id ) {
 				// Include WooCommerce abilities by default
 				$include = str_starts_with( $ability_id, 'woocommerce/' );
 
 				// Allow filter to override inclusion decision
 				return apply_filters( 'woocommerce_mcp_include_ability', $include, $ability_id );
 			}
 		);
 
-		// Re-index array
-		return array_values( $mcp_abilities );
+		// Re-index array and cache result
+		$this->cached_mcp_abilities = array_values( $mcp_abilities );
+		return $this->cached_mcp_abilities;
 	}
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e86925 and f086674.

⛔ Files ignored due to path filters (1)
  • plugins/woocommerce/composer.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • plugins/woocommerce/composer.json (2 hunks)
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugins/woocommerce/composer.json
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
🧠 Learnings (1)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
🧬 Code graph analysis (1)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (6)
plugins/woocommerce/includes/class-woocommerce.php (3)
  • WooCommerce (45-1643)
  • __construct (251-256)
  • instance (160-165)
plugins/woocommerce/src/Utilities/FeaturesUtil.php (1)
  • FeaturesUtil (14-102)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (3)
  • AbilitiesRegistry (18-49)
  • __construct (23-25)
  • getAbilitiesIDs (39-48)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
  • WooCommerceRestTransport (22-189)
  • __construct (34-39)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_get_logger (2122-2152)
plugins/woocommerce/woocommerce.php (1)
  • wc_get_container (57-59)
🔇 Additional comments (2)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (2)

65-79: LGTM! Improved error handling and class existence check.

The implementation properly guards against missing dependencies and provides appropriate logging for debugging purposes.


94-133: Verify the MCP adapter parameter type and validate abilities filtering.

The method accepts an untyped $adapter parameter but calls methods on it without validation. Additionally, the abilities filtering logic should be verified to ensure it works correctly with the current abilities structure.

@budzanowski budzanowski force-pushed the add/mcp-to-woocommerce branch from f086674 to c216491 Compare September 22, 2025 09:46
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (8)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)

70-84: i18n and stricter header validation for X‑MCP‑API‑Key.

Localize messages and ensure both parts are present/non-empty.

-		if ( empty( $api_key ) ) {
+		if ( empty( $api_key ) ) {
 			return new \WP_Error(
 				'missing_api_key',
-				'X-MCP-API-Key header required. Format: consumer_key:consumer_secret',
+				__( 'X-MCP-API-Key header required. Format: consumer_key:consumer_secret', 'woocommerce' ),
 				array( 'status' => 401 )
 			);
 		}
 
-		if ( strpos( $api_key, ':' ) === false ) {
+		if ( strpos( $api_key, ':' ) === false ) {
 			return new \WP_Error(
 				'invalid_api_key',
-				'X-MCP-API-Key must be in format consumer_key:consumer_secret',
+				__( 'X-MCP-API-Key must be in format consumer_key:consumer_secret', 'woocommerce' ),
 				array( 'status' => 401 )
 			);
 		}
 
-		list( $consumer_key, $consumer_secret ) = explode( ':', $api_key, 2 );
+		list( $consumer_key, $consumer_secret ) = explode( ':', $api_key, 2 );
+		if ( '' === trim( (string) $consumer_key ) || '' === trim( (string) $consumer_secret ) ) {
+			return new \WP_Error(
+				'invalid_api_key',
+				__( 'X-MCP-API-Key must include both consumer key and secret.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);
+		}

165-187: Silence PHPMD: $controller is unused here.

No behavior change.

 	public function check_ability_permission( $allowed, $method, $controller ) {
+		// $controller is part of the filter signature; not used here.
+		unset( $controller );
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (5)

27-39: Guard required config and derive route when missing.

Avoid fatals when 'route' isn’t provided.

 	public static function register_controller_abilities( array $config ): void {
 		$controller_class = $config['controller'];
 
 		if ( ! class_exists( $controller_class ) ) {
 			return;
 		}
 
-		$controller = new $controller_class();
+		$controller = new $controller_class();
+		$route      = $config['route'] ?? null;
+		if ( ! $route && property_exists( $controller, 'namespace' ) && property_exists( $controller, 'rest_base' ) ) {
+			$route = '/' . ltrim( (string) $controller->namespace, '/' ) . '/' . trim( (string) $controller->rest_base, '/' );
+		}
+		if ( ! $route ) {
+			return;
+		}
 
-		foreach ( $config['abilities'] as $ability_config ) {
-			self::register_single_ability( $controller, $ability_config, $config['route'] );
+		foreach ( $config['abilities'] as $ability_config ) {
+			self::register_single_ability( $controller, $ability_config, $route );
 		}
 	}

71-75: Prefer wc_get_logger over error_log.

Aligns with WooCommerce logging and respects filters.

-		} catch ( \Throwable $e ) {
-			// Log the error for debugging but don't break the registration of other abilities
-			error_log( "Failed to register ability {$ability_config['id']}: " . $e->getMessage() );
+		} catch ( \Throwable $e ) {
+			if ( function_exists( 'wc_get_logger' ) ) {
+				wc_get_logger()->error(
+					"Failed to register ability {$ability_config['id']}: " . $e->getMessage(),
+					array( 'source' => 'woocommerce-mcp' )
+				);
+			}
 		}

271-275: Normalize route formatting.

Ensure a leading slash and strip trailing slashes to avoid dispatch edge cases.

-		$request_route = $route;
+		$request_route = '/' . ltrim( (string) $route, '/' );
+		$request_route = rtrim( $request_route, '/' );

267-299: Remove unused $controller parameter from execute_operation (or unset).

Either drop it and update the call site, or unset to satisfy static analysis.

-	private static function execute_operation( $controller, string $operation, array $input, string $route ) {
+	private static function execute_operation( string $operation, array $input, string $route ) {

And update the caller:

-					'execute_callback'    => function( $input ) use ( $controller, $ability_config, $route ) {
-						return self::execute_operation( $controller, $ability_config['operation'], $input, $route );
+					'execute_callback'    => function( $input ) use ( $ability_config, $route ) {
+						return self::execute_operation( $ability_config['operation'], $input, $route );
 					},

If you prefer not to change the signature now:

+		// $controller is unused; keep signature for BC.
+		unset( $controller );

325-337: Silence PHPMD: $input unused in check_permission.

No behavior change.

 	private static function check_permission( $controller, string $operation, array $input ): bool {
+		unset( $input );
 		// Get HTTP method for the operation
 		$method = self::get_http_method_for_operation( $operation );
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1)

143-162: Feature-scope filter looks good; minor: tolerate non-string IDs.

Cast ability_id to string before str_starts_with to avoid accidental warnings.

-			function( $ability_id ) {
+			function( $ability_id ) {
+				$ability_id = (string) $ability_id;
 				// Include WooCommerce abilities by default
-				$include = str_starts_with( $ability_id, 'woocommerce/' );
+				$include = str_starts_with( $ability_id, 'woocommerce/' );
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f086674 and c216491.

⛔ Files ignored due to path filters (3)
  • plugins/woocommerce/bin/composer/phpcs/composer.lock is excluded by !**/*.lock
  • plugins/woocommerce/bin/composer/phpunit/composer.lock is excluded by !**/*.lock
  • plugins/woocommerce/composer.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • plugins/woocommerce/changelog/add-mcp-to-woocommerce (1 hunks)
  • plugins/woocommerce/composer.json (2 hunks)
  • plugins/woocommerce/includes/class-woocommerce.php (2 hunks)
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (1 hunks)
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php (1 hunks)
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1 hunks)
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1 hunks)
  • plugins/woocommerce/src/Internal/Features/FeaturesController.php (1 hunks)
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1 hunks)
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • plugins/woocommerce/changelog/add-mcp-to-woocommerce
🚧 Files skipped from review as they are similar to previous changes (5)
  • plugins/woocommerce/composer.json
  • plugins/woocommerce/src/Internal/Features/FeaturesController.php
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php
  • plugins/woocommerce/includes/class-woocommerce.php
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
🧠 Learnings (2)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
📚 Learning: 2025-09-17T16:16:48.098Z
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.

Applied to files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
🧬 Code graph analysis (3)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (6)
plugins/woocommerce/includes/class-woocommerce.php (3)
  • WooCommerce (45-1645)
  • __construct (251-256)
  • instance (160-165)
plugins/woocommerce/src/Utilities/FeaturesUtil.php (1)
  • FeaturesUtil (14-102)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (3)
  • AbilitiesRegistry (18-49)
  • __construct (23-25)
  • getAbilitiesIDs (39-48)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
  • WooCommerceRestTransport (22-189)
  • __construct (34-39)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_get_logger (2122-2152)
plugins/woocommerce/woocommerce.php (1)
  • wc_get_container (57-59)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (2)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
  • WooCommerceRestTransport (22-189)
  • check_permission (47-49)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1)
  • RestAbility (20-37)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1)
  • check_permission (325-337)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_api_hash (1437-1439)
🪛 PHPMD (2.15.0)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php

267-267: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)


325-325: Avoid unused parameters such as '$input'. (undefined)

(UnusedFormalParameter)

plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php

33-33: Avoid unused parameters such as '$output'. (undefined)

(UnusedFormalParameter)

plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php

165-165: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (22)
  • GitHub Check: Blocks e2e tests 10/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 7/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 5/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 8/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 3/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 6/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Metrics - @woocommerce/plugin-woocommerce [performance]
  • GitHub Check: Core e2e tests 4/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 6/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 4/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 2/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.3] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Core e2e tests 3/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.3] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Blocks e2e tests 1/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 5/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 2/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core API tests - @woocommerce/plugin-woocommerce [api]
  • GitHub Check: Core e2e tests 1/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Lint - @woocommerce/plugin-woocommerce
  • GitHub Check: build
🔇 Additional comments (5)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1)

33-36: Silence PHPMD unused param warning in validate_output.

Unset the parameter to avoid false positives without changing behavior.

 	protected function validate_output( $output ) {
-		// Skip validation - trust that REST controllers return valid data
+		// Skip validation - trust that REST controllers return valid data.
+		unset( $output ); // silence static analysis (unused param)
 		return true;
 	}
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1)

34-41: Nice: initialization moved to rest_api_init.

This limits MCP setup to REST traffic and addresses prior concerns. Please confirm admin‑area flows (e.g., tools hitting REST internally) still work as expected.

plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (3)

139-146: Enforce minimal key permissions and ensure user exists before switching context.

Prevents orphaned keys and overly permissive access.

 		// Store permissions for tool-level checking
 		self::$current_mcp_permissions = $user_data->permissions;
 
-		// Set the current user
-		wp_set_current_user( $user_data->user_id );
+		// Enforce minimal permissions for MCP (filterable).
+		$allowed_permissions = apply_filters( 'woocommerce_mcp_allowed_key_permissions', array( 'read', 'read_write' ) );
+		if ( ! in_array( $user_data->permissions, $allowed_permissions, true ) ) {
+			return new \WP_Error(
+				'insufficient_api_key_permissions',
+				__( 'The API key does not grant required permissions for MCP.', 'woocommerce' ),
+				array( 'status' => 403 )
+			);
+		}
+
+		// Ensure the user exists before switching context.
+		$user = get_user_by( 'id', (int) $user_data->user_id );
+		if ( ! $user ) {
+			return new \WP_Error(
+				'mcp_user_not_found',
+				__( 'The user associated with this API key no longer exists.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);
+		}
+
+		// Set the current user.
+		wp_set_current_user( $user->ID );

108-110: Do not mutate secrets; trim only. Localize auth errors.

sanitize_text_field() can alter secrets. Use trim casts; keep timing-safe compare.

-		$hashed_consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
+		$hashed_consumer_key = wc_api_hash( trim( (string) $consumer_key ) );
@@
-		if ( empty( $user_data ) ) {
+		if ( empty( $user_data ) ) {
 			return new \WP_Error(
 				'invalid_consumer_key',
-				'Consumer key is invalid.',
+				__( 'Invalid credentials.', 'woocommerce' ),
 				array( 'status' => 401 )
 			);
 		}
@@
-		if ( ! hash_equals( $user_data->consumer_secret, $consumer_secret ) ) {
+		if ( ! hash_equals( $user_data->consumer_secret, trim( (string) $consumer_secret ) ) ) {
 			return new \WP_Error(
 				'invalid_consumer_secret',
-				'Consumer secret is invalid.',
+				__( 'Invalid credentials.', 'woocommerce' ),
 				array( 'status' => 401 )
 			);
 		}

Also applies to: 130-137


47-49: Type-safety: prevent null from reaching validate_request.

As written, a null $request would cause a TypeError in validate_request. Either type-hint here or guard/null-check.

-	public function check_permission( $request = null ) {
-		return $this->validate_request( $request );
-	}
+	public function check_permission( $request = null ) {
+		if ( ! $request instanceof \WP_REST_Request ) {
+			return new \WP_Error(
+				'invalid_request',
+				__( 'Invalid request object.', 'woocommerce' ),
+				array( 'status' => 400 )
+			);
+		}
+		return $this->validate_request( $request );
+	}

@budzanowski budzanowski requested a review from a team as a code owner September 22, 2025 11:44
@github-actions github-actions bot added the Documentation Issues and PRs related to improving documentation label Sep 22, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (10)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1)

167-189: Silence unused-parameter warning for $controller

$controller is required by the filter signature but unused here. Unset to satisfy static analysis.

 public function check_ability_permission( $allowed, $method, $controller ) {
+		// Ensure no unused-parameter warnings.
+		unset( $controller );
plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1)

33-36: Address PHPMD unused parameter

Avoid the $output unused-parameter warning.

 	protected function validate_output( $output ) {
-		// Skip validation - trust that REST controllers return valid data.
-		return true;
+		// Skip validation - trust that REST controllers return valid data.
+		unset( $output );
+		return true;
 	}
plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php (1)

27-96: Make configurations filterable to allow extensions to tailor abilities

Expose the configurations array via a filter so sites/plugins can adjust without forking.

-	private static function get_configurations(): array {
-		return array(
+	private static function get_configurations(): array {
+		$configs = array(
 			/* products + orders configs */
-		);
+		);
+		/** Allow extensions to modify/extend exposed abilities. */
+		return apply_filters( 'woocommerce_mcp_rest_bridge_configurations', $configs );
 	}
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (3)

57-60: Hook before initialize to avoid missing the init signal

If the adapter fires mcp_adapter_init during/soon after instantiation, registering the hook after calling instance() risks missing it. Register first, then initialize.

-		$this->initialize_mcp_adapter();
-		$this->register_hooks();
+		$this->register_hooks();
+		$this->initialize_mcp_adapter();
 		$this->initialized = true;

94-101: Type-check the adapter before use

Defensively verify the adapter instance exposes create_server() before calling it.

 	public function initialize_mcp_server( $adapter ): void {
+		if ( ! is_object( $adapter ) || ! method_exists( $adapter, 'create_server' ) ) {
+			return;
+		}

Also applies to: 108-121


148-157: Avoid PHP 8-only str_starts_with; use portable check

Keep BC with environments that might still run <8.0.

-				$include = str_starts_with( $ability_id, 'woocommerce/' );
+				$include = 0 === strpos( (string) $ability_id, 'woocommerce/' );
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (4)

10-10: Remove unused import

WooCommerceRestTransport isn’t referenced in this file.

-use Automattic\WooCommerce\Internal\MCP\Transport\WooCommerceRestTransport;

71-75: Prefer Woo logger over error_log

Use wc_get_logger() for consistency and routing via WC logging settings.

-		} catch ( \Throwable $e ) {
-			// Log the error for debugging but don't break the registration of other abilities.
-			// Log error for debugging purposes.
-			error_log( "Failed to register ability {$ability_config['id']}: " . $e->getMessage() );
+		} catch ( \Throwable $e ) {
+			if ( function_exists( 'wc_get_logger' ) ) {
+				wc_get_logger()->error(
+					"Failed to register ability {$ability_config['id']}: " . $e->getMessage(),
+					array( 'source' => 'woocommerce-mcp' )
+				);
+			}
 		}

268-276: Silence unused $controller in execute_operation

This param isn’t used; rename to underscore or unset to satisfy PHPMD.

-	private static function execute_operation( $controller, string $operation, array $input, string $route ) {
+	private static function execute_operation( $_controller, string $operation, array $input, string $route ) {

326-338: Silence unused $input in check_permission

Avoid PHPMD warning while preserving signature.

 	private static function check_permission( $controller, string $operation, array $input ): bool {
+		unset( $input );
 		// Get HTTP method for the operation.
 		$method = self::get_http_method_for_operation( $operation );
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c8b47d and a9455dc.

📒 Files selected for processing (6)
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (1 hunks)
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php (1 hunks)
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1 hunks)
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1 hunks)
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1 hunks)
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
🧠 Learnings (2)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
📚 Learning: 2025-09-17T16:16:48.098Z
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.

Applied to files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php
🧬 Code graph analysis (4)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (3)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1)
  • __construct (34-41)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1)
  • check_permission (326-338)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_api_hash (1437-1439)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php (3)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (2)
  • RestAbilityFactory (20-339)
  • register_controller_abilities (27-39)
plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php (1)
  • WC_REST_Products_Controller (27-2152)
plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php (1)
  • WC_REST_Orders_Controller (26-505)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (6)
plugins/woocommerce/includes/class-woocommerce.php (3)
  • WooCommerce (45-1645)
  • __construct (251-256)
  • instance (160-165)
plugins/woocommerce/src/Utilities/FeaturesUtil.php (1)
  • FeaturesUtil (14-102)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (3)
  • AbilitiesRegistry (18-49)
  • __construct (23-25)
  • get_abilities_ids (39-48)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
  • WooCommerceRestTransport (22-190)
  • __construct (36-41)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_get_logger (2122-2152)
plugins/woocommerce/woocommerce.php (1)
  • wc_get_container (57-59)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (2)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
  • WooCommerceRestTransport (22-190)
  • check_permission (49-51)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1)
  • RestAbility (20-37)
🪛 PHPMD (2.15.0)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php

167-167: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)

plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php

33-33: Avoid unused parameters such as '$output'. (undefined)

(UnusedFormalParameter)

plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php

268-268: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)


326-326: Avoid unused parameters such as '$input'. (undefined)

(UnusedFormalParameter)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: Blocks e2e tests 8/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 1/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 4/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 1/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 2/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Metrics - @woocommerce/plugin-woocommerce [performance]
  • GitHub Check: Core API tests - @woocommerce/plugin-woocommerce [api]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Core e2e tests 4/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 5/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.3] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.3] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Lint - @woocommerce/plugin-woocommerce
  • GitHub Check: build
🔇 Additional comments (4)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (3)

60-67: TLS gate looks good

Requiring HTTPS by default with a dev override is the right default.


72-86: Harden auth: don’t mutate secrets, avoid credential enumeration, localize, and verify user/permissions

  • Don’t use sanitize_text_field() on credentials; trim only.
  • Localize all error messages.
  • Return a generic “Invalid credentials.” to avoid key/secret enumeration.
  • Ensure associated user exists before switching context.
  • Enforce allowed key permissions (filterable).
@@
-		if ( empty( $api_key ) ) {
-			return new \WP_Error(
-				'missing_api_key',
-				'X-MCP-API-Key header required. Format: consumer_key:consumer_secret',
-				array( 'status' => 401 )
-			);
-		}
+		if ( empty( $api_key ) ) {
+			return new \WP_Error(
+				'missing_api_key',
+				__( 'X-MCP-API-Key header required. Format: consumer_key:consumer_secret.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);
+		}
@@
-		if ( strpos( $api_key, ':' ) === false ) {
-			return new \WP_Error(
-				'invalid_api_key',
-				'X-MCP-API-Key must be in format consumer_key:consumer_secret',
-				array( 'status' => 401 )
-			);
-		}
+		if ( strpos( $api_key, ':' ) === false ) {
+			return new \WP_Error(
+				'invalid_api_key',
+				__( 'X-MCP-API-Key must be in format consumer_key:consumer_secret.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);
+		}
@@
-		$hashed_consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
+		$hashed_consumer_key = wc_api_hash( trim( (string) $consumer_key ) );
@@
-		if ( empty( $user_data ) ) {
-			return new \WP_Error(
-				'invalid_consumer_key',
-				'Consumer key is invalid.',
-				array( 'status' => 401 )
-			);
-		}
+		if ( empty( $user_data ) ) {
+			return new \WP_Error(
+				'invalid_credentials',
+				__( 'Invalid credentials.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);
+		}
@@
-		if ( ! hash_equals( $user_data->consumer_secret, $consumer_secret ) ) {
-			return new \WP_Error(
-				'invalid_consumer_secret',
-				'Consumer secret is invalid.',
-				array( 'status' => 401 )
-			);
-		}
+		if ( ! hash_equals( $user_data->consumer_secret, trim( (string) $consumer_secret ) ) ) {
+			return new \WP_Error(
+				'invalid_credentials',
+				__( 'Invalid credentials.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);
+		}
+
+		// Enforce minimal allowed permissions for MCP (filterable).
+		$allowed_permissions = apply_filters( 'woocommerce_mcp_allowed_key_permissions', array( 'read', 'write', 'read_write' ) );
+		if ( ! in_array( $user_data->permissions, $allowed_permissions, true ) ) {
+			return new \WP_Error(
+				'insufficient_api_key_permissions',
+				__( 'The API key does not grant required permissions for MCP.', 'woocommerce' ),
+				array( 'status' => 403 )
+			);
+		}
+
+		// Ensure the user still exists before switching context.
+		$user = get_user_by( 'id', (int) $user_data->user_id );
+		if ( ! $user ) {
+			return new \WP_Error(
+				'mcp_user_not_found',
+				__( 'The user associated with this API key no longer exists.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);
+		}
@@
-		wp_set_current_user( $user_data->user_id );
+		wp_set_current_user( $user->ID );

Also applies to: 110-139, 141-148


43-51: Require WP_REST_Request param; avoid nullable that can TypeError

validate_request() requires a \WP_REST_Request; passing null here can fatally error. Type-hint and drop the default.

-	 * @param WP_REST_Request|null $request The REST request object.
+	 * @param WP_REST_Request $request The REST request object.
@@
-	public function check_permission( $request = null ) {
+	public function check_permission( WP_REST_Request $request ) {
 		return $this->validate_request( $request );
 	}
plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1)

20-36: Bypassing output validation is acceptable for REST abilities

Given known schema drift in WC REST responses, skipping output validation here is reasonable and aligns with the intended design.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (3)

76-78: Use consistent phrasing and i18n for error messages.

The error message "X-MCP-API-Key header required. Format: consumer_key:consumer_secret" should follow the same localization pattern as other error messages in the validation method, and be consistent with the detailed format error message below.

-			return new \WP_Error(
-				'missing_api_key',
-				'X-MCP-API-Key header required. Format: consumer_key:consumer_secret',
-				array( 'status' => 401 )
-			);
+			return new \WP_Error(
+				'missing_api_key',
+				__( 'X-MCP-API-Key header required. Format: consumer_key:consumer_secret', 'woocommerce' ),
+				array( 'status' => 401 )
+			);

84-86: Use i18n for error message.

The error message should be localized for consistency with other error messages in the file.

-			return new \WP_Error(
-				'invalid_api_key',
-				'X-MCP-API-Key must be in format consumer_key:consumer_secret',
-				array( 'status' => 401 )
-			);
+			return new \WP_Error(
+				'invalid_api_key',
+				__( 'X-MCP-API-Key must be in format consumer_key:consumer_secret', 'woocommerce' ),
+				array( 'status' => 401 )
+			);

128-130: Use i18n for error message.

The error message should be localized for consistency.

-			return new \WP_Error(
-				'invalid_consumer_key',
-				'Consumer key is invalid.',
-				array( 'status' => 401 )
-			);
+			return new \WP_Error(
+				'invalid_consumer_key',
+				__( 'Invalid credentials.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1)

268-299: Unused parameter should be removed from method signature.

The static analysis correctly identifies that the $controller parameter is not used in this method. Since the method relies on rest_do_request() for proper routing and doesn't need direct controller access, this parameter can be removed.

Apply this diff:

-	private static function execute_operation( $controller, string $operation, array $input, string $route ) {
+	private static function execute_operation( string $operation, array $input, string $route ) {

And update the callback in Line 63:

-					'execute_callback'    => function ( $input ) use ( $controller, $ability_config, $route ) {
-						return self::execute_operation( $controller, $ability_config['operation'], $input, $route );
+					'execute_callback'    => function ( $input ) use ( $ability_config, $route ) {
+						return self::execute_operation( $ability_config['operation'], $input, $route );
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7e1649 and 2a64b9a.

📒 Files selected for processing (4)
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1 hunks)
  • plugins/woocommerce/src/Internal/Features/FeaturesController.php (1 hunks)
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1 hunks)
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugins/woocommerce/src/Internal/Features/FeaturesController.php
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
🧠 Learnings (2)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
📚 Learning: 2025-09-17T16:16:48.098Z
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.098Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.

Applied to files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
🧬 Code graph analysis (2)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (2)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
  • WooCommerceRestTransport (22-191)
  • check_permission (49-51)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1)
  • RestAbility (20-37)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (3)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1)
  • __construct (34-41)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1)
  • check_permission (325-338)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_api_hash (1437-1439)
🪛 PHPMD (2.15.0)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php

268-268: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)

plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php

168-168: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.3] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Lint - @woocommerce/plugin-woocommerce
  • GitHub Check: build
🔇 Additional comments (7)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (4)

168-190: LGTM: Well-implemented permission checking logic.

The method correctly maps HTTP methods to API key permissions and only applies MCP-specific logic when in MCP context. The switch statement covers all standard HTTP methods appropriately.


112-112: Critical security issue: sanitize_text_field modifies consumer keys.

Consumer keys stored in the database are SHA256 hashed versions, not the original consumer keys shown to users. Using sanitize_text_field() can modify the actual consumer key value, causing authentication failures since it strips characters and converts to lowercase in some cases.

Apply this diff to preserve the exact consumer key value:

-		// Hash the consumer key as WooCommerce does.
-		$hashed_consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
+		// Hash the consumer key as WooCommerce does (do not mutate value beyond trimming).
+		$hashed_consumer_key = wc_api_hash( trim( (string) $consumer_key ) );

134-140: Critical security issue: sanitization not applied to consumer_secret.

The consumer secret comparison uses the raw value from the header without trimming, which could lead to authentication failures due to whitespace differences. Also localize the error message.

Apply this diff:

-		// Validate consumer secret using hash_equals for timing attack protection.
-		if ( ! hash_equals( $user_data->consumer_secret, $consumer_secret ) ) {
-			return new \WP_Error(
-				'invalid_consumer_secret',
-				'Consumer secret is invalid.',
-				array( 'status' => 401 )
-			);
+		// Validate consumer secret using hash_equals for timing attack protection (trim only).
+		if ( ! hash_equals( $user_data->consumer_secret, trim( (string) $consumer_secret ) ) ) {
+			return new \WP_Error(
+				'invalid_consumer_secret',
+				__( 'Invalid credentials.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);

145-147: Critical security gap: no validation of user existence.

The code sets the current user without verifying the user still exists in the database. If the associated user has been deleted, this could lead to undefined behavior or security issues.

Apply this diff to add user validation:

+		// Ensure the user exists before switching context.
+		$user = get_user_by( 'id', (int) $user_data->user_id );
+		if ( ! $user ) {
+			return new \WP_Error(
+				'mcp_user_not_found',
+				__( 'The user associated with this API key no longer exists.', 'woocommerce' ),
+				array( 'status' => 401 )
+			);
+		}
+
 		// Set the current user.
-		wp_set_current_user( $user_data->user_id );
+		wp_set_current_user( $user->ID );
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (3)

325-338: LGTM: Proper permission delegation to MCP transport.

The permission checking correctly delegates to the MCP transport layer via the woocommerce_check_rest_ability_permissions_for_method filter, which is handled by the WooCommerceRestTransport::check_ability_permission() method. This ensures consistent permission enforcement across the MCP integration.


85-143: LGTM: Comprehensive schema generation with proper sanitization.

The schema generation logic correctly handles different operation types and properly sanitizes WordPress REST arguments to valid JSON Schema format. The fallback schema ensures robustness, and the addition of ID requirements for update operations is appropriate.


157-216: LGTM: Well-implemented JSON Schema sanitization.

The method properly converts WordPress REST API arguments to valid JSON Schema by removing callbacks, normalizing field formats (like readonly to readOnly), and correctly handling the required fields conversion from boolean-per-field to array-of-names format.

budzanowski and others added 8 commits September 28, 2025 08:07
Regenerated lock file to ensure MCP dependencies are properly resolved.
Fix incorrect version change from "dev-main" to "dev-add/mcp-to-woocommerce"
in composer.lock during dependency regeneration. The monorepo-plugin version
should remain as "dev-main" for proper dependency resolution.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Regenerated composer.lock and corrected monorepo-plugin version from
"dev-add/mcp-to-woocommerce" to "dev-trunk" to match trunk branch.
This prevents composer dependency resolution issues.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
…cp_integration feature

Add missing 'default_plugin_compatibility' => FeaturePluginCompatibility::COMPATIBLE
to the mcp_integration feature definition in FeaturesController.php. This resolves
PHPUnit test failures complaining about "Unexpected incorrect usage notice for
add_feature_definition" and "Assuming positive compatibility by default will be
deprecated in the future" warnings.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Refactor MCP feature description logic from inline closure to dedicated
get_mcp_integration_description() method for better code organization
and maintainability. Use direct method call instead of callback for
improved performance and clarity.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Fixes: 'Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space' on line 595.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
* Add comprehensive unit tests for MCP integration

This PR adds extensive test coverage for the WooCommerce Model Context Protocol
(MCP) integration, establishing testing patterns and validating core functionality.

## Test Coverage Added

### MCPAdapterProviderTest (10 tests)
- Feature flag enable/disable logic
- Namespace filtering (woocommerce/ abilities by default)
- Custom filter support for extending ability inclusion
- Initialization state management and double-init prevention
- Error handling for edge cases

### WooCommerceRestTransportTest (15 tests)
- HTTPS enforcement with proper bypass for testing
- API key validation (format, headers, authentication flow)
- Database authentication logic with hash_equals validation
- Permission system (read/write/read_write for HTTP methods)
- User context switching and existence validation
- Security edge cases (invalid credentials, missing users)

## Testing Framework Established

- **Dependency Bootstrapping**: Manual loading of MCP adapter and WordPress Abilities API
- **Feature Flag Testing**: Proper WordPress option-based feature management
- **SSL Bypass Pattern**: Using woocommerce_mcp_allow_insecure_transport filter with clear comments
- **Database Testing**: Safe API key insertion/cleanup patterns
- **Permission Testing**: Reflection-based testing of static properties

## Key Testing Principles Applied

- Test business logic, not external dependencies
- Use proper WordPress testing patterns
- Focus on edge cases and security scenarios
- Clear separation between unit and integration concerns
- Comprehensive coverage without over-testing

**Total: 25 tests, 35+ assertions**

All tests pass and validate the security, authentication, and business logic
of the MCP integration without requiring actual MCP protocol connections.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>

* Update test to use proper McpTransportContext mock

Updates WooCommerceRestTransportTest to match the corrected constructor
signature after fixing the interface compatibility issue.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>

* Fix parameter naming and code formatting in MCP tests

- Change generic $include parameter to descriptive $should_include in filter callback
- Apply WordPress coding standards formatting to test files
- Fix array syntax and spacing consistency
- Add missing trailing newlines

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>

---------

Co-authored-by: Claude <[email protected]>
@budzanowski budzanowski force-pushed the add/mcp-to-woocommerce branch from 95e376e to 73af99e Compare September 28, 2025 06:10
Reset bin/composer/phpcs/composer.lock and bin/composer/phpunit/composer.lock
to match trunk versions to avoid unnecessary diff in the branch.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95e376e and 73af99e.

⛔ Files ignored due to path filters (3)
  • plugins/woocommerce/bin/composer/phpcs/composer.lock is excluded by !**/*.lock
  • plugins/woocommerce/bin/composer/phpunit/composer.lock is excluded by !**/*.lock
  • plugins/woocommerce/composer.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • docs/features/mcp/README.md (1 hunks)
  • docs/features/mcp/_category_.json (1 hunks)
  • plugins/woocommerce/changelog/add-mcp-to-woocommerce (1 hunks)
  • plugins/woocommerce/composer.json (2 hunks)
  • plugins/woocommerce/includes/class-woocommerce.php (2 hunks)
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (1 hunks)
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php (1 hunks)
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1 hunks)
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1 hunks)
  • plugins/woocommerce/src/Internal/Features/FeaturesController.php (2 hunks)
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1 hunks)
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1 hunks)
  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php (1 hunks)
  • plugins/woocommerce/tests/php/src/Internal/MCP/WooCommerceRestTransportTest.php (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • docs/features/mcp/category.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • plugins/woocommerce/includes/class-woocommerce.php
  • docs/features/mcp/README.md
  • plugins/woocommerce/changelog/add-mcp-to-woocommerce
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php
  • plugins/woocommerce/tests/php/src/Internal/MCP/WooCommerceRestTransportTest.php
  • plugins/woocommerce/composer.json
  • plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
  • plugins/woocommerce/src/Internal/Features/FeaturesController.php
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php
  • plugins/woocommerce/src/Internal/Features/FeaturesController.php
  • plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php
  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
  • plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
plugins/woocommerce/tests/**/*.php

📄 CodeRabbit inference engine (.cursor/rules/woo-phpunit.mdc)

plugins/woocommerce/tests/**/*.php: Ensure test classes define public setUp() and tearDown() methods (not protected)
All PHPUnit test methods must be public, not protected
Add declare(strict_types=1); at the top of each test file
Test classes should extend WC_Unit_Test_Case

Files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
plugins/woocommerce/tests/php/src/**/*.php

📄 CodeRabbit inference engine (plugins/woocommerce/CLAUDE.md)

Place PHP unit tests under tests/php/src/

Files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
plugins/woocommerce/tests/php/**

📄 CodeRabbit inference engine (plugins/woocommerce/CLAUDE.md)

Store PHP test data and fixtures under tests/php/

Files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
🧠 Learnings (5)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.140Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
📚 Learning: 2025-09-17T16:16:48.140Z
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.140Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.

Applied to files:

  • plugins/woocommerce/src/Internal/Features/FeaturesController.php
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
📚 Learning: 2025-08-21T07:39:42.952Z
Learnt from: NeosinneR
PR: woocommerce/woocommerce#60520
File: plugins/woocommerce/src/Internal/Admin/EmailPreview/EmailPreview.php:257-259
Timestamp: 2025-08-21T07:39:42.952Z
Learning: In WooCommerce email preview functionality (EmailPreview.php), rel attributes on links should be overwritten rather than preserved for safety reasons, and anchor links (#) can be treated the same as external links since they're not expected in email previews.

Applied to files:

  • plugins/woocommerce/src/Internal/Features/FeaturesController.php
📚 Learning: 2025-08-18T06:11:48.768Z
Learnt from: CR
PR: woocommerce/woocommerce#0
File: .cursor/rules/woo-phpunit.mdc:0-0
Timestamp: 2025-08-18T06:11:48.768Z
Learning: Applies to plugins/woocommerce/tests/**/*.php : Ensure test classes define public setUp() and tearDown() methods (not protected)

Applied to files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
📚 Learning: 2025-09-03T11:50:02.208Z
Learnt from: tpaksu
PR: woocommerce/woocommerce#60735
File: plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Fulfillments/class-wc-rest-fulfillments-v4-controller-tests.php:10-10
Timestamp: 2025-09-03T11:50:02.208Z
Learning: For REST API controller tests in plugins/woocommerce/tests/**/*.php, test classes should extend WC_REST_Unit_Test_Case instead of WC_Unit_Test_Case because REST routes need to be registered before tests run.

Applied to files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
🧬 Code graph analysis (6)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (3)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
  • WooCommerceRestTransport (23-206)
  • check_permission (50-52)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1)
  • RestAbility (20-37)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_get_logger (2122-2152)
plugins/woocommerce/src/Internal/Features/FeaturesController.php (1)
plugins/woocommerce/src/Enums/FeaturePluginCompatibility.php (1)
  • FeaturePluginCompatibility (9-42)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (6)
plugins/woocommerce/includes/class-woocommerce.php (3)
  • WooCommerce (45-1644)
  • __construct (251-256)
  • instance (160-165)
plugins/woocommerce/src/Utilities/FeaturesUtil.php (1)
  • FeaturesUtil (14-102)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (3)
  • AbilitiesRegistry (18-49)
  • __construct (23-25)
  • get_abilities_ids (39-48)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (2)
  • WooCommerceRestTransport (23-206)
  • __construct (37-42)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_get_logger (2122-2152)
plugins/woocommerce/woocommerce.php (1)
  • wc_get_container (57-59)
plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php (5)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (4)
  • MCPAdapterProvider (22-230)
  • maybe_initialize (60-74)
  • is_initialized (206-208)
  • disable_mcp_validation (197-199)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (1)
  • AbilitiesRegistry (18-49)
plugins/woocommerce/src/Utilities/FeaturesUtil.php (1)
  • FeaturesUtil (14-102)
plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php (1)
  • WC_Unit_Test_Case (21-487)
plugins/woocommerce/tests/php/src/Internal/MCP/WooCommerceRestTransportTest.php (2)
  • setUp (27-45)
  • tearDown (50-58)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php (1)
plugins/woocommerce/includes/class-woocommerce.php (1)
  • WooCommerce (45-1644)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (3)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1)
  • __construct (48-55)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1)
  • check_permission (329-342)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_api_hash (1437-1439)
🪛 PHPMD (2.15.0)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php

272-272: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)

plugins/woocommerce/src/Internal/Abilities/REST/RestAbility.php

33-33: Avoid unused parameters such as '$output'. (undefined)

(UnusedFormalParameter)

plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php

183-183: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (26)
  • GitHub Check: Blocks e2e tests 7/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Metrics - @woocommerce/plugin-woocommerce [performance]
  • GitHub Check: Blocks e2e tests 8/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest (HPOS:off) [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Blocks e2e tests 9/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 10/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 6/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 5/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 1/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 4/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 2/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest (HPOS:off) [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Blocks e2e tests 3/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core API tests - @woocommerce/plugin-woocommerce [api]
  • GitHub Check: Core e2e tests 5/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 4/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 6/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 3/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 2/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.3] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.3] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Core e2e tests 1/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Lint - @woocommerce/plugin-woocommerce
  • GitHub Check: build
🔇 Additional comments (3)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1)

50-52: Harden check_permission() against null requests.

check_permission() forwards whatever it receives straight into validate_request(), which has a strict WP_REST_Request type hint. If WordPress (or a future refactor) calls this without a request object, you’ll trigger a fatal TypeError. Add a guard that returns a WP_Error when the argument is not a WP_REST_Request before delegating. As per coding guidelines.

 	public function check_permission( $request = null ) {
-		return $this->validate_request( $request );
+		if ( ! ( $request instanceof \WP_REST_Request ) ) {
+			return new \WP_Error(
+				'invalid_request_context',
+				__( 'Invalid request context for MCP.', 'woocommerce' ),
+				array( 'status' => 400 )
+			);
+		}
+
+		return $this->validate_request( $request );
 	}
plugins/woocommerce/src/Internal/Features/FeaturesController.php (1)

595-617: Escape URLs and harden the MCP description links.

The new description prints filterable URLs directly into <a> attributes and omits the usual rel safeguards. A malicious plugin (or compromised filter) could inject a javascript: payload into those links. Please wrap both URLs with esc_url(), switch the translated strings to their escaped variants, and add rel="noreferrer noopener" to the external link. As per coding guidelines.

-			$permalinks_url    = admin_url( 'options-permalink.php' );
+			$permalinks_url    = esc_url( admin_url( 'options-permalink.php' ) );
 			$permalink_warning = sprintf(
-				'<br><br><strong>%s:</strong> %s <a href="%s">%s</a>',
-				__( 'Configuration Required', 'woocommerce' ),
-				__( 'WordPress permalinks must be set to anything other than "Plain" for MCP to work.', 'woocommerce' ),
-				$permalinks_url,
-				__( 'Configure Permalinks', 'woocommerce' )
+				'<br><br><strong>%s:</strong> %s <a href="%s">%s</a>',
+				esc_html__( 'Configuration Required', 'woocommerce' ),
+				esc_html__( 'WordPress permalinks must be set to anything other than "Plain" for MCP to work.', 'woocommerce' ),
+				$permalinks_url,
+				esc_html__( 'Configure Permalinks', 'woocommerce' )
 			);
 			// Add documentation link to permalink warning.
 			$documentation_link = sprintf(
-				' <a href="%s" target="_blank">%s</a>',
-				'https://github.com/woocommerce/woocommerce/blob/trunk/docs/features/mcp/README.md',
-				__( 'Learn more', 'woocommerce' )
+				' <a href="%s" target="_blank" rel="noreferrer noopener">%s</a>',
+				esc_url( 'https://github.com/woocommerce/woocommerce/blob/trunk/docs/features/mcp/README.md' ),
+				esc_html__( 'Learn more', 'woocommerce' )
 			);
@@
-		$documentation_link = sprintf(
-			' <a href="%s" target="_blank">%s</a>',
-			'https://github.com/woocommerce/woocommerce/blob/trunk/docs/features/mcp/README.md',
-			__( 'Learn more', 'woocommerce' )
+		$documentation_link = sprintf(
+			' <a href="%s" target="_blank" rel="noreferrer noopener">%s</a>',
+			esc_url( 'https://github.com/woocommerce/woocommerce/blob/trunk/docs/features/mcp/README.md' ),
+			esc_html__( 'Learn more', 'woocommerce' )
 		);

Also applies to: 613-617

plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1)

221-229: Fix MCP route detection to prevent spoofing

The substring check on the raw REQUEST_URI flags any REST request that merely contains /woocommerce/mcp, even in query parameters (e.g. GET /wp-json/wc/v3/orders?foo=/woocommerce/mcp). That erroneously treats unrelated REST calls as MCP traffic, so all downstream code that relies on is_mcp_request() (temporary admin context, abilities bridging, etc.) triggers on attacker-controlled requests. Please parse the actual REST route and require an exact /woocommerce/mcp (or sub-route) match before returning true.

-		// Get the request URI.
-		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
-
-		// Build the MCP endpoint path dynamically from constants.
-		$mcp_endpoint = '/' . self::MCP_NAMESPACE . '/' . self::MCP_ROUTE;
-
-		// Check if the request is for the MCP endpoint.
-		return false !== strpos( $request_uri, $mcp_endpoint );
+		$rest_route = '';
+
+		if ( isset( $_GET['rest_route'] ) ) {
+			$rest_route = ltrim( sanitize_text_field( wp_unslash( $_GET['rest_route'] ) ), '/' );
+		} else {
+			$path = wp_parse_url(
+				isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '',
+				PHP_URL_PATH
+			);
+
+			if ( is_string( $path ) ) {
+				$rest_prefix = '/' . rest_get_url_prefix() . '/';
+
+				if ( str_starts_with( $path, $rest_prefix ) ) {
+					$rest_route = substr( $path, strlen( $rest_prefix ) );
+				}
+			}
+		}
+
+		if ( '' === $rest_route ) {
+			return false;
+		}
+
+		$mcp_route = self::MCP_NAMESPACE . '/' . self::MCP_ROUTE;
+
+		return $rest_route === $mcp_route || str_starts_with( $rest_route, $mcp_route . '/' );

budzanowski and others added 3 commits September 28, 2025 11:05
Wrap two WP_Error message strings with __() translation function using
'woocommerce' text domain:
- 'X-MCP-API-Key header required. Format: consumer_key:consumer_secret'
- 'X-MCP-API-Key must be in format consumer_key:consumer_secret'

Error codes and status arrays remain unchanged. Messages are now translatable
for international WooCommerce users.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Add original_abilities_registry property to store original AbilitiesRegistry
- Capture original registry before replacing with mock in setUp()
- Restore original registry in tearDown() and clear stored property
- Ensures subsequent tests see the real AbilitiesRegistry instead of mock

This prevents test interference where the mock registry from one test
could affect other tests that depend on the real registry behavior.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Replace container->resolve() with container->get() since TestingContainer
inherits get() method from RuntimeContainer but doesn't have resolve().

Tests now pass: 25 tests, 37 assertions successful.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1)

183-205: Address unused parameter warning from static analysis.

The $controller parameter is flagged as unused by static analysis. Since this method is a filter callback that must match the expected signature, document this requirement.

 	/**
 	 * Check REST ability permissions for HTTP method.
 	 *
 	 * @param bool   $allowed    Whether the operation is allowed. Default false.
 	 * @param string $method     HTTP method (GET, POST, PUT, DELETE).
-	 * @param object $controller REST controller instance.
+	 * @param object $controller REST controller instance (unused, but required by filter signature).
 	 * @return bool Whether permission is granted.
 	 */
 	public function check_ability_permission( $allowed, $method, $controller ) {
+		// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Required by filter signature.
 		// Only check permissions if we have MCP context.
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73af99e and c0cf067.

📒 Files selected for processing (2)
  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (1 hunks)
  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
🧠 Learnings (2)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.140Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
📚 Learning: 2025-09-17T16:16:48.140Z
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.140Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.

Applied to files:

  • plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php
🧬 Code graph analysis (1)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (3)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (1)
  • __construct (48-55)
plugins/woocommerce/src/Internal/Abilities/REST/RestAbilityFactory.php (1)
  • check_permission (329-342)
plugins/woocommerce/includes/wc-core-functions.php (1)
  • wc_api_hash (1437-1439)
🪛 PHPMD (2.15.0)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php

183-183: Avoid unused parameters such as '$controller'. (undefined)

(UnusedFormalParameter)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (26)
  • GitHub Check: Blocks e2e tests 5/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 10/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 9/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest (HPOS:off) [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Blocks e2e tests 8/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 6/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Metrics - @woocommerce/plugin-woocommerce [performance]
  • GitHub Check: Blocks e2e tests 7/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 4/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 2/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest (HPOS:off) [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Blocks e2e tests 1/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 3/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 6/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 1/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 3/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core API tests - @woocommerce/plugin-woocommerce [api]
  • GitHub Check: Blocks e2e tests 2/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 5/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.3] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Core e2e tests 4/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.3] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Lint - @woocommerce/plugin-woocommerce
  • GitHub Check: build
🔇 Additional comments (7)
plugins/woocommerce/src/Internal/MCP/Transport/WooCommerceRestTransport.php (7)

50-52: Type safety issue with null parameter handling.

The check_permission method accepts $request = null but directly passes it to validate_request() which expects a \WP_REST_Request. This will cause a TypeError under strict types if null is passed.

This exact concern was raised in previous reviews. The method needs input validation before delegating to validate_request().


60-75: LGTM! HTTPS enforcement properly implemented.

The HTTPS check follows WordPress REST API best practices by validating security requirements early in the request lifecycle, and the filter allows necessary flexibility for development environments.


78-94: LGTM! Error messages are properly internationalized.

The authentication error handling follows WordPress REST API patterns with proper WP_Error responses and internationalized messages.


118-119: LGTM! Secret handling follows security best practices.

The code correctly trims the consumer key without mutating it through sanitization, which prevents potential credential corruption while maintaining security.


131-147: LGTM! Authentication follows secure practices.

The implementation correctly:

  • Uses consistent error messages to prevent credential enumeration
  • Employs hash_equals() for timing attack protection
  • Handles secrets appropriately without over-sanitization

152-163: LGTM! User context switching is secure.

The code properly validates the user exists before switching context and returns the user ID on success, following WordPress security best practices.


183-205: Permission mapping aligns with WooCommerce API
The docblock for get_current_user_permissions() (lines 169–170) explicitly lists read, write, and read_write as valid return values, and the switch in check_ability_permission() correctly uses 'write' (or 'read_write') for all mutating HTTP methods—no changes needed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c0cf067 and f399ede.

📒 Files selected for processing (1)
  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
plugins/woocommerce/tests/**/*.php

📄 CodeRabbit inference engine (.cursor/rules/woo-phpunit.mdc)

plugins/woocommerce/tests/**/*.php: Ensure test classes define public setUp() and tearDown() methods (not protected)
All PHPUnit test methods must be public, not protected
Add declare(strict_types=1); at the top of each test file
Test classes should extend WC_Unit_Test_Case

Files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
plugins/woocommerce/tests/php/src/**/*.php

📄 CodeRabbit inference engine (plugins/woocommerce/CLAUDE.md)

Place PHP unit tests under tests/php/src/

Files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
plugins/woocommerce/tests/php/**

📄 CodeRabbit inference engine (plugins/woocommerce/CLAUDE.md)

Store PHP test data and fixtures under tests/php/

Files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.

Files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
🧠 Learnings (5)
📓 Common learnings
Learnt from: budzanowski
PR: woocommerce/woocommerce#60901
File: plugins/woocommerce/src/Internal/Abilities/AbilitiesRestBridge.php:27-94
Timestamp: 2025-09-17T16:16:48.140Z
Learning: The WooCommerce MCP integration is intentionally designed for comprehensive store data manipulation, including orders (with PII) and write operations (create/update/delete). This is by design, not a security oversight, and relies on existing WooCommerce API key authentication and permission scoping for security.
📚 Learning: 2025-08-18T06:11:48.768Z
Learnt from: CR
PR: woocommerce/woocommerce#0
File: .cursor/rules/woo-phpunit.mdc:0-0
Timestamp: 2025-08-18T06:11:48.768Z
Learning: Applies to plugins/woocommerce/tests/**/*.php : Ensure test classes define public setUp() and tearDown() methods (not protected)

Applied to files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
📚 Learning: 2025-09-11T07:18:56.840Z
Learnt from: CR
PR: woocommerce/woocommerce#0
File: plugins/woocommerce/CLAUDE.md:0-0
Timestamp: 2025-09-11T07:18:56.840Z
Learning: Applies to plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/**/*.php : Place unit tests for payment extension suggestions in tests/php/src/Internal/Admin/Suggestions/

Applied to files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
📚 Learning: 2025-08-18T06:11:48.768Z
Learnt from: CR
PR: woocommerce/woocommerce#0
File: .cursor/rules/woo-phpunit.mdc:0-0
Timestamp: 2025-08-18T06:11:48.768Z
Learning: Applies to plugins/woocommerce/tests/**/*.php : Test classes should extend WC_Unit_Test_Case

Applied to files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
📚 Learning: 2025-09-03T11:50:02.208Z
Learnt from: tpaksu
PR: woocommerce/woocommerce#60735
File: plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Fulfillments/class-wc-rest-fulfillments-v4-controller-tests.php:10-10
Timestamp: 2025-09-03T11:50:02.208Z
Learning: For REST API controller tests in plugins/woocommerce/tests/**/*.php, test classes should extend WC_REST_Unit_Test_Case instead of WC_Unit_Test_Case because REST routes need to be registered before tests run.

Applied to files:

  • plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php
🧬 Code graph analysis (1)
plugins/woocommerce/tests/php/src/Internal/MCP/MCPAdapterProviderTest.php (6)
plugins/woocommerce/src/Internal/MCP/MCPAdapterProvider.php (4)
  • MCPAdapterProvider (22-230)
  • maybe_initialize (60-74)
  • is_initialized (206-208)
  • disable_mcp_validation (197-199)
plugins/woocommerce/src/Internal/Abilities/AbilitiesRegistry.php (1)
  • AbilitiesRegistry (18-49)
plugins/woocommerce/src/Utilities/FeaturesUtil.php (1)
  • FeaturesUtil (14-102)
plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php (1)
  • WC_Unit_Test_Case (21-487)
plugins/woocommerce/tests/php/src/Internal/MCP/WooCommerceRestTransportTest.php (2)
  • setUp (27-45)
  • tearDown (50-58)
plugins/woocommerce/woocommerce.php (1)
  • wc_get_container (57-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (26)
  • GitHub Check: Blocks e2e tests 10/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 5/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 9/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 7/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 8/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 6/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Metrics - @woocommerce/plugin-woocommerce [performance]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.3] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Blocks e2e tests 3/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest (HPOS:off) [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Blocks e2e tests 4/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 6/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 1/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 2/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest (HPOS:off) [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Core e2e tests 4/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 1/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core API tests - @woocommerce/plugin-woocommerce [api]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Core e2e tests 3/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 5/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 2/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.3] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Lint - @woocommerce/plugin-woocommerce
  • GitHub Check: build

Replace wp_actions manipulation with proper remove_action() calls to
prevent cross-test contamination:

- remove_action('rest_api_init', [sut, 'maybe_initialize'], 10)
- remove_action('mcp_adapter_init', [sut, 'initialize_mcp_server'], 10)

This ensures action callbacks are actually removed instead of just
unsetting tracking counters, preventing later tests from hitting
previous test instances when hooks fire.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@justlevine
Copy link
Contributor

Hey @budzanowski , not sure the ideal place to ask this, so apologies for spamming an old ticket:

Am I correct in assuming that the reason the MCP Adapter canonical plugin was added here as a composer dep is just because it wasn't (and as of writing still isnt) available on dot-org ?

(related: WordPress/mcp-adapter#87 )

@budzanowski
Copy link
Contributor Author

@justlevine correct. No worries about spamming.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Documentation Issues and PRs related to improving documentation focus: monorepo infrastructure Issues and PRs related to monorepo tooling. plugin: woocommerce Issues related to the WooCommerce Core plugin.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants