): mixed */ private $createAdapterServer; /** @var callable(string): bool */ private $classExists; private ExposurePolicy $policy; public function __construct( private IntegrationRegistry $registry, private InstructionsBuilder $instructions, ?callable $oauthEnabled = null, ?callable $createAdapterServer = null, ?callable $classExists = null, ?ExposurePolicy $policy = null ) { $this->policy = $policy ?? new ExposurePolicy(); $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 { // mcp-adapter's default server is a second, unprotected (plain HttpTransport, // is_user_logged_in()) endpoint exposing execute-ability – and thus our own // abilities. We register our own OAuth-protected server instead; mcp-oauth's // ServerRegistrar registers the shared meta-abilities itself once this filter // disables the default server (see its ensure_shared_abilities_registered()). add_filter('mcp_adapter_create_default_server', '__return_false'); add_action('mcp_adapter_init', [$this, 'createServer'], 5); } /** @return array|null */ public function serverArguments(): ?array { if (! ($this->oauthEnabled)()) { return null; } return [ 'id' => self::SERVER_ID, 'namespace' => self::NAMESPACE, 'route' => self::ROUTE, 'name' => 'LIT MCP', 'description' => $this->instructions->build(), 'version' => Plugin::VERSION, 'tools' => array_merge($this->policy->directNames($this->registry->abilities()), self::META_TOOLS), ]; } public function createServer(): void { if ( ! doing_action('mcp_adapter_init') || ! class_exists(McpAdapter::class) || ! class_exists(OAuthHttpTransport::class) ) { return; } $args = $this->serverArguments(); if ($args === null) { return; } $this->unhookOAuthDefaultServer(); $result = ($this->createAdapterServer)($args); if (is_wp_error($result)) { $message = '[wp-lit-mcp] MCP server registration failed: ' . $result->get_error_message(); error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log } } /** * Removes wp-media/mcp-oauth's own `mcp_adapter_init` callback so it never attempts to * register its (now redundant) copy of the `mcp-oauth-server` server. * * Reads the raw `WP_Hook` for `mcp_adapter_init` from `$GLOBALS['wp_filter']` – there is no * public API to look up a hooked callback by the class of its bound object, only by the exact * callable, which we don't have here. Removing a same-action, later-priority callback while an * earlier-priority one for the same action is running is explicitly supported by * `WP_Hook::remove_filter()` (it calls `resort_active_iterations()` for exactly this case). * No-op if the hook has not fired yet, has no callbacks, or the mcp-oauth package is absent. */ public function unhookOAuthDefaultServer(): void { if (! ($this->classExists)(OAuthServerRegistrar::class)) { return; } $hook = $GLOBALS['wp_filter']['mcp_adapter_init'] ?? null; if (! is_object($hook) || ! isset($hook->callbacks) || ! is_array($hook->callbacks)) { return; } foreach ($hook->callbacks as $priority => $callbacks) { foreach ($callbacks as $callback) { $function = $callback['function'] ?? null; if (is_array($function) && ($function[0] ?? null) instanceof OAuthServerRegistrar) { remove_action('mcp_adapter_init', $function, $priority); } } } } }