Skip to content

Conversation

@galatanovidiu
Copy link
Contributor

Why

The plugin was using PHP exceptions for error handling, which contradicts WordPress conventions where WP_Error is the standard pattern.

What

Refactored all error handling from exceptions to WordPress's native WP_Error pattern across the entire plugin.

Changes Made

Core Layer (includes/Core/)

  • McpAdapter::create_server() now returns self|WP_Error (removed 5 exception throws)
  • McpComponentRegistry removed 4 try-catch blocks, now uses is_wp_error() checks
  • McpTransportFactory logs errors using _doing_it_wrong and continues instead of throwing

Domain Layer (includes/Domain/)

  • All get_ability() methods return WP_Ability|WP_Error instead of throwing
  • All validators (McpToolValidator, McpResourceValidator, McpPromptValidator) return bool|WP_Error
  • All validate() methods return self|WP_Error
  • All registration classes propagate WP_Error instead of throwing exceptions

Handlers Layer (includes/Handlers/)

  • Added is_wp_error() checks after get_ability() calls
  • Preserved try-catch for \Throwable to catch unexpected PHP errors

Testing

  • Added assertWPError() helper to TestCase
  • Updated 37 tests to expect WP_Error instead of exceptions

Benefits

  • Follows WordPress core conventions
  • Removed 14+ try-catch blocks
  • Better error recovery with graceful degradation
  • Enhanced observability with specific error codes
  • Zero breaking changes

Closes #24

Replace exception-based error handling with WordPress-native WP_Error pattern
in the Core layer components.

Changes:
- McpAdapter::create_server() now returns self|WP_Error instead of throwing
  exceptions for validation failures (invalid error handler, observability
  handler, timing, and duplicate server IDs)
- DefaultServerFactory::create() adds is_wp_error() check and logs errors
- McpComponentRegistry removes 4 try-catch blocks from register_tools(),
  add_tool(), register_resources(), and register_prompts()
- McpTransportFactory logs errors and continues processing other transports
  instead of throwing exceptions
- Update McpTransportFactoryTest to expect graceful error handling

Error codes introduced:
- invalid_error_handler
- invalid_observability_handler
- invalid_timing
- duplicate_server_id

All 357 tests passing.

Related to #24
Convert all domain validators and validate() methods to return WP_Error
instead of throwing exceptions, following WordPress conventions.

Changes:
- All validator methods (validate_tool_data, validate_tool_instance,
  validate_tool_uniqueness, and equivalents for Resources and Prompts)
  now return bool|WP_Error instead of void
- McpTool::get_ability() returns WP_Ability|WP_Error instead of throwing
  InvalidArgumentException
- McpTool::validate(), McpResource::validate(), and McpPrompt::validate()
  return self|WP_Error instead of throwing
- ToolsHandler::handle_tool_call() adds is_wp_error() check after
  calling get_ability()
- Add assertWPError() and assertNotWPError() helper methods to TestCase
- Update 37 validator tests across 3 test files to expect WP_Error
  instead of exceptions

Error codes introduced:
- tool_validation_failed, tool_not_unique
- resource_validation_failed, resource_not_unique
- prompt_validation_failed, prompt_not_unique
- ability_not_found

All 357 tests passing with 1521 assertions.

Related to #24
…ng exceptions

Convert all RegisterAbilityAs* classes to return WP_Error instead of
throwing exceptions when converting WordPress abilities to MCP components.

Changes:
- RegisterAbilityAsMcpTool::make() returns McpTool|WP_Error
- RegisterAbilityAsMcpResource::make() returns McpResource|WP_Error
- RegisterAbilityAsMcpResource::get_uri() returns string|WP_Error instead
  of throwing InvalidArgumentException
- RegisterAbilityAsMcpResource::get_data() propagates WP_Error from get_uri()
- RegisterAbilityAsMcpPrompt::make() returns McpPrompt|WP_Error
- Remove all @throws annotations from registration classes

Error propagation pattern:
- Check is_wp_error() at each method in the chain
- Return WP_Error immediately if found
- Allow from_array() validation errors to bubble up naturally

This completes the elimination of all exception throws in the registration
layer, with errors now properly propagated as WP_Error objects.

All 357 tests passing with 1521 assertions.

Related to #24
Add is_wp_error() checks in handlers after get_ability() calls to properly
handle errors propagated from the registration and domain layers.

Changes:
- PromptsHandler::get_prompt() checks is_wp_error() after get_ability()
  for non-builder prompts
- ResourcesHandler::read_resource() checks is_wp_error() after get_ability()
- Both handlers log errors and return proper MCP error responses with
  metadata including error_code and failure_reason
- Error responses include 'ability_retrieval_failed' as failure_reason

This completes the refactoring to eliminate all exception-based error
handling in favor of WordPress's native WP_Error pattern. The codebase
now has:
- Zero exception throws in production code (7 eliminated)
- Zero try-catch blocks for InvalidArgumentException (4 removed)
- Consistent WP_Error propagation from Core → Domain → Registration → Handlers

All 357 tests passing with 1521 assertions.

Fixes #24
Ensure consistency across all MCP component types by making get_ability()
return WP_Ability|WP_Error instead of nullable WP_Ability.

Changes:
- McpResource::get_ability() now returns WP_Ability|WP_Error instead of
  ?WP_Ability (null)
- McpPrompt::get_ability() now returns WP_Ability|WP_Error instead of
  ?WP_Ability (null)
- McpPromptBuilder anonymous class get_ability() returns WP_Error with
  'builder_has_no_ability' error code
- Update 2 tests to expect WP_Error instead of null from builder prompts

This ensures PHPStan type checking passes and all get_ability() methods
across the codebase follow the same error handling pattern.

All 357 tests passing with 1523 assertions.
PHPStan Level 8 analysis passing with no errors.

Related to #24
@codecov
Copy link

codecov bot commented Oct 21, 2025

Codecov Report

❌ Patch coverage is 88.88889% with 36 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.74%. Comparing base (0e29107) to head (3d1e4d5).
⚠️ Report is 22 commits behind head on trunk.

Files with missing lines Patch % Lines
includes/Core/McpComponentRegistry.php 87.14% 18 Missing ⚠️
includes/Domain/Prompts/McpPrompt.php 75.00% 4 Missing ⚠️
includes/Domain/Resources/McpResource.php 77.77% 4 Missing ⚠️
includes/Domain/Tools/McpTool.php 69.23% 4 Missing ⚠️
includes/Domain/Prompts/McpPromptValidator.php 81.81% 2 Missing ⚠️
includes/Domain/Resources/McpResourceValidator.php 81.81% 2 Missing ⚠️
includes/Domain/Tools/McpToolValidator.php 81.81% 2 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##              trunk      #71       +/-   ##
=============================================
+ Coverage     68.32%   79.74%   +11.41%     
- Complexity      825      858       +33     
=============================================
  Files            46       46               
  Lines          3084     3080        -4     
=============================================
+ Hits           2107     2456      +349     
+ Misses          977      624      -353     
Flag Coverage Δ
unit 79.74% <88.88%> (+11.41%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…d McpResource files. Update error logging in DefaultServerFactory to escape error messages and codes.
Introduces comprehensive unit test suites for the `ToolsHandler` and `ResourcesHandler` to verify their core functionality. These tests cover success paths, error conditions like missing parameters or components not being found, and permission handling.

Additionally, enhances the `McpComponentRegistry` tests to ensure that non-string inputs are correctly skipped during registration and that no observability events are recorded when the feature is disabled.
@galatanovidiu galatanovidiu marked this pull request as ready for review October 21, 2025 10:06
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors error handling across the plugin by replacing PHP exceptions with WordPress's native WP_Error pattern, aligning with WordPress core conventions. The changes ensure graceful error handling and better error recovery throughout the plugin.

Key Changes:

  • Replaced exception-based error handling with WP_Error returns in all validation, registration, and retrieval methods
  • Updated handlers to check for WP_Error instances using is_wp_error() before proceeding with operations
  • Modified test suite to expect WP_Error objects instead of exceptions

Reviewed Changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/Unit/Prompts/McpPromptBuilderTest.php Updated test to verify WP_Error return instead of null for builder-based prompts
tests/Unit/Handlers/ToolsHandlerTest.php New comprehensive test file for ToolsHandler with error scenarios
tests/Unit/Handlers/ResourcesHandlerTest.php New comprehensive test file for ResourcesHandler with error scenarios
tests/Unit/Handlers/PromptsHandlerTest.php Added metadata assertion to success test
tests/Unit/Domain/Tools/McpToolValidatorTest.php Converted exception-based tests to WP_Error assertions
tests/Unit/Domain/Resources/McpResourceValidatorTest.php Converted exception-based tests to WP_Error assertions
tests/Unit/Domain/Prompts/McpPromptValidatorTest.php Converted exception-based tests to WP_Error assertions
tests/Unit/Core/McpTransportFactoryTest.php Updated test to expect graceful error handling instead of exceptions
tests/Unit/Core/McpComponentRegistryTest.php Added tests for error handling and non-string input filtering
tests/TestCase.php Added assertWPError() and assertNotWPError() helper methods
tests/Integration/BuilderPromptExecutionTest.php Updated to verify WP_Error return for builder-based prompts
includes/Servers/DefaultServerFactory.php Added error checking and logging after server creation
includes/Handlers/Tools/ToolsHandler.php Added WP_Error check after get_ability() call
includes/Handlers/Resources/ResourcesHandler.php Added WP_Error check after get_ability() call
includes/Handlers/Prompts/PromptsHandler.php Added WP_Error check after get_ability() call
includes/Domain/Tools/RegisterAbilityAsMcpTool.php Changed return type to support WP_Error
includes/Domain/Tools/McpToolValidator.php Replaced exception throws with WP_Error returns
includes/Domain/Tools/McpTool.php Changed get_ability() to return WP_Error instead of throwing
includes/Domain/Resources/RegisterAbilityAsMcpResource.php Changed return type to support WP_Error and added error propagation
includes/Domain/Resources/McpResourceValidator.php Replaced exception throws with WP_Error returns
includes/Domain/Resources/McpResource.php Changed get_ability() to return WP_Error instead of throwing
includes/Domain/Prompts/RegisterAbilityAsMcpPrompt.php Changed return type to support WP_Error
includes/Domain/Prompts/McpPromptValidator.php Replaced exception throws with WP_Error returns
includes/Domain/Prompts/McpPromptBuilder.php Changed get_ability() to return WP_Error with descriptive message
includes/Domain/Prompts/McpPrompt.php Changed get_ability() to return WP_Error instead of throwing
includes/Core/McpTransportFactory.php Removed exception throw, added error logging and graceful continuation
includes/Core/McpComponentRegistry.php Replaced try-catch blocks with is_wp_error() checks throughout
includes/Core/McpAdapter.php Changed exception throws to WP_Error returns in create_server()

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@github-actions
Copy link

github-actions bot commented Oct 21, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: galatanovidiu <[email protected]>
Co-authored-by: JasonTheAdams <[email protected]>
Co-authored-by: neillmcshea <[email protected]>
Co-authored-by: justlevine <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@galatanovidiu galatanovidiu self-assigned this Oct 21, 2025
galatanovidiu and others added 4 commits October 22, 2025 10:34
Implement proper distinction between protocol errors and tool execution
errors according to MCP spec (https://modelcontextprotocol.io/specification/2025-06-18/server/tools#error-handling):

Protocol errors (JSON-RPC format with 'error' key):
- Tool not found
- Missing required parameters
- Internal errors (ability retrieval failures)

Tool execution errors (isError: true format):
- Permission denied
- Permission check failures
- Execution failures
- WP_Error from ability execution

Changes:
- ToolsHandler::call_tool() now distinguishes error types based on
  failure_reason metadata
- Permission denied, permission check failures, and execution errors
  now return {content: [{type: 'text', text: '...'}], isError: true}
- Protocol errors still return JSON-RPC error format
- Update 7 tests to expect isError format for tool execution errors
- Remove impossible is_wp_error() check (PHPStan was correct)

All 357 tests passing with 1527 assertions.
PHPStan Level 8 analysis passing with no errors.

Fixes #69
Add proper WP_Error detection after ability execution in PromptsHandler
  and ResourcesHandler to prevent crashes and return graceful error responses.
Updates test expectations for permission-related errors in ToolsHandler to use isError format instead of JSON-RPC error format.

Per MCP spec: "Any errors that originate from the tool SHOULD be reported inside the result object, with isError set to true, not as an MCP protocol-level error response. Otherwise, the LLM would not be able to see that an error occurred and self-correct."

Permission errors originate from the tool's permission check phase, so they should be returned as tool execution errors (isError: true) rather than protocol-level errors. This allows LLMs to observe and potentially handle permission failures.

Changes:
  - test_call_tool_permission_exception_returns_error: Now expects isError
  - test_call_tool_permission_denied_returns_error: Now expects isError
  - Added spec-based comments explaining the error handling approach
@JasonTheAdams
Copy link
Member

I'm really curious about this. Is throwing exceptions truly never the solution in WordPress? I searched the WP code, and found there are 166 PHP files that contains throw new at least once.

I can't find a document on this, so I asked ChatGPT, and it seems to have given sensible criteria. Funnily enough, it actually quotes the Abilities API work for some of its reasoning. Hahah!

@galatanovidiu
Copy link
Contributor Author

I'm really curious about this. Is throwing exceptions truly never the solution in WordPress? I searched the WP code, and found there are 166 PHP files that contains throw new at least once.

I can't find a document on this, so I asked ChatGPT, and it seems to have given sensible criteria. Funnily enough, it actually quotes the Abilities API work for some of its reasoning. Hahah!

@JasonTheAdams I’m usually in the “throw exceptions” camp, but in this codebase we were mostly throwing, catching immediately, and then logging or returning an MCP error. This refactor is a win: using WP_Error aligns with WP conventions, removes boilerplate try/catch (e.g., in McpComponentRegistry/handlers), and gives us consistent error codes while keeping behavior MCP-spec compliant.

Improves test robustness by adding assertions to ensure abilities are registered before being used in tests.

This prevents unexpected errors and provides more informative feedback when abilities are not correctly registered.
Improves the stability of prompt registration by wrapping the prompt builder instantiation and build process in a try-catch block.

This prevents a single prompt builder exception from halting the entire component registration process. When an exception occurs, it logs the error and records a failure event, allowing the system to continue registering other components.
Adds a return type declaration to the `get_ability` method for better type hinting.
Enhances developer experience by providing more informative error messages and adding new tests to ensure proper error handling in various scenarios.

Adds a mechanism to capture `_doing_it_wrong` calls for more robust testing of developer-facing errors. This allows for assertions that specific functions trigger `_doing_it_wrong` with the expected messages, leading to more reliable detection of incorrect API usage.

Specifically, the changes cover:

- Creating servers outside of the `mcp_adapter_init` hook.
- Using duplicate server IDs.
- Using non-existent or invalid transport classes.
- Cloning and waking up the Plugin singleton.

These improvements help developers identify and fix integration issues more easily, resulting in a more stable and maintainable system.
Ensures that errors during prompt building are caught more broadly by catching `\Throwable` instead of just `\Exception`.

This allows the system to handle a wider range of potential issues that might arise when constructing prompts, improving stability and providing more informative error logs.
Ensures abilities are registered within the `wp_abilities_api_init` hook.

This is necessary as the WordPress abilities API checks if the hook is currently running.

Adds a helper function `register_ability_in_hook` to facilitate registering abilities correctly.
Refactors the way abilities are registered in tests to ensure proper cleanup and prevent potential duplicate registrations when the `wp_abilities_api_init` hook is fired multiple times. It achieves this by using a static function for the callback and registers the ability using the `register_ability_in_hook` method.
Improves the testing framework for ability registration by adding assertions to verify that abilities are correctly registered before use. This change aims to prevent unexpected errors during tests and provides clearer feedback when registration issues occur, contributing to a more reliable testing environment.
Copy link
Member

@JasonTheAdams JasonTheAdams left a comment

Choose a reason for hiding this comment

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

Thanks for explaining! LGTM!

Implements validation checks for MCP components (tools and prompts) during registration, ensuring that components meet the required criteria before being added to the system.

This change ensures that the validation flag on the McpComponentRegistry is respected by directly calling the validators, and that components are validated during registration and when added individually. This improves the reliability and consistency of the MCP system by preventing the registration of invalid components. It also records component registration failures for observability purposes.

Adds the ability to set the MCP server on the McpResource.
Implements additional tests for error scenarios in MCP components, including handling WP_Error and exceptions during ability execution, resource reading, and tool calls. This change ensures that the system correctly identifies and reports errors, improving the reliability of the MCP framework. It also verifies that appropriate error messages and metadata are returned, contributing to better observability and debugging capabilities.
@galatanovidiu galatanovidiu merged commit db59ef5 into trunk Nov 5, 2025
18 checks passed
@galatanovidiu galatanovidiu deleted the refactor/24-exception-to-wp-error branch November 5, 2025 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reduce use of throw {ExceptionType} in favor of WordPress error handling

3 participants