Everything in this course so far has been architecture you write once and trust
forever, unless something breaks it silently. A permission_callback that quietly
starts returning true for everyone after a refactor, or an execute_callback that
starts returning the wrong shape after a schema change, is exactly the kind of bug that
doesn’t show up until an agent (or a security review) hits it in production. This
lesson wires ability registration, permission logic, and execution behavior into a real
automated test suite, using WordPress’s own standard testing tools, not a custom
framework.
Comfortable with PHPUnit basics and having a local WordPress environment (wp-env, from Course 1). This lesson assumes you already have at least one plugin registering abilities to test against.
Step 1: Scaffold the test suite
WP-CLI generates the standard PHPUnit scaffolding for a plugin, a bootstrap file, a
phpunit.xml.dist, and a starter test case:
wp-env run cli wp scaffold plugin-tests my-plugin
This produces tests/bootstrap.php (which loads the WordPress test suite itself
before your plugin), phpunit.xml.dist, and a tests/test-sample.php you’ll replace.
Run it inside wp-env, which already has the WordPress test library available:
wp-env run tests-cli --env-cwd=wp-content/plugins/my-plugin phpunit
Step 2: Test that registration actually happened, and matches expectations
Don’t just assert the plugin activates without a fatal error, assert the specific ability exists with the schema and permission you expect:
// File: tests/test-abilities-registration.php
class Test_Abilities_Registration extends WP_UnitTestCase {
public function test_ability_is_registered(): void {
$this->assertTrue( wp_has_ability( 'my-plugin/list-tickets' ) );
}
public function test_ability_has_expected_schema_shape(): void {
$ability = wp_get_ability( 'my-plugin/list-tickets' );
$schema = $ability->get_input_schema();
$this->assertSame( 'object', $schema['type'] );
$this->assertArrayHasKey( 'status', $schema['properties'] );
}
public function test_ability_is_not_publicly_exposed_by_default(): void {
$ability = wp_get_ability( 'my-plugin/list-tickets' );
$meta = $ability->get_meta();
$this->assertFalse( $meta['mcp']['public'] ?? false );
}
}
That last test matters more than it looks: it’s a regression test against someone
accidentally flipping meta.mcp.public to true in a future pull request without
anyone noticing during review.
Step 3: Test permission_callback across real user contexts
This is the single highest-value test in the whole suite, since a broken permission check is a security incident, not a functional bug:
// File: tests/test-permission-callback.php
class Test_Permission_Callback extends WP_UnitTestCase {
public function test_editor_can_call_ability(): void {
$editor = self::factory()->user->create( array( 'role' => 'editor' ) );
wp_set_current_user( $editor );
$ability = wp_get_ability( 'my-plugin/list-tickets' );
$this->assertTrue( $ability->has_permission( array() ) );
}
public function test_subscriber_cannot_call_ability(): void {
$subscriber = self::factory()->user->create( array( 'role' => 'subscriber' ) );
wp_set_current_user( $subscriber );
$ability = wp_get_ability( 'my-plugin/list-tickets' );
$this->assertFalse( $ability->has_permission( array() ) );
}
public function test_logged_out_user_cannot_call_ability(): void {
wp_set_current_user( 0 );
$ability = wp_get_ability( 'my-plugin/list-tickets' );
$this->assertFalse( $ability->has_permission( array() ) );
}
}
Step 4: Test execute_callback behavior directly, no transport involved
execute_callback is plain PHP, test it exactly like any other function, no MCP
client, no HTTP request, no STDIO process needed:
// File: tests/test-execute-callback.php
class Test_Execute_Callback extends WP_UnitTestCase {
public function test_list_tickets_returns_expected_shape(): void {
self::factory()->post->create( array( 'post_type' => 'ticket', 'post_status' => 'publish', 'post_title' => 'Sample' ) );
$ability = wp_get_ability( 'my-plugin/list-tickets' );
$result = $ability->execute( array( 'status' => 'open' ) );
$this->assertIsArray( $result );
$this->assertArrayHasKey( 'title', $result[0] );
}
public function test_list_tickets_respects_status_filter(): void {
self::factory()->post->create( array( 'post_type' => 'ticket', 'post_status' => 'draft' ) );
$ability = wp_get_ability( 'my-plugin/list-tickets' );
$result = $ability->execute( array( 'status' => 'open' ) );
$this->assertCount( 0, $result );
}
}
Step 5: Wire it into CI
# File: .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
phpunit:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.1', '8.3']
wordpress: ['6.9', 'latest']
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- name: Start wp-env
run: npx wp-env start
- name: Run PHPUnit
run: npx wp-env run tests-cli --env-cwd=wp-content/plugins/my-plugin phpunit
Test it
Deliberately break something (comment out current_user_can() in a
permission_callback, for instance), rerun the suite locally, and confirm
test_subscriber_cannot_call_ability fails loudly:
wp-env run tests-cli --env-cwd=wp-content/plugins/my-plugin phpunit --filter Test_Permission_Callback
A red test here is exactly the signal this whole pipeline exists to produce.
Spinning up an actual MCP client connection inside a unit test is slow, flaky, and
tests the adapter’s own transport code, not your plugin’s logic. Test
permission_callback and execute_callback directly through WP_Ability::has_permission()
and WP_Ability::execute(), and save real end-to-end MCP client tests (if you need
them at all) for a small, separate, slower suite.
Recap
A CI pipeline for an MCP plugin is a standard WordPress PHPUnit setup (wp scaffold plugin-tests, WP_UnitTestCase, wp-env run tests-cli phpunit) pointed at three
specific concerns: registration matches expectations (including meta.mcp.public
staying where you left it), permission_callback behaves correctly across real user
roles, and execute_callback returns the right shape for the right input. None of it
needs a live MCP client, since abilities are testable as plain PHP.
Resources & further reading
- wp scaffold plugin-tests, developer.wordpress.org
- Plugin unit tests handbook, make.wordpress.org
- MCP Adapter repository, GitHub