You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Some users hit a fatal error right after updating to 3.23.1. WordPress' fatal error protection then pauses WP Rocket and emails the site owner, who reads it as WP Rocket having been uninstalled by WordPress. The error is:
PHP Fatal error: Could not check compatibility between
WP\MCP\Domain\Tools\McpTool::get_protocol_dto(): WP\McpSchema\Server\Tools\DTO\Tool and
WP\MCP\Domain\Contracts\McpComponentInterface::get_protocol_dto(): WP\McpSchema\Common\AbstractDataTransferObject,
because class WP\McpSchema\Server\Tools\DTO\Tool is not available in
.../wp-rocket/vendor/wordpress/mcp-adapter/includes/Domain/Tools/McpTool.php on line 251
Every file is present and correct on disk the whole time, which is why reinstalling changes nothing and why it looks impossible to reproduce. After this change the same conditions produce no fatal error, the plugin stays active, and MCP keeps working.
Type of change
New feature (non-breaking change which adds functionality).
Bug fix (non-breaking change which fixes an issue).
Enhancement (non-breaking change which improves an existing functionality).
Breaking change (fix or feature that would cause existing functionality to not work as before).
Sub-task of #(issue number)
Chore
Release
Detailed scenario
What was tested
Automated. New unit test covering rocket_get_vendor_class_relative_path(), 8 cases: both bundled prefixes, a class at the prefix root, and the cases that must be ignored (WP Rocket classes, Mozart prefixed classes, a namespace that merely shares the start of a prefix, an unrelated class, an empty class name).
vendor/bin/phpunit --testsuite unit --configuration tests/Unit/phpunit.xml.dist \
--filter Test_RocketGetVendorClassRelativePath
OK (8 tests, 8 assertions)
Manual. Reproduced the reported fatal error against a real 3.23.1 install, by registering a namespace prefix table that knows WP\MCP\ but not WP\McpSchema\, which is exactly what 3.23.0 shipped, with 3.23.1's files on disk, then linking McpTool:
Without the fallback: the fatal error above, byte for byte, same file, same line 251.
With the fallback registered: WP\McpSchema\Server\Tools\DTO\Tool resolves and McpTool links with no error.
PHPCS and PHPStan are clean on both changed source files.
How to test
MCP only initialises on REST requests (rest_api_init priority 15), on WP 6.9+ with the Abilities API available, so a plain page view is not enough.
On WP 7.0 with 3.23.1 installed, confirm vendor/wordpress/php-mcp-schema/ is present and GET /wp-json/ returns normally.
Reproduce the broken state in one request, which is what the update window creates: register an autoloader that resolves WP\MCP\ from vendor/wordpress/mcp-adapter/includes/ and nothing else, then call class_exists( 'WP\MCP\Domain\Tools\McpTool' ). On develop this fatals with the error above. On this branch it does not.
Regression: with everything normal, check that GET /wp-json/ and the MCP route still respond, and that no extra file is loaded on a front end page view (the fallback only runs on an autoload miss).
Affected Features & Quality Assurance Scope
Plugin bootstrap, inc/main.php, so smoke testing the whole plugin is relevant: activation, deactivation, admin pages, cache clearing.
MCP and the OAuth MCP server, since those are the bundled dependencies the fallback covers.
Plugin update path, updating from 3.23.x to this build, and the auto update path.
Technical description
Documentation
Composer builds its namespace prefix table once, when vendor/autoload.php is required, and the ClassLoader object keeps it in memory for the rest of the request. WordPress replaces the plugin directory in place during an update, so a request that started on the previous version keeps that version's table while the files on disk are already the new ones. Any namespace the previous version did not know about is then unresolvable, even though its files are there.
3.23.1 is where this became visible: 3.23.0 shipped mcp-adapter 0.4.1, whose table contains a single prefix, WP\MCP\. 3.23.1 ships 0.5.0 plus the new wordpress/php-mcp-schema package, and McpTool::get_protocol_dto() narrows its return type to a class in the new WP\McpSchema\ namespace. PHP resolves that class when it links McpTool, and a table without the prefix cannot provide it, so the request dies with E_COMPILE_ERROR. That error type cannot be caught, so it has to be prevented rather than handled.
This adds a last resort autoloader, appended to the SPL stack, that maps the bundled dependency prefixes to their directory and resolves them straight from disk:
WP\MCP\ to vendor/wordpress/mcp-adapter/includes/
WP\McpSchema\ to vendor/wordpress/php-mcp-schema/src/
Three properties matter:
It is appended, so it only runs after every other autoloader has missed. A copy of the same dependency provided by another plugin is never overridden, since that plugin's autoloader resolves the class first.
It names no class and no version, so it also covers whatever changes in a future version. This is deliberately not the narrower guard of checking that one specific DTO or method exists.
The prefix mapping is repeated in our own code rather than read back from vendor/composer/autoload_psr4.php, because that file is precisely what can be outdated in memory when the fallback is needed.
Only the dependencies that keep their upstream namespace are listed. The Mozart prefixed ones live under WP_Rocket\Dependencies\, a prefix present in every version of the table, so they are not exposed to this.
Same approach as WooCommerce, which added an appended PSR-4 fallback for the same failure, see plugins/woocommerce/woocommerce.php and src/Autoloader.php in their repository.
New dependencies
None.
Risks
Performance. The closure only runs on an autoload miss for one of two namespace prefixes, and returns immediately on a prefix mismatch. In a healthy request Composer resolves those classes first, so the fallback is never reached. Worst case is one is_readable() call.
Loading the wrong copy of a dependency. Prevented by appending instead of prepending: if another plugin provides the class, its autoloader answers before ours.
Path traversal through the class name. The class name is only used after a strict prefix match, and \ becomes / with .php appended, so the result stays under the plugin directory. is_readable() guards the include.
Masking a genuinely broken install. If the files really are missing, is_readable() fails and the fallback stays silent, exactly like Composer would.
Mandatory Checklist
Code validation
I validated all the Acceptance Criteria. If possible, provide screenshots or videos.
I triggered all changed lines of code at least once without new errors/warnings/notices.
I implemented built-in tests to cover the new/changed code.
Code style
I wrote a self-explanatory code about what it does.
I protected entry points against unexpected inputs.
I did not introduce unnecessary complexity.
Output messages (errors, notices, logs) are explicit enough for users to understand the issue and are actionnable.
Unticked items justification
The autoloader is deliberately silent. It runs during class linking, before the plugin is in a state where logging is safe, and anything it cannot resolve is left to the other autoloaders exactly as before. There is no user facing message to make actionable.
Additional Checks
In the case of complex code, I wrote comments to explain it.
When possible, I prepared ways to observe the implemented system (logs, data, etc.).
I added error handling logic when using functions that could throw errors (HTTP/API request, filesystem, etc.)
Observability was left out on purpose, for the reason above. The fix is verifiable from the outside instead: the fatal error stops appearing in debug.log, and WordPress stops pausing the plugin after an update.
Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>
Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer TIP This summary will be updated as you push new changes.
@Miraeld Thanks for the PR. I have a few questions:
Shouldn't this be part of the OAuth MCP library so all plugins using the MCP adapter would benefit from it? Or should we consider this covering a larger scope than the MCP Adapter?
In "What was tested - Manual", you mention that you reproduced the issue, but using a manually altered namespace, correct? Or do we have a way to reproduce possibly recurring issues even after an update, as potentially happening?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes #8652
Some users hit a fatal error right after updating to 3.23.1. WordPress' fatal error protection then pauses WP Rocket and emails the site owner, who reads it as WP Rocket having been uninstalled by WordPress. The error is:
Every file is present and correct on disk the whole time, which is why reinstalling changes nothing and why it looks impossible to reproduce. After this change the same conditions produce no fatal error, the plugin stays active, and MCP keeps working.
Type of change
Detailed scenario
What was tested
Automated. New unit test covering
rocket_get_vendor_class_relative_path(), 8 cases: both bundled prefixes, a class at the prefix root, and the cases that must be ignored (WP Rocket classes, Mozart prefixed classes, a namespace that merely shares the start of a prefix, an unrelated class, an empty class name).Manual. Reproduced the reported fatal error against a real 3.23.1 install, by registering a namespace prefix table that knows
WP\MCP\but notWP\McpSchema\, which is exactly what 3.23.0 shipped, with 3.23.1's files on disk, then linkingMcpTool:WP\McpSchema\Server\Tools\DTO\Toolresolves andMcpToollinks with no error.PHPCS and PHPStan are clean on both changed source files.
How to test
MCP only initialises on REST requests (
rest_api_initpriority 15), on WP 6.9+ with the Abilities API available, so a plain page view is not enough.vendor/wordpress/php-mcp-schema/is present andGET /wp-json/returns normally.WP\MCP\fromvendor/wordpress/mcp-adapter/includes/and nothing else, then callclass_exists( 'WP\MCP\Domain\Tools\McpTool' ). Ondevelopthis fatals with the error above. On this branch it does not.GET /wp-json/and the MCP route still respond, and that no extra file is loaded on a front end page view (the fallback only runs on an autoload miss).Affected Features & Quality Assurance Scope
inc/main.php, so smoke testing the whole plugin is relevant: activation, deactivation, admin pages, cache clearing.Technical description
Documentation
Composer builds its namespace prefix table once, when
vendor/autoload.phpis required, and theClassLoaderobject keeps it in memory for the rest of the request. WordPress replaces the plugin directory in place during an update, so a request that started on the previous version keeps that version's table while the files on disk are already the new ones. Any namespace the previous version did not know about is then unresolvable, even though its files are there.3.23.1 is where this became visible: 3.23.0 shipped mcp-adapter 0.4.1, whose table contains a single prefix,
WP\MCP\. 3.23.1 ships 0.5.0 plus the newwordpress/php-mcp-schemapackage, andMcpTool::get_protocol_dto()narrows its return type to a class in the newWP\McpSchema\namespace. PHP resolves that class when it linksMcpTool, and a table without the prefix cannot provide it, so the request dies withE_COMPILE_ERROR. That error type cannot be caught, so it has to be prevented rather than handled.This adds a last resort autoloader, appended to the SPL stack, that maps the bundled dependency prefixes to their directory and resolves them straight from disk:
WP\MCP\tovendor/wordpress/mcp-adapter/includes/WP\McpSchema\tovendor/wordpress/php-mcp-schema/src/Three properties matter:
vendor/composer/autoload_psr4.php, because that file is precisely what can be outdated in memory when the fallback is needed.Only the dependencies that keep their upstream namespace are listed. The Mozart prefixed ones live under
WP_Rocket\Dependencies\, a prefix present in every version of the table, so they are not exposed to this.Same approach as WooCommerce, which added an appended PSR-4 fallback for the same failure, see
plugins/woocommerce/woocommerce.phpandsrc/Autoloader.phpin their repository.New dependencies
None.
Risks
is_readable()call.\becomes/with.phpappended, so the result stays under the plugin directory.is_readable()guards the include.is_readable()fails and the fallback stays silent, exactly like Composer would.Mandatory Checklist
Code validation
Code style
Unticked items justification
The autoloader is deliberately silent. It runs during class linking, before the plugin is in a state where logging is safe, and anything it cannot resolve is left to the other autoloaders exactly as before. There is no user facing message to make actionable.
Additional Checks
Observability was left out on purpose, for the reason above. The fix is verifiable from the outside instead: the fatal error stops appearing in
debug.log, and WordPress stops pausing the plugin after an update.