$this->slug(), 'risk' => 'none', 'summary' => "Reports the site's runtime and health context: WordPress/PHP/DB versions, " . 'environment type, active theme, plugin counts, pending updates, the latest Site ' . 'Health test summary if available, and which Agent Toolbelt abilities are enabled. ' . 'Read-only; call first to understand the site before acting.', 'annotations' => array( 'readonly' => true, ), 'input_schema' => array( // Plain 'object' with an empty default: a parameterless REST GET run // passes input=null and core fills the schema default before validating // (WP_Ability::normalize_input, since 6.9). The former ['object','null'] // made the MCP Adapter wrap this tool's input in a required "input" // object (SchemaTransformer wraps any non-'object' type) — v1.6.0. // The default spells out dry_run=false (what an absent input means // for a read-only ability) rather than an empty array: the adapter's // schema normalizer renders an empty array as [] on an object schema. 'type' => 'object', 'default' => array( 'dry_run' => false ), 'properties' => array(), ), ); } public function dry_run( $atb_input ) { return $this->output( true, true, $this->gather() ); } public function run( $atb_input ) { return $this->output( true, false, $this->gather() ); } /** * @return array */ protected function gather() { global $wp_version, $wpdb; return array( 'wp_version' => isset( $wp_version ) ? (string) $wp_version : '', 'php_version' => phpversion(), 'db_version' => ( isset( $wpdb ) && method_exists( $wpdb, 'db_version' ) ) ? (string) $wpdb->db_version() : '', 'environment' => function_exists( 'wp_get_environment_type' ) ? wp_get_environment_type() : 'production', 'active_theme' => $this->active_theme_name(), 'active_plugins' => count( (array) get_option( 'active_plugins', array() ) ), 'pending_updates' => $this->pending_updates(), 'site_health' => $this->site_health(), 'health_fresh' => $this->health_fresh(), 'maintenance' => $this->maintenance_state(), 'toolbelt' => $this->toolbelt_info(), ); } /** * The only read path for the maintenance-mode window (v1.4 F4): the * maintenance-mode ability's own dry-run previews a CHANGE, it does * not report current state — so an agent checking "did my window * stick / is it still open?" looks here. * * @return array { active: bool, expires_at: int|null, seconds_remaining: int } */ protected function maintenance_state() { if ( ! class_exists( 'ATB_Operation_Maintenance_Mode' ) ) { return array( 'active' => false, 'expires_at' => null, 'seconds_remaining' => 0 ); } $atb_stored = get_option( ATB_Operation_Maintenance_Mode::OPTION_NAME, false ); $atb_remaining = ATB_Operation_Maintenance_Mode::seconds_remaining( $atb_stored, time() ); return array( 'active' => $atb_remaining > 0, 'expires_at' => $atb_remaining > 0 ? (int) $atb_stored : null, 'seconds_remaining' => $atb_remaining, ); } /** * The toolbelt's own version, enabled abilities (build-plan §2 * delta 1) and observe-mode state (v1.1): the agent's first call * reveals what operations are available — and whether real mutations * would currently be refused — without a second discovery step. * * @return array { version: string, enabled_abilities: string[], observe_mode: bool } */ protected function toolbelt_info() { $atb_enabled = array(); $atb_observe = false; if ( class_exists( 'Agent_Toolbelt' ) ) { $atb_enabled = array_keys( Agent_Toolbelt::get_operations() ); $atb_observe = Agent_Toolbelt::is_observe_mode(); } return array( 'version' => defined( 'AGENT_TOOLBELT_VERSION' ) ? AGENT_TOOLBELT_VERSION : '', 'enabled_abilities' => $atb_enabled, 'observe_mode' => $atb_observe, // Agents are told to call site-status first, so the calling // conventions travel with it (v1.3 D4). 'agent_protocol' => self::agent_protocol(), ); } /** * The calling conventions, keyed by topic. The single source: quoted * here, in the MCP server instructions (Agent_Toolbelt_MCP, v1.6.0) * and by the readme. Agent-facing English, deliberately not * translated (same as ability summaries). * * @return array { dry_run, confirm, refusals, rest_get, audit } */ public static function agent_protocol() { return array( 'dry_run' => 'Every mutating ability accepts "dry_run": true; high-risk abilities default to dry-run, so a real execution needs an explicit "dry_run": false.', 'confirm' => 'A high-risk dry run returns a one-time confirm_token; execute by re-sending the SAME input plus "dry_run": false and "confirm": .', 'refusals' => 'details.refused=true is an answer, not an error. reason: observe_mode = site is read-only for agents; rate_budget = wait retry_after_s seconds; confirm_token = redo the dry run; disabled = ask the site owner; loopback_unavailable = the site cannot reach itself over HTTP (basic auth, WAF, blocked loopback), so the recover abilities\' safety net cannot work — ask the site owner.', 'rest_get' => 'Read-only abilities run via GET with PHP-array query input, e.g. ?input[limit]=3 — bare query args like ?limit=3 are silently ignored. Mutating abilities run via POST with {"input":{...}}.', 'audit' => 'Every call lands in an audit log the site owner reads; agent-toolbelt/read-audit-log shows it to you too.', ); } /** * @return string */ protected function active_theme_name() { if ( ! function_exists( 'wp_get_theme' ) ) { return ''; } $atb_theme = wp_get_theme(); return is_object( $atb_theme ) && method_exists( $atb_theme, 'get' ) ? (string) $atb_theme->get( 'Name' ) : ''; } /** * Pending-update counts. get_core_updates()/get_plugin_updates()/ * get_theme_updates() live in wp-admin/includes/update.php — required * manually since this ability can run outside wp-admin. * * @return array { core: bool, plugins: int, themes: int } */ protected function pending_updates() { if ( ! function_exists( 'get_core_updates' ) && file_exists( ABSPATH . 'wp-admin/includes/update.php' ) ) { require_once ABSPATH . 'wp-admin/includes/update.php'; } $atb_core_pending = false; if ( function_exists( 'get_core_updates' ) ) { $atb_core_updates = get_core_updates(); if ( is_array( $atb_core_updates ) ) { foreach ( $atb_core_updates as $atb_update ) { if ( isset( $atb_update->response ) && 'upgrade' === $atb_update->response ) { $atb_core_pending = true; break; } } } } return array( 'core' => $atb_core_pending, 'plugins' => function_exists( 'get_plugin_updates' ) ? count( (array) get_plugin_updates() ) : 0, 'themes' => function_exists( 'get_theme_updates' ) ? count( (array) get_theme_updates() ) : 0, ); } /** * @return array|null { good: int, recommended: int, critical: int, checked_at: null } */ protected function site_health() { $atb_raw = get_transient( 'health-check-site-status-result' ); if ( false === $atb_raw || '' === $atb_raw ) { return null; } $atb_decoded = json_decode( is_string( $atb_raw ) ? $atb_raw : wp_json_encode( $atb_raw ), true ); if ( ! is_array( $atb_decoded ) ) { return null; } return array( 'good' => isset( $atb_decoded['good'] ) ? (int) $atb_decoded['good'] : 0, 'recommended' => isset( $atb_decoded['recommended'] ) ? (int) $atb_decoded['recommended'] : 0, 'critical' => isset( $atb_decoded['critical'] ) ? (int) $atb_decoded['critical'] : 0, 'checked_at' => null, ); } /** * Timestamped counts from the last full run-site-health-checks run * (v1.2 §3.6) — unlike core's cached numbers above, these carry a real * checked_at, so agents can tell how fresh the picture is. * * @return array|null { counts: {good,recommended,critical}, checked_at: string } */ protected function health_fresh() { $atb_snapshot = get_option( 'agent_toolbelt_health_snapshot' ); if ( ! is_array( $atb_snapshot ) || ! isset( $atb_snapshot['counts'], $atb_snapshot['checked_at'] ) ) { return null; } return array( 'counts' => $atb_snapshot['counts'], 'checked_at' => (string) $atb_snapshot['checked_at'], ); } } }