Everything so far has tested your ability’s own PHP directly, WP_Ability::execute(),
no transport involved, on purpose. This lesson tests the other side of the boundary:
does your server actually behave correctly once a real MCP client is talking to it
over the wire, and does your own custom error or observability handler (from Courses 4,
8, and 19) actually get called with the data you expect. Those are two different
questions, and this lesson uses two different, real tools for them, the MCP Adapter’s
own test-double pattern for the first, and MCP Inspector for the second.
A custom McpErrorHandlerInterface or McpObservabilityHandlerInterface
implementation from Course 4 or Course 19’s error-handler lesson, wired in through
your server’s config filter. Node.js available for npx if you want to run Inspector
in Step 4.
Step 1: Where the real Dummy handlers actually live, and why you can’t install them
The MCP Adapter’s own docs/guides/testing.md documents two real fixtures used inside
its own test suite: DummyObservabilityHandler, which captures every record_event()
call into a static array for assertions, and DummyErrorHandler, which does the same
for log() calls. Both are genuinely useful to read, they’re the exact pattern the
adapter’s own authors use to verify their internal handlers get invoked correctly. But
they live at tests/phpunit/Fixtures/ inside the WordPress/mcp-adapter repository,
and that whole tests/ directory is explicitly excluded from the distributed package
(its .gitattributes marks /tests as export-ignore). A normal
composer require wordpress/mcp-adapter in your own plugin never pulls these classes
in at all, there’s nothing to use from your own test suite.
That’s not a limitation worth working around, it’s the correct boundary: those
fixtures test the adapter’s own internals, ToolsHandler and friends, code you don’t
own and shouldn’t be reaching into from your plugin’s tests. What you can, and should,
take from them is the pattern itself.
Step 2: Build your own equivalent, for your own handler
Both real interfaces are small, stable, public contracts, the same ones Course 19’s
error-handler lesson and Course 4’s observability lesson already have you
implementing. Copy the same shape the adapter’s own fixtures use, a static array, a
reset() method, and the interface’s one method recording into it:
// File: tests/Unit/Fixtures/TestObservabilityHandler.php
class TestObservabilityHandler implements McpObservabilityHandlerInterface {
/** @var list<array{event:string,tags:array,duration_ms:?float}> */
public static array $events = array();
public static function reset(): void {
self::$events = array();
}
public function record_event( string $event, array $tags = array(), ?float $duration_ms = null ): void {
self::$events[] = array(
'event' => $event,
'tags' => $tags,
'duration_ms' => $duration_ms,
);
}
}
This isn’t a copy of anything you had to install, it’s a small class you own, written
once, that follows a proven, documented shape. The value of having read the real
DummyObservabilityHandler is knowing this exact shape already works for this exact
purpose, not needing to invent your own from nothing.
Step 3: Contract-test your production handler, not the double
The test double above exists to verify your production handler, the one that writes
to a database table or fires an alert, actually gets called correctly by whatever
production code path is supposed to call it. Test that path directly, wherever your
own code invokes log() or record_event(), the audit-log wrapper around
execute_callback from Course 4, or the error handling inside your own custom
McpErrorHandlerInterface implementation from Course 19:
// File: tests/Unit/MyPluginErrorHandlerTest.php
<?php
class MyPluginErrorHandlerTest extends WP_UnitTestCase {
public function test_log_writes_expected_row_to_the_error_table(): void {
global $wpdb;
$handler = new My_Plugin_Mcp_Error_Handler();
$handler->log( 'Tool execution failed', array( 'tool_name' => 'my-plugin/list-tickets' ), 'error' );
$row = $wpdb->get_row(
"SELECT type, message FROM {$wpdb->prefix}mcp_error_log ORDER BY id DESC LIMIT 1",
ARRAY_A
);
$this->assertSame( 'error', $row['type'] );
$this->assertSame( 'Tool execution failed', $row['message'] );
}
}
This is the contract that actually matters for your plugin: given the exact
log() signature the McpErrorHandlerInterface contract defines, does your
implementation do the right thing with it. It needs no adapter internals at all, only
the interface your own class already implements.
Step 4: MCP Inspector for the one thing PHPUnit can’t confirm, the real wire
Everything above tests your PHP directly. It doesn’t confirm what an actual MCP client sees on the wire, the tool list, each tool’s schema, a real call’s response shape. That’s what MCP Inspector, from Course 19, is for, and it’s the right complement here, not a replacement:
# File: terminal
npx @modelcontextprotocol/inspector \
wp --path=/path/to/your/wordpress/site mcp-adapter serve \
--server=my-plugin-content-server --user=admin
Use it after a schema change or a new ability registration, before assuming the PHPUnit suite alone proves a client would actually see what you expect. A schema that validates correctly inside a PHPUnit test can still render oddly to a specific client’s UI, or expose a field you didn’t mean to make public, differences only visible by looking at the actual wire contract, not by asserting against PHP objects.
It’s technically possible to require the MCP Adapter’s dev dependencies and reach for
classes like WP\MCP\Handlers\Tools\ToolsHandler directly, since PHP doesn’t stop you
from instantiating a class you can autoload. Resist it. Those classes are internal
implementation details of the adapter’s own contributor test suite, not a public
contract for downstream plugins, and they can change shape between adapter releases
with no deprecation notice, since they were never a supported integration point.
Contract-test your own handler against the interface, and contract-test the wire with
Inspector, that’s the full, real surface available to you.
Test it: break your handler’s wiring and confirm both layers catch it differently
Comment out the line in your server config filter that sets
$config['error_handler'], then run both checks. Your PHPUnit contract test for
My_Plugin_Mcp_Error_Handler::log() still passes, since that class works correctly in
isolation, it was never wired in wrong at that level. Trigger a real failure through
Inspector instead, and confirm wp-content/debug.log shows the adapter’s default
ErrorLogMcpErrorHandler output rather than a row in your custom table. That gap
between “the class works” and “the class is actually being used” is exactly what
having both layers is for.
Recap
DummyObservabilityHandler and DummyErrorHandler are real, documented fixtures
inside the MCP Adapter’s own test suite, but they live in a tests/ directory that’s
excluded from the distributed package, so the value they offer a downstream plugin is
the pattern, not an installable class. Copy that same shape for your own custom
handler’s test double, contract-test your production log() or record_event()
implementation directly against the interface it implements, and use MCP Inspector
separately to confirm the real wire contract, since no amount of PHPUnit against your
own PHP classes can substitute for looking at what an actual client sees.