fix: close etappe-0 whole-branch review findings

- Autoloader: add automattic/jetpack-autoloader and load
  vendor/autoload_packages.php; require wordpress/mcp-adapter directly (^0.5).
- ServerRegistrar: make the adapter create_server() call and the
  OAuthServerRegistrar class_exists() check injectable so createServer()'s
  unhook-before-create ordering and the "mcp-oauth absent" branch are unit
  tested; add a smoke-test check that booting REST triggers no
  _doing_it_wrong().
- mcp-smoke.sh: assert mcp-adapter's default server route 404s, and that the
  subscriber role-gate denial actually carries our/the adapter's denial text
  (not just any isError/error).
- Guard tests/Integration/mint-token.php and tec-probe.php with
  `defined('WP_CLI') && WP_CLI || exit;`; add .gitattributes export-ignore
  for tests/docs-superpowers/.superpowers/phpunit/phpcs config.
- Security: add Server/RequestContext, tracking via
  rest_request_before_callbacks whether the current REST dispatch is our
  OAuth-protected /mcp/mcp-oauth-server route; AbilityRegistrar's
  permission_callback now denies (after the role gate) when abilities are
  invoked outside that route. Wired in Plugin::boot().
- Minors: single Plugin::VERSION constant used by ServerRegistrar;
  IntegrationRegistry caches its computed abilities list; tec-probe.php
  checks wp_insert_post's result and cleans up its probe post in a
  try/finally.

composer test: 47/47 green. composer lint: clean. Full mcp-smoke.sh run ends
ALL CHECKS PASSED (plus the expected /.well-known WARN).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-25 19:23:09 +02:00
co-authored by Claude Opus 5.5
parent 2c03286ba0
commit 0e3332ad3e
17 changed files with 430 additions and 38 deletions
+61
View File
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Server;
use Brain\Monkey\Functions;
use LIT\MCP\Server\RequestContext;
use LIT\MCP\Tests\Unit\TestCase;
use WP_REST_Request;
final class RequestContextTest extends TestCase
{
protected function tearDown(): void
{
RequestContext::reset();
parent::tearDown();
}
public function testIsOAuthServerRequestIsFalseByDefault(): void
{
self::assertFalse(RequestContext::isOAuthServerRequest());
}
public function testRecordRouteMarksTheOAuthServerRoute(): void
{
$request = new WP_REST_Request('/mcp/mcp-oauth-server');
RequestContext::recordRoute(null, [], $request);
self::assertTrue(RequestContext::isOAuthServerRequest());
}
public function testRecordRouteIgnoresOtherRoutes(): void
{
$request = new WP_REST_Request('/wp/v2/users/me');
RequestContext::recordRoute(null, [], $request);
self::assertFalse(RequestContext::isOAuthServerRequest());
}
public function testRecordRouteReturnsResponseUnchanged(): void
{
$response = new \stdClass();
$request = new WP_REST_Request('/mcp/mcp-oauth-server');
$result = RequestContext::recordRoute($response, [], $request);
self::assertSame($response, $result);
}
public function testRegisterHooksRecordRouteOnRestRequestBeforeCallbacks(): void
{
Functions\expect('add_filter')
->once()
->with('rest_request_before_callbacks', [RequestContext::class, 'recordRoute'], 10, 3);
(new RequestContext())->register();
}
}
+65
View File
@@ -146,4 +146,69 @@ final class ServerRegistrarTest extends TestCase
(new ServerRegistrar($this->registry(), $this->instructions()))->unhookOAuthDefaultServer();
}
public function testUnhookOAuthDefaultServerIsNoOpWhenOAuthPackageIsAbsent(): void
{
$oauthRegistrar = Mockery::mock(OAuthServerRegistrar::class);
$oauthCallback = [$oauthRegistrar, 'register_server'];
$hook = new class {
/** @var array<int, array<string, array{function: mixed, accepted_args: int}>> */
public array $callbacks = [];
};
$hook->callbacks = [10 => ['oauth-id' => ['function' => $oauthCallback, 'accepted_args' => 0]]];
$GLOBALS['wp_filter']['mcp_adapter_init'] = $hook;
Functions\expect('remove_action')->never();
// Simulate wp-media/mcp-oauth not being installed via the injectable
// classExists probe (class_exists() is a PHP internal and cannot be
// stubbed directly through Brain\Monkey/Patchwork).
$registrar = new ServerRegistrar(
$this->registry(),
$this->instructions(),
null,
null,
static fn (): bool => false
);
$registrar->unhookOAuthDefaultServer();
}
public function testCreateServerUnhooksOAuthDefaultServerBeforeCreatingAdapterServer(): void
{
Functions\when('doing_action')->justReturn(true);
Functions\when('is_wp_error')->justReturn(false);
$oauthRegistrar = Mockery::mock(OAuthServerRegistrar::class);
$oauthCallback = [$oauthRegistrar, 'register_server'];
$hook = new class {
/** @var array<int, array<string, array{function: mixed, accepted_args: int}>> */
public array $callbacks = [];
};
$hook->callbacks = [10 => ['oauth-id' => ['function' => $oauthCallback, 'accepted_args' => 0]]];
$GLOBALS['wp_filter']['mcp_adapter_init'] = $hook;
/** @var list<string> $order */
$order = [];
Functions\expect('remove_action')
->once()
->with('mcp_adapter_init', $oauthCallback, 10)
->andReturnUsing(static function () use (&$order): void {
$order[] = 'unhook';
});
$registrar = new ServerRegistrar(
$this->registry(),
$this->instructions(),
static fn (): bool => true,
static function (array $args) use (&$order): bool {
$order[] = 'create';
return true;
}
);
$registrar->createServer();
self::assertSame(['unhook', 'create'], $order, 'unhook must run before the adapter creates the server');
}
}