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:
@@ -6,6 +6,7 @@ namespace LIT\MCP\Abilities;
|
||||
|
||||
use LIT\MCP\Access;
|
||||
use LIT\MCP\Integrations\IntegrationRegistry;
|
||||
use LIT\MCP\Server\RequestContext;
|
||||
use Throwable;
|
||||
use WP_Error;
|
||||
|
||||
@@ -55,6 +56,10 @@ final class AbilityRegistrar
|
||||
return Errors::forbidden('Dein Benutzerkonto ist für LIT MCP nicht freigegeben.');
|
||||
}
|
||||
|
||||
if (! RequestContext::isOAuthServerRequest()) {
|
||||
return Errors::forbidden('LIT MCP ist nur über den OAuth-geschützten MCP-Server nutzbar.');
|
||||
}
|
||||
|
||||
return $ability->permission(is_array($input) ? $input : []);
|
||||
},
|
||||
'execute_callback' => static function (mixed $input = null) use ($ability): mixed {
|
||||
|
||||
@@ -11,6 +11,9 @@ final class IntegrationRegistry
|
||||
/** @var list<Integration> */
|
||||
private array $integrations;
|
||||
|
||||
/** @var list<Ability>|null */
|
||||
private ?array $abilities = null;
|
||||
|
||||
public function __construct(Integration ...$integrations)
|
||||
{
|
||||
$this->integrations = array_values($integrations);
|
||||
@@ -28,12 +31,16 @@ final class IntegrationRegistry
|
||||
/** @return list<Ability> */
|
||||
public function abilities(): array
|
||||
{
|
||||
$abilities = [];
|
||||
foreach ($this->available() as $integration) {
|
||||
array_push($abilities, ...$integration->abilities());
|
||||
if ($this->abilities === null) {
|
||||
$abilities = [];
|
||||
foreach ($this->available() as $integration) {
|
||||
array_push($abilities, ...$integration->abilities());
|
||||
}
|
||||
|
||||
$this->abilities = $abilities;
|
||||
}
|
||||
|
||||
return $abilities;
|
||||
return $this->abilities;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
|
||||
@@ -8,6 +8,7 @@ use LIT\MCP\Abilities\AbilityRegistrar;
|
||||
use LIT\MCP\Guides\InstructionsBuilder;
|
||||
use LIT\MCP\Integrations\Core\CoreIntegration;
|
||||
use LIT\MCP\Integrations\IntegrationRegistry;
|
||||
use LIT\MCP\Server\RequestContext;
|
||||
use LIT\MCP\Server\ServerRegistrar;
|
||||
|
||||
/**
|
||||
@@ -15,6 +16,8 @@ use LIT\MCP\Server\ServerRegistrar;
|
||||
*/
|
||||
final class Plugin
|
||||
{
|
||||
public const VERSION = '0.1.0';
|
||||
|
||||
private static string $pluginFile = '';
|
||||
|
||||
public static function boot(string $pluginFile): void
|
||||
@@ -28,6 +31,7 @@ final class Plugin
|
||||
}
|
||||
|
||||
(new Access())->register();
|
||||
(new RequestContext())->register();
|
||||
|
||||
$registry = new IntegrationRegistry(
|
||||
new CoreIntegration(self::pluginDir() . '/docs/guides')
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Server;
|
||||
|
||||
use WP_REST_Request;
|
||||
|
||||
/**
|
||||
* Tracks whether the current REST dispatch is serving our OAuth-protected MCP
|
||||
* server route (`/mcp/mcp-oauth-server`).
|
||||
*
|
||||
* Abilities must never run outside that route – not through mcp-adapter's
|
||||
* (disabled) default server, not through a differently configured server, and
|
||||
* not through any unrelated REST request. WordPress fires
|
||||
* `rest_request_before_callbacks` for every matched REST route before any
|
||||
* permission_callback runs (see WP_REST_Server::dispatch()), so by the time an
|
||||
* ability's permission_callback executes – itself only reachable from inside
|
||||
* the MCP route's own callback – the flag below is already accurate for the
|
||||
* current request.
|
||||
*/
|
||||
final class RequestContext
|
||||
{
|
||||
private static bool $isOAuthServerRequest = false;
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
add_filter('rest_request_before_callbacks', [self::class, 'recordRoute'], 10, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $response
|
||||
* @param array<string, mixed> $handler
|
||||
* @return mixed
|
||||
*/
|
||||
public static function recordRoute(mixed $response, array $handler, WP_REST_Request $request): mixed
|
||||
{
|
||||
if ($request->get_route() === self::route()) {
|
||||
self::$isOAuthServerRequest = true;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public static function isOAuthServerRequest(): bool
|
||||
{
|
||||
return self::$isOAuthServerRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test-only: resets (or forces) the recorded state between unit tests.
|
||||
*/
|
||||
public static function reset(bool $isOAuthServerRequest = false): void
|
||||
{
|
||||
self::$isOAuthServerRequest = $isOAuthServerRequest;
|
||||
}
|
||||
|
||||
private static function route(): string
|
||||
{
|
||||
return '/' . ServerRegistrar::NAMESPACE . '/' . ServerRegistrar::ROUTE;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace LIT\MCP\Server;
|
||||
|
||||
use LIT\MCP\Guides\InstructionsBuilder;
|
||||
use LIT\MCP\Integrations\IntegrationRegistry;
|
||||
use LIT\MCP\Plugin;
|
||||
use WP\MCP\Core\McpAdapter;
|
||||
use WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler;
|
||||
use WPMedia\MCP\OAuth\Context as OAuthContext;
|
||||
@@ -41,13 +42,38 @@ final class ServerRegistrar
|
||||
/** @var callable(): bool */
|
||||
private $oauthEnabled;
|
||||
|
||||
/** @var callable(array<string, mixed>): mixed */
|
||||
private $createAdapterServer;
|
||||
|
||||
/** @var callable(string): bool */
|
||||
private $classExists;
|
||||
|
||||
public function __construct(
|
||||
private IntegrationRegistry $registry,
|
||||
private InstructionsBuilder $instructions,
|
||||
?callable $oauthEnabled = null
|
||||
?callable $oauthEnabled = null,
|
||||
?callable $createAdapterServer = null,
|
||||
?callable $classExists = null
|
||||
) {
|
||||
$this->oauthEnabled = $oauthEnabled
|
||||
?? static fn (): bool => (new OAuthContext())->is_enabled();
|
||||
$this->classExists = $classExists
|
||||
?? static fn (string $class): bool => class_exists($class);
|
||||
$this->createAdapterServer = $createAdapterServer
|
||||
?? static function (array $args): mixed {
|
||||
return McpAdapter::instance()->create_server(
|
||||
$args['id'],
|
||||
$args['namespace'],
|
||||
$args['route'],
|
||||
$args['name'],
|
||||
$args['description'],
|
||||
$args['version'],
|
||||
[OAuthHttpTransport::class],
|
||||
ErrorLogMcpErrorHandler::class,
|
||||
(new OAuthContext())->observability_handler(),
|
||||
$args['tools']
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
@@ -75,7 +101,7 @@ final class ServerRegistrar
|
||||
'route' => self::ROUTE,
|
||||
'name' => 'LIT MCP',
|
||||
'description' => $this->instructions->build(),
|
||||
'version' => '0.1.0',
|
||||
'version' => Plugin::VERSION,
|
||||
'tools' => array_merge($this->registry->directAbilityNames(), self::META_TOOLS),
|
||||
];
|
||||
}
|
||||
@@ -97,18 +123,7 @@ final class ServerRegistrar
|
||||
|
||||
$this->unhookOAuthDefaultServer();
|
||||
|
||||
$result = McpAdapter::instance()->create_server(
|
||||
$args['id'],
|
||||
$args['namespace'],
|
||||
$args['route'],
|
||||
$args['name'],
|
||||
$args['description'],
|
||||
$args['version'],
|
||||
[OAuthHttpTransport::class],
|
||||
ErrorLogMcpErrorHandler::class,
|
||||
(new OAuthContext())->observability_handler(),
|
||||
$args['tools']
|
||||
);
|
||||
$result = ($this->createAdapterServer)($args);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
$message = '[wp-lit-mcp] MCP server registration failed: ' . $result->get_error_message();
|
||||
@@ -129,7 +144,7 @@ final class ServerRegistrar
|
||||
*/
|
||||
public function unhookOAuthDefaultServer(): void
|
||||
{
|
||||
if (! class_exists(OAuthServerRegistrar::class)) {
|
||||
if (! ($this->classExists)(OAuthServerRegistrar::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user