*/ final class Main { /** * The single instance of the class. * * @var AcrossAI_MCP_Manager * @since 0.0.1 */ protected static $_instance = null; /** * The autoloader instance. * * @since 0.0.1 * @access protected * @var Autoloader $autoloader The plugin autoloader instance. */ protected $autoloader; /** * The loader that's responsible for maintaining and registering all hooks that power * the plugin. * * @since 0.0.1 * @access protected * @var AcrossAI_MCP_Manager_Loader $loader Maintains and registers all hooks for the plugin. */ protected $loader; /** * The unique identifier of this plugin. * * @since 0.0.1 * @access protected * @var string $plugin_name The string used to uniquely identify this plugin. */ protected $plugin_name; /** * The plugin dir path * * @since 0.0.1 * @access protected * @var string $plugin_path The string for plugin dir path */ protected $plugin_path; /** * The current version of the plugin. * * @since 0.0.1 * @access protected * @var string $version The current version of the plugin. */ protected $version; /** * Plugin directory path. * * @var string */ protected $plugin_dir; /** * Define the core functionality of the plugin. * * Set the plugin name and the plugin version that can be used throughout the plugin. * Load the dependencies, define the locale, and set the hooks for the admin area and * the public-facing side of the site. * * @since 0.0.1 */ public function __construct() { $this->define_constants(); $this->version = ACROSSAI_MCP_MANAGER_VERSION; $this->plugin_name = 'acrossai-mcp-manager'; $this->plugin_dir = ACROSSAI_MCP_MANAGER_PLUGIN_PATH; $this->load_composer_dependencies(); $this->load_dependencies(); $this->set_locale(); $this->load_hooks(); } /** * Main AcrossAI_MCP_Manager Instance. * * Ensures only one instance of WooCommerce is loaded or can be loaded. * * @since 0.0.1 * @static * @see AcrossAI_MCP_Manager() * @return AcrossAI_MCP_Manager - Main instance. */ public static function instance() { if ( null === self::$_instance ) { self::$_instance = new self(); } return self::$_instance; } /** * Define WCE Constants */ private function define_constants() { $this->define( 'ACROSSAI_MCP_MANAGER_PLUGIN_BASENAME', plugin_basename( \ACROSSAI_MCP_MANAGER_PLUGIN_FILE ) ); $this->define( 'ACROSSAI_MCP_MANAGER_PLUGIN_PATH', plugin_dir_path( \ACROSSAI_MCP_MANAGER_PLUGIN_FILE ) ); $this->define( 'ACROSSAI_MCP_MANAGER_PLUGIN_URL', plugin_dir_url( \ACROSSAI_MCP_MANAGER_PLUGIN_FILE ) ); $this->define( 'ACROSSAI_MCP_MANAGER_PLUGIN_NAME_SLUG', 'acrossai-mcp-manager' ); $this->define( 'ACROSSAI_MCP_MANAGER_PLUGIN_NAME', 'AcrossAI MCP Manager' ); $this->define( 'ACROSSAI_MCP_MANAGER_VERSION', '0.0.6' ); } /** * Define constant if not already set * * @param string $name * @param string|bool $value */ private function define( $name, $value ) { if ( ! defined( $name ) ) { define( $name, $value ); } } /** * Register all the hook once all the active plugins are loaded * * Uses the plugins_loaded to load all the hooks and filters * * @since 0.0.1 * @access private */ public function load_hooks() { /** * Check if plugin can be loaded safely or not * * @since 0.0.1 */ if ( apply_filters( 'acrossai_mcp_manager_load', true ) ) { $this->bootstrap_database_tables(); $this->define_admin_hooks(); $this->define_public_hooks(); } } /** * Instantiate the four BerlinDB Table subclasses at request time. * * BerlinDB v3 requires the Table subclass to be instantiated so its * `sunrise()` / `set_prefixes()` boot registers the physical table * name with the DB interface. Without this, Query subclasses fall * back to using $table_alias as the FROM clause, producing * `Table 'db.' doesn't exist` errors at runtime. * * Matches the sibling plugin's boot pattern * (acrossai-abilities-manager Main::define_admin_hooks:349) but is * hoisted here so it also covers public/REST request paths * (MCP\Controller::has_any_enabled_server hits Query on rest_api_init). * * @since 0.0.1 * @return void */ private function bootstrap_database_tables() { \AcrossAI_MCP_Manager\Includes\Database\MCPServer\Table::instance(); \AcrossAI_MCP_Manager\Includes\Database\CliAuthLog\Table::instance(); \AcrossAI_MCP_Manager\Includes\Database\OAuthToken\Table::instance(); \AcrossAI_MCP_Manager\Includes\Database\OAuthAudit\Table::instance(); } /** * Load the required composer dependencies for this plugin. * * @since 0.0.1 * @access private */ private function load_composer_dependencies() { /** * Add composer file */ $plugin_path = ACROSSAI_MCP_MANAGER_PLUGIN_PATH; if ( file_exists( $plugin_path . 'vendor/autoload_packages.php' ) ) { require_once $plugin_path . 'vendor/autoload_packages.php'; } } /** * Load the required dependencies for this plugin. * * Include the following files that make up the plugin: * * - AcrossAI_MCP_Manager\Admin\Loader. Orchestrates the hooks of the plugin. * - AcrossAI_MCP_Manager\Admin\I18n. Defines internationalization functionality. * - AcrossAI_MCP_Manager\Admin\Main. Defines all hooks for the admin area. * - AcrossAI_MCP_Manager_Public. Defines all hooks for the public side of the site. * * Create an instance of the loader which will be used to register the hooks * with WordPress. * * @since 0.0.1 * @access private */ private function load_dependencies() { $this->loader = Loader::instance(); // Phase 5 — WP-CLI command for the OAuth cleanup sweep. Only // registered when running under WP-CLI; guarded inside the helper. if ( defined( 'WP_CLI' ) && \WP_CLI ) { \AcrossAI_MCP_Manager\Includes\OAuth\CliCommand::register(); } } /** * Define the locale for this plugin for internationalization. * * Uses the AcrossAI_MCP_Manager_i18n class in order to set the domain and to register the hook * with WordPress. * * @since 0.0.1 * @access private */ private function set_locale() { $i18n = new I18n(); // Now attach it to `init`, not `plugins_loaded` $this->loader->add_action( 'init', $i18n, 'do_load_textdomain' ); } /** * Register all of the hooks related to the admin area functionality * of the plugin. * * @since 0.0.1 * @access private */ private function define_admin_hooks() { $plugin_admin = \AcrossAI_MCP_Manager\Admin\Main::instance(); $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_styles' ); $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts' ); /** * Add the Plugin Submenus under the shared `acrossai` parent (Feature 010 — FR-021). * * Menu::register_submenu() registers MCP Manager (position 2), CLI Auth Log * (position 3), and Access Control (position 4, conditional). The shared * `acrossai` parent menu itself is auto-hooked internally by * \AcrossAI_Main_Menu\SettingsPage (bootstrapped from the plugin entry file * on plugins_loaded priority 0 per FR-029 / D15 / DEV4). * * plugin_action_links_ is the row-specific filter — fires * only for this plugin's row on the Plugins screen (FR-003). */ $menu = \AcrossAI_MCP_Manager\Admin\Partials\Menu::instance(); $this->loader->add_action( 'admin_menu', $menu, 'register_submenu' ); $this->loader->add_filter( 'plugin_action_links_' . ACROSSAI_MCP_MANAGER_PLUGIN_BASENAME, $menu, 'plugin_action_links', 10, 1 ); /** * Settings action handler hooks (US2 — Phase 2). * * `Settings` owns the list-page controller for MCP servers (toggle, * delete, create, bulk actions dispatched via * `?page=acrossai_mcp_manager&action=...`). It is deliberately kept * separate from `SettingsMenu` below, which owns the WordPress * Settings API surface on the shared `?page=acrossai-settings` page. * * - handle_actions runs at priority 5 on admin_init so it fires * BEFORE other admin_init handlers. * - render_action_result_notice consumes the `?notice=...` query var * set by handle_actions redirects (FR-016). */ $settings = \AcrossAI_MCP_Manager\Admin\Partials\Settings::instance(); // Auto-heal the default MCP server row (reference-plugin pattern). // Runs at priority 4 so the row exists before handle_actions/list-render. $this->loader->add_action( 'admin_init', $settings, 'maybe_seed_default_server', 4 ); $this->loader->add_action( 'admin_init', $settings, 'handle_actions', 5 ); /** * Shared AcrossAI Settings page — MCP tab (Feature 012). * * Registers the MCP tab on the vendor-owned `?page=acrossai-settings` * page via the `acrossai_settings_tabs` filter, and wires the tab's * three sections + toggles via the Settings API on admin_init. The * vendor's `SettingsPage` is guaranteed present at admin_init because * the acrossai-co/main-menu package is a hard-require in composer.json * (Feature 010 — D15 / DEV4). See DEC-VENDOR-SETTINGS-TAB-INTEGRATION. * * Loader-order note: this wiring lands after the Feature 011 * `bootstrap_database_tables()` call in load_hooks() (per * DEC-BERLINDB-TABLE-REQUEST-BOOT), so BerlinDB Tables are booted * before any admin_init handler runs. */ $settings_menu = \AcrossAI_MCP_Manager\Admin\Partials\SettingsMenu::instance(); $this->loader->add_filter( 'acrossai_settings_tabs', $settings_menu, 'register_tab' ); $this->loader->add_action( 'admin_init', $settings_menu, 'register_settings' ); /** * Admin notices — extracted to Admin\Partials\Notices per RT-2. * - render_action_result_notice consumes the `?notice=...` query var set by * Settings::handle_actions() redirects (FR-016) * - render_missing_adapter_notice + handle_adapter_notice_dismissal * together implement the dismissible adapter-missing warning (FR-015 + Q3) */ $notices = \AcrossAI_MCP_Manager\Admin\Partials\Notices::instance(); $this->loader->add_action( 'admin_notices', $notices, 'render_action_result_notice' ); /** * ApplicationPasswords REST routes (US3 — Phase 2). * Registers POST /generate-app-password + GET /list-app-passwords * on the rest_api_init action. The Tokens tab JS calls these via * wp.apiFetch with the per-user `wp_rest` nonce. */ $application_passwords = \AcrossAI_MCP_Manager\Admin\Partials\ApplicationPasswords::instance(); $this->loader->add_action( 'rest_api_init', $application_passwords, 'register_rest_routes' ); // Feature 015 — Access Control v2 adoption. Replaces the previous Phase 7 // TODO block that referenced v1's ::instance() static (fatal in v2). All // wiring flows through the plugin-scoped wrapper; the mcp_adapter_pre_tool_call // filter (D18) is the canonical MCP-boundary enforcement hook. // NB: `register_default_providers` filter is intentionally NOT wired — // the vendor's AccessControlManager::load_providers() already registers // WpRoleProvider + WpUserProvider + WpCapabilityProvider + BuddyBoss + // MemberPress as defaults. Third-party plugins can still hook the // `acrossai_mcp_access_control_providers` filter to append their own. $access_control = \AcrossAI_MCP_Manager\Includes\AccessControl\AcrossAI_MCP_Access_Control::instance(); $this->loader->add_action( 'init', $access_control, 'boot_manager', 5 ); $this->loader->add_action( 'rest_api_init', $access_control, 'register_rest_api' ); $this->loader->add_action( 'admin_notices', $access_control, 'maybe_show_library_notice' ); $this->loader->add_filter( 'mcp_adapter_pre_tool_call', $access_control, 'gate_mcp_tool_call', 10, 4 ); /** * Adapter-missing notice (US4 — FR-015 + Q3). Lives on Notices per RT-2. * - render_missing_adapter_notice runs unconditionally on admin_notices; * the renderer self-guards on class_exists('\WP\MCP\Plugin') AND on the * per-user user_meta dismissal flag (sticky, never resets on upgrade). * - handle_adapter_notice_dismissal is the admin-ajax endpoint called * when the user clicks the X button. Nonce + manage_options gated. */ $this->loader->add_action( 'admin_notices', $notices, 'render_missing_adapter_notice' ); $this->loader->add_action( 'admin_notices', $notices, 'render_oauth_https_notice' ); $this->loader->add_action( 'admin_notices', $notices, 'render_disable_wp_cron_notice' ); $this->loader->add_action( 'wp_ajax_acrossai_mcp_dismiss_adapter_notice', $notices, 'handle_adapter_notice_dismissal' ); // TODO (phase N): wire Admin\Partials\ApplicationPasswords. // $app_passwords = \AcrossAI_MCP_Manager\Admin\Partials\ApplicationPasswords::instance(); /** * Phase 4 gap closure — Feature-009 (2026-07-01). Boots the WP MCP * adapter singleton when at least one enabled MCP server row exists. * v0.0.4 wired this on `init` priority 1; the target design defers to * `rest_api_init` so `Plugin::instance()`'s internal REST route * registration lands during WordPress's REST bootstrap window. * Graceful when adapter package is absent (US3 — sets status * 'not-found'; `Notices::render_missing_adapter_notice()` handles the * admin banner separately). */ $mcp_controller = \AcrossAI_MCP_Manager\Includes\MCP\Controller::instance(); $this->loader->add_action( 'rest_api_init', $mcp_controller, 'initialize_adapter' ); // TODO (phase 5): wire REST\CliController. // $cli_controller = \AcrossAI_MCP_Manager\Includes\REST\CliController::instance(); // $this->loader->add_action( 'rest_api_init', $cli_controller, 'register_routes' ); // TODO (phase 6): wire Includes\OAuth\ClaudeConnectors (10 hooks). // $claude_connectors = \AcrossAI_MCP_Manager\Includes\OAuth\ClaudeConnectors::instance(); // $this->loader->add_action( 'init', $claude_connectors, 'register_rewrite_rules' ); // $this->loader->add_action( 'init', $claude_connectors, 'maybe_flush_rewrite_rules', 20 ); // $this->loader->add_filter( 'query_vars', $claude_connectors, 'add_query_vars' ); // $this->loader->add_filter( 'redirect_canonical', $claude_connectors, 'disable_canonical_redirects', 10, 2 ); // $this->loader->add_action( 'wp_enqueue_scripts', $claude_connectors, 'enqueue_assets' ); // $this->loader->add_action( 'template_redirect', $claude_connectors, 'handle_frontend_request' ); // $this->loader->add_action( 'rest_api_init', $claude_connectors, 'register_rest_routes' ); // $this->loader->add_filter( 'determine_current_user', $claude_connectors, 'determine_current_user_from_bearer', 20 ); // $this->loader->add_filter( 'rest_post_dispatch', $claude_connectors, 'decorate_mcp_response', 10, 3 ); // $this->loader->add_action( 'acrossai_mcp_access_denied', $claude_connectors, 'log_access_denied_event', 10, 4 ); } /** * Register all of the hooks related to the public-facing functionality * of the plugin. * * @since 0.0.1 * @access private */ private function define_public_hooks() { $plugin_public = \AcrossAI_MCP_Manager\Public\Main::instance(); $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_styles' ); $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_scripts' ); /** * Phase 5 — OAuth / Claude Connectors wiring (FR-021 / A1). * * Every OAuth hook trace MUST be in this method — feature classes * never call add_action / add_filter themselves. */ $claude_connectors = \AcrossAI_MCP_Manager\Includes\OAuth\ClaudeConnectors::instance(); $this->loader->add_action( 'init', $claude_connectors, 'register_rewrite_rules' ); $this->loader->add_filter( 'query_vars', $claude_connectors, 'add_query_var' ); $this->loader->add_action( 'template_redirect', $claude_connectors, 'serve_discovery_or_authorize', 9 ); $this->loader->add_action( 'acrossai_mcp_oauth_cleanup', $claude_connectors, 'handle_cleanup_event' ); $token_controller = \AcrossAI_MCP_Manager\Includes\OAuth\TokenController::instance(); $this->loader->add_action( 'rest_api_init', $token_controller, 'register_routes' ); $bearer_auth = \AcrossAI_MCP_Manager\Includes\OAuth\BearerAuth::instance(); $this->loader->add_filter( 'determine_current_user', $bearer_auth, 'resolve_bearer_token', 20 ); /** * Phase 6 — REST CLI Authentication Controller + Phase 6.0 FrontendAuth. * * Every CLI-flow hook trace MUST be in this method — feature classes * never call add_action / add_filter themselves (A1 / FR-021). */ $frontend_auth = \AcrossAI_MCP_Manager\Public\Partials\FrontendAuth::instance(); $this->loader->add_action( 'init', $frontend_auth, 'register_rewrite_rule' ); $this->loader->add_filter( 'query_vars', $frontend_auth, 'add_query_var' ); $this->loader->add_action( 'template_redirect', $frontend_auth, 'maybe_render_page', 10 ); $this->loader->add_action( 'wp_enqueue_scripts', $frontend_auth, 'enqueue_assets' ); $cli_controller = \AcrossAI_MCP_Manager\Includes\REST\CliController::instance(); $this->loader->add_action( 'rest_api_init', $cli_controller, 'register_routes' ); /** * Feature 013 — Public Renderer layer. * * Wires the REST endpoint + 3 shortcodes for the public client-config * renderers (NpmClientBlock, MCPClientsBlock, ClaudeConnectorBlock). * Third-party plugins (BuddyBoss, WooCommerce, etc.) consume via: * - do_action( 'acrossai_mcp_render_client_block', $slug, $server_id, $context ) * - apply_filters( 'acrossai_mcp_client_block_context', $context, $slug, $server_id ) * - apply_filters( 'acrossai_mcp_client_classes', $default_fqns ) * - Shortcodes: [acrossai_mcp_npm_block server=X], etc. * All @experimental until 1.0.0 per DEC-CLIENT-RENDERER-PUBLIC-API. */ $client_renderer_rest = \AcrossAI_MCP_Manager\Includes\REST\ClientRendererController::instance(); $this->loader->add_action( 'rest_api_init', $client_renderer_rest, 'register_rest_routes' ); $this->loader->add_action( 'init', $client_renderer_rest, 'register_shortcodes_and_actions' ); } /** * Run the loader to execute all of the hooks with WordPress. * * @since 0.0.1 */ public function run() { $this->loader->run(); } /** * The name of the plugin used to uniquely identify it within the context of * WordPress and to define internationalization functionality. * * @since 0.0.1 * @return string The name of the plugin. */ public function get_plugin_name() { return $this->plugin_name; } /** * The reference to the class that orchestrates the hooks with the plugin. * * @since 0.0.1 * @return AcrossAI_MCP_Manager_Loader Orchestrates the hooks of the plugin. */ public function get_loader() { return $this->loader; } /** * The reference to the autoloader instance. * * @since 0.0.1 * @return Autoloader The plugin autoloader instance. */ public function get_autoloader() { return $this->autoloader; } /** * Retrieve the version number of the plugin. * * @since 0.0.1 * @return string The version number of the plugin. */ public function get_version() { return $this->version; } }