plugin = $plugin; } public function register(): void { add_action('admin_menu', function (): void { add_submenu_page('aiwend', __('MCP', 'aiwend'), esc_html__('MCP', 'aiwend'), Capabilities::MANAGE, self::SLUG, [$this, 'render']); }); add_action('admin_post_aiwend_mcp_save', [$this, 'save']); } private function may(): bool { return $this->plugin->modules->isEnabled(McpModule::ID) && current_user_can(Capabilities::MANAGE); } public function save(): void { if (!$this->may()) { wp_die(esc_html__('Not authorized.', 'aiwend'), '', ['response' => 403]); } check_admin_referer('aiwend_mcp_save'); $catalog = McpConfig::catalog($this->plugin); $posted = isset($_POST['tools']) && is_array($_POST['tools']) ? array_map('sanitize_key', wp_unslash($_POST['tools'])) : []; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- verified above; each value is passed through sanitize_key and intersected with the registered names McpConfig::saveEnabled($catalog, array_values($posted)); $this->plugin->audit->record(get_current_blog_id(), $this->plugin->currentPrincipal()->actorRef(), 'mcp.tools_updated', 'mcp', 'ok', null, (string) count(McpConfig::enabled($catalog)) . ' enabled'); wp_safe_redirect(add_query_arg(['page' => self::SLUG, 'saved' => '1'], admin_url('admin.php'))); exit; } public function render(): void { if (!$this->may()) { wp_die(esc_html__('Not authorized.', 'aiwend'), '', ['response' => 403]); } nocache_headers(); AdminView::open($this->plugin, self::SLUG, __('MCP', 'aiwend'), __('Lets an AI app that you connect read a few facts about this site. It can only read, and only what you tick below.', 'aiwend')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice flag if (isset($_GET['saved'])) { echo '

' . esc_html__('Saved.', 'aiwend') . '

'; } $catalog = McpConfig::catalog($this->plugin); $on = McpConfig::enabled($catalog); echo '

' . esc_html__('Connect an app', 'aiwend') . '

'; echo '

' . esc_html__('Address of the endpoint (POST, JSON):', 'aiwend') . ' ' . esc_html(McpConfig::endpointUrl()) . '

'; echo '

' . esc_html__('The app signs in with an application password of an administrator (Users, your profile, Application Passwords). Browser sessions are refused. Revoke the password there to disconnect the app. Supported protocol versions: 2025-11-25 and 2026-07-28.', 'aiwend') . '

'; echo '

' . esc_html__('WordPress offers application passwords only over HTTPS, or on a local development site (a development or local environment type). On a plain HTTP production site an app cannot sign in.', 'aiwend') . '

'; if (function_exists('wp_is_application_passwords_available') && !wp_is_application_passwords_available()) { echo '

' . esc_html__('Application passwords are not available on this site right now, so an app cannot sign in. Serve the site over HTTPS, or use a local development environment.', 'aiwend') . '

'; } echo '
'; $userLimit = max(0, (int) apply_filters('aiwend_mcp_user_limit', 60)); $siteLimit = max(0, (int) apply_filters('aiwend_mcp_site_limit', 300)); echo '

' . esc_html__('What a connected app can and cannot do', 'aiwend') . '

'; echo '
'; wp_nonce_field('aiwend_mcp_save'); echo '
' . esc_html__('Tools the endpoint offers', 'aiwend') . ''; foreach ($catalog->all() as $name => $tool) { $id = 'aw-mcp-' . $name; echo '


' . esc_html($tool->description()) . ' ' . ($catalog->isBuiltin($name) ? esc_html__('Read-only.', 'aiwend') : '' . esc_html__('Added by another plugin. Aiwend cannot check what it does; tick it only if you trust that plugin.', 'aiwend') . '') . '

'; } echo '
'; foreach ($catalog->refused() as $r) { echo '

' . esc_html(sprintf( /* translators: 1: tool name offered by another plugin, 2: reason code */ __('A tool "%1$s" from another plugin was not accepted (%2$s).', 'aiwend'), $r['name'], $r['reason'] )) . '

'; } echo '

'; AdminView::close(); } }