Skip to content

Move HttpTransport hook registration to boot() - #222

Open
mathetos wants to merge 1 commit into
WordPress:trunkfrom
mathetos:fix/http-transport-boot-lifecycle
Open

Move HttpTransport hook registration to boot()#222
mathetos wants to merge 1 commit into
WordPress:trunkfrom
mathetos:fix/http-transport-boot-lifecycle

Conversation

@mathetos

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #52 (review comment on #48): HttpTransport no longer registers
est_api_init in its constructor.

  • Add boot() to McpTransportInterface for WordPress hook registration after construction
  • McpTransportFactory calls boot() immediately after instantiating each transport
  • HttpTransport::boot() keeps the existing rest_api_init priority (16) behavior
  • Update custom transport docs and add a small unit test for the boot lifecycle

Motivation

Constructors should set up dependencies only; hook registration is explicit and easier to test in isolation. Happy to adjust naming or scope if maintainers prefer a lighter-touch approach (e.g. factory-only hook without an interface change).

Closes #52

Test plan

  • CI unit/integration suite passes
  • HttpTransportBootTest confirms the constructor does not register hooks and boot() registers rest_api_init at priority 16
  • Smoke: MCP REST route still resolves after plugin activation (rest_url( 'mcp/mcp-adapter-default-server' ))

Add boot() to McpTransportInterface so transports register WordPress hooks
after construction. McpTransportFactory calls boot() immediately after
instantiation. Closes WordPress#52.
@github-actions

Copy link
Copy Markdown

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: mathetos <webdevmattcrom@git.wordpress.org>
Co-authored-by: galatanovidiu <ovidiu-galatan@git.wordpress.org>

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

@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.86%. Comparing base (90252e7) to head (08a2b4d).

Additional details and impacted files
@@            Coverage Diff            @@
##              trunk     #222   +/-   ##
=========================================
  Coverage     87.86%   87.86%           
- Complexity     1245     1246    +1     
=========================================
  Files            53       53           
  Lines          4037     4039    +2     
=========================================
+ Hits           3547     3549    +2     
  Misses          490      490           
Flag Coverage Δ
unit 87.86% <100.00%> (+<0.01%) ⬆️

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

☔ View full report in Codecov by Harness.
📢 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.

@abhi3315 abhi3315 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for working on this. I found one issue I think blocks merging: adding boot() to the existing public interface breaks current custom transports. The other comments are smaller test and documentation fixes.

One documentation gap isn’t on a changed line: docs/architecture/overview.md still shows the old interface and a custom transport without the new lifecycle. It should match whichever contract is chosen. The branch also predates 0.6.1, so it will need an update from trunk and fresh CI before merge.

$this->assertSame( 16, has_action( 'rest_api_init', array( $transport, 'register_routes' ) ) );
}

private function createTransportContext(): McpTransportContext {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These could go in tests/phpunit/Integration/HttpTransportTest.php instead. Its set_up() already builds the server, context and an HttpTransport, so you'd just assert against $this->transport and drop this helper. There are already five other copies of createTransportContext() in the suite.

Assertions look fine. This is only about placement.

*
* Called by McpTransportFactory after construction so constructors stay free of hook side effects.
*/
public function boot(): void {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"so constructors stay free of hook side effects" is only true of this one. new McpServer( ..., array( HttpTransport::class ), ... ) still registers the hook before the constructor returns, through setup_components() -> initialize_transports() -> boot().

#52 is about this constructor, so the PR does close it. The plural here just overstates it. Maybe narrow it to HttpTransport::__construct().

Also worth adding @since n.e.x.t here and on the interface method.

$this->context = $context;
}

public function boot(): void {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This calls register_routes() directly, but HttpTransport::boot() adds a hook. That matters under WP-CLI.

McpAdapter.php:62-66 hooks init at 20 under WP-CLI and rest_api_init at 15 otherwise. So on the CLI path this runs before rest_api_init has fired, and register_rest_route() hits core's ! did_action( 'rest_api_init' ) check and calls _doing_it_wrong().

Same shape as HttpTransport works. 16 because the adapter itself runs at 15:

public function boot(): void {
    add_action( 'rest_api_init', array( $this, 'register_routes' ), 16 );
}

Not new in this PR, the constructor on trunk did the same thing. But line 166 now says boot() registers hooks, so the example doesn't match it.

new $mcp_transport( $context );
$context = $this->create_transport_context();
$transport = new $mcp_transport( $context );
$transport->boot();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If you go with the separate interface (see my comment on McpTransportInterface.php), this becomes:

if ( $transport instanceof BootableTransportInterface ) {
    $transport->boot();
}

* Register WordPress hooks here rather than in the constructor.
* Called by McpTransportFactory immediately after instantiation.
*/
public function boot(): void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think this one blocks. Adding boot() here breaks any existing custom transport that doesn't already have a public boot(): void. PHP throws a fatal when the class is declared and you can't catch it. Under autoload that happens on the class_exists() at line 48, before the interface check at :67.

There is a real implementation outside this repo. wp-media/mcp-oauth v1.1 has OAuthHttpTransport implements McpRestTransportInterface with no boot(). Its ^0.5 constraint means Composer won't pull it in on 0.6.x, so it isn't broken today, but it shows the interface does get implemented downstream. #159 is the closer precedent, that reverted a property visibility change because it broke a downstream transport.

A separate interface avoids touching the existing contract:

interface BootableTransportInterface {
    public function boot(): void;
}

HttpTransport implements both, and the factory checks instanceof before calling boot().

new $mcp_transport( $context );
$context = $this->create_transport_context();
$transport = new $mcp_transport( $context );
$transport->boot();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No test asserts that the factory calls boot(). The new tests call HttpTransport::boot() directly. I removed this line locally and the suite still passed, OK (1004 tests, 3876 assertions). Codecov marks it covered because it runs, but nothing checks it.

A counter on DummyTransport::boot() would test it. Needs to be static since the factory never returns the instance, and reset per test since the fixture is shared:

DummyTransport::$boot_count = 0;
$this->transport_factory->initialize_transports( array( DummyTransport::class ) );
$this->assertSame( 1, DummyTransport::$boot_count );

If boot() moves to its own interface, worth checking the other case too, that a transport with only the old interface still initializes.

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.

Follow-up #48: Can we move this to an initialize() method or somewhere else? I think it's better to not have hooks as a side-effect of constructors.

2 participants