`, with label, description, input * schema and annotations), and appends the calling conventions to the server * description, which the adapter sends to MCP clients as the server * `instructions` (InitializeHandler; verified in adapter 0.7.0 and in * WooCommerce's bundled copy). * * Nothing here executes or authorizes anything: a tool call still runs * WP_Ability::check_permissions() and Agent_Toolbelt::execute() exactly as a * REST call does. The adapter resolves every name through wp_get_ability() * and logs-and-skips unknown ones, so a stale name can never break the * server. */ // Exit if accessed directly. if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'Agent_Toolbelt_MCP' ) ) { class Agent_Toolbelt_MCP { /** * The adapter's default-server configuration filter (since adapter 0.3.0). */ const CONFIG_FILTER = 'mcp_adapter_default_server_config'; /** * The adapter's defaults as handed to the first filter callback. A * callback is allowed to return only its overrides (the adapter merges * the result with the defaults), which discards everything added * before it — so the defaults are remembered first and merged last. * * @var array|null */ protected static $atb_defaults = null; /** * Hooks both callbacks: remember first, extend last. */ public static function register() { add_filter( self::CONFIG_FILTER, array( __CLASS__, 'remember_defaults' ), -10000 ); add_filter( self::CONFIG_FILTER, array( __CLASS__, 'extend_config' ), 10000 ); } /** * Filter callback (earliest priority): keeps a copy of the defaults. * * @param mixed $atb_config * @return mixed Unchanged. */ public static function remember_defaults( $atb_config ) { if ( is_array( $atb_config ) ) { self::$atb_defaults = $atb_config; } return $atb_config; } /** * Filter callback (latest priority): adds the enabled MCP-public * abilities to the default server's tool list and the calling * conventions to its description. Appends, never overwrites. * * @param mixed $atb_config * @return mixed */ public static function extend_config( $atb_config ) { if ( ! is_array( $atb_config ) ) { return $atb_config; } if ( class_exists( 'Agent_Toolbelt' ) && Agent_Toolbelt::is_kill_switch_active() ) { return $atb_config; } $atb_names = self::tool_names(); if ( array() === $atb_names ) { return $atb_config; } /** * Whether the enabled abilities are listed as first-class tools on * the adapter's default MCP server (default true). False keeps a * site on the adapter's three meta-tools only; the abilities stay * reachable through execute-ability. * * @since 1.6.0 * * @param bool $atb_direct */ if ( (bool) apply_filters( 'agent_toolbelt_mcp_direct_tools', true ) ) { $atb_tools = isset( $atb_config['tools'] ) && is_array( $atb_config['tools'] ) ? $atb_config['tools'] : self::default_item( 'tools', array() ); $atb_config['tools'] = self::merge_names( $atb_tools, $atb_names ); } $atb_description = isset( $atb_config['server_description'] ) && is_string( $atb_config['server_description'] ) ? $atb_config['server_description'] : (string) self::default_item( 'server_description', '' ); $atb_config['server_description'] = self::append_instructions( $atb_description ); return $atb_config; } /** * The registered `agent-toolbelt/*` abilities the adapter may expose: * the same rule as the adapter's McpAbilityExposure (meta.mcp.public * when set, else meta.public), read from the live registry so the * kill switch, disabled abilities and the agent_toolbelt_mcp_public * filter are all honoured without a second copy of that logic. * * @return string[] Ability names, registration order. */ public static function tool_names() { if ( ! function_exists( 'wp_get_abilities' ) ) { return array(); } $atb_names = array(); foreach ( (array) wp_get_abilities() as $atb_ability ) { if ( ! is_object( $atb_ability ) || ! method_exists( $atb_ability, 'get_name' ) || ! method_exists( $atb_ability, 'get_meta' ) ) { continue; } $atb_name = (string) $atb_ability->get_name(); if ( 0 !== strpos( $atb_name, Agent_Toolbelt_Abilities::NAMESPACE_PREFIX ) ) { continue; } if ( ! self::is_meta_public( $atb_ability->get_meta() ) ) { continue; } $atb_names[] = $atb_name; } return $atb_names; } /** * Mirror of the adapter's exposure rule (McpAbilityExposure::is_meta_public). * * @param mixed $atb_meta * @return bool */ public static function is_meta_public( $atb_meta ) { if ( ! is_array( $atb_meta ) ) { return false; } $atb_mcp = isset( $atb_meta['mcp'] ) ? $atb_meta['mcp'] : array(); if ( ! is_array( $atb_mcp ) ) { return false; } if ( isset( $atb_mcp['public'] ) ) { return (bool) $atb_mcp['public']; } return isset( $atb_meta['public'] ) && true === $atb_meta['public']; } /** * The calling conventions for an MCP client, quoted from the single * source (site-status's agent_protocol) plus the one MCP-specific * line: the direct tool names. Plain English, deliberately not * translated (agent-facing, like ability descriptions). * * @return string */ public static function instructions() { $atb_protocol = class_exists( 'ATB_Operation_Site_Status' ) ? ATB_Operation_Site_Status::agent_protocol() : array(); $atb_lines = array( 'Agent Toolbelt (guarded site maintenance) is installed. Its tools are named agent-toolbelt-; prefer them over execute-ability.', '1. Call agent-toolbelt-site-status first: versions, pending updates, enabled abilities, observe mode, and toolbelt.agent_protocol.', ); $atb_n = 2; foreach ( array( 'dry_run', 'confirm', 'refusals', 'audit' ) as $atb_key ) { if ( ! empty( $atb_protocol[ $atb_key ] ) ) { $atb_lines[] = $atb_n . '. ' . $atb_protocol[ $atb_key ]; $atb_n++; } } $atb_lines[] = $atb_n . '. Pass a tool\'s input as the JSON object its schema (and examples) describe; unknown keys are refused, not ignored.'; return implode( "\n", $atb_lines ); } /** * Appends the instructions to a server description once. * * @param string $atb_description * @return string */ public static function append_instructions( $atb_description ) { $atb_description = (string) $atb_description; $atb_instructions = self::instructions(); if ( false !== strpos( $atb_description, $atb_instructions ) ) { return $atb_description; } return '' === trim( $atb_description ) ? $atb_instructions : rtrim( $atb_description ) . "\n\n" . $atb_instructions; } /** * Appends ability names not already listed. Existing items are kept * as they are (the adapter also accepts McpTool instances there). * * @param array $atb_tools * @param string[] $atb_names * @return array */ public static function merge_names( $atb_tools, $atb_names ) { $atb_tools = array_values( (array) $atb_tools ); foreach ( $atb_names as $atb_name ) { if ( ! in_array( $atb_name, $atb_tools, true ) ) { $atb_tools[] = $atb_name; } } return $atb_tools; } /** * One remembered default, or the fallback when the filter never * passed through remember_defaults(). * * @param string $atb_key * @param mixed $atb_fallback * @return mixed */ protected static function default_item( $atb_key, $atb_fallback ) { return is_array( self::$atb_defaults ) && isset( self::$atb_defaults[ $atb_key ] ) ? self::$atb_defaults[ $atb_key ] : $atb_fallback; } /** * Test hook: forgets the remembered defaults. */ public static function reset() { self::$atb_defaults = null; } } }