*/ private $tools = array(); /** * Get singleton. * * @return Agentdesk_Registry */ public static function instance() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Load tool modules. */ public function load_tools() { $tool_files = array( 'includes/tools/core/site.php', 'includes/tools/core/content.php', 'includes/tools/core/media.php', 'includes/tools/core/taxonomies.php', 'includes/tools/core/users.php', 'includes/tools/core/comments.php', 'includes/tools/core/plugins.php', 'includes/tools/core/themes.php', 'includes/tools/core/options.php', 'includes/tools/core/diagnostics.php', 'includes/tools/core/debug.php', 'includes/tools/core/gateway.php', 'includes/tools/woocommerce/products.php', 'includes/tools/woocommerce/orders.php', 'includes/tools/elementor/basic.php', 'includes/tools/elementor/deep.php', 'includes/tools/integrations/acf-seo-cache.php', ); foreach ( $tool_files as $relative ) { $path = AGENTDESK_PLUGIN_DIR . $relative; if ( file_exists( $path ) ) { require_once $path; } } /** * Register additional MCP tools. * * @param Agentdesk_Registry $registry Registry instance. */ do_action( 'agentdesk_register_tools', $this ); $this->register_gateway_tools(); } /** * Register a tool. * * @param string $name Tool name. * @param array $schema Tool schema (description, inputSchema, domain, etc.). * @param callable $callback Execution callback. */ public function register( $name, $schema, $callback ) { $this->tools[ $name ] = array_merge( $schema, array( 'name' => $name, 'callback' => $callback, ) ); } /** * Get all tools. * * @return array */ public function all() { return $this->tools; } /** * Get a tool by name. * * @param string $name Tool name. * @return array|null */ public function get( $name ) { return isset( $this->tools[ $name ] ) ? $this->tools[ $name ] : null; } /** * Tools exposed directly in tools/list (gateway + essentials). * * @return array */ public function get_public_tools() { $always = array( 'site_info', 'health_check', 'audit_recent', 'agentdesk_discover', 'agentdesk_info', 'agentdesk_run', 'agentdesk_batch' ); $result = array(); foreach ( $always as $name ) { if ( isset( $this->tools[ $name ] ) ) { $result[] = $this->format_tool_for_mcp( $this->tools[ $name ] ); } } return $result; } /** * Discover tools by query/domain. * * @param array $args Arguments. * @return array */ public function discover( $args ) { $query = isset( $args['query'] ) ? strtolower( (string) $args['query'] ) : ''; $domain = isset( $args['domain'] ) ? strtolower( (string) $args['domain'] ) : ''; $limit = isset( $args['limit'] ) ? max( 1, min( 100, absint( $args['limit'] ) ) ) : 50; $matches = array(); foreach ( $this->tools as $name => $tool ) { if ( in_array( $name, array( 'agentdesk_discover', 'agentdesk_info', 'agentdesk_run', 'agentdesk_batch' ), true ) ) { continue; } $tool_domain = isset( $tool['domain'] ) ? strtolower( (string) $tool['domain'] ) : 'core'; if ( $domain && $tool_domain !== $domain ) { continue; } $haystack = strtolower( $name . ' ' . ( $tool['description'] ?? '' ) . ' ' . $tool_domain ); if ( $query && false === strpos( $haystack, $query ) ) { continue; } $matches[] = array( 'name' => $name, 'description' => $tool['description'] ?? '', 'domain' => $tool_domain, 'power_mode' => ! empty( $tool['power_mode'] ), ); if ( count( $matches ) >= $limit ) { break; } } return array( 'tools' => $matches, 'total' => count( $matches ), 'domains'=> $this->get_domains(), ); } /** * Get tool info/schema. * * @param string $name Tool name. * @return array|WP_Error */ public function info( $name ) { $tool = $this->get( $name ); if ( ! $tool ) { /* translators: %s: MCP tool name. */ return new WP_Error( 'agentdesk_tool_not_found', sprintf( __( 'Tool not found: %s', 'agentdesk-site-operations' ), $name ) ); } return array( 'name' => $name, 'description' => $tool['description'] ?? '', 'domain' => $tool['domain'] ?? 'core', 'inputSchema' => $tool['inputSchema'] ?? array( 'type' => 'object' ), 'capability' => $tool['capability'] ?? 'read', 'power_mode' => ! empty( $tool['power_mode'] ), 'blocked_in_safe_mode'=> ! empty( $tool['blocked_in_safe_mode'] ), ); } /** * Execute a tool. * * @param string $name Tool name. * @param array $args Arguments. * @param bool $dry_run Dry run flag. * @return mixed|WP_Error */ public function run( $name, $args = array(), $dry_run = false ) { $tool = $this->get( $name ); if ( ! $tool ) { /* translators: %s: MCP tool name. */ return new WP_Error( 'agentdesk_tool_not_found', sprintf( __( 'Tool not found: %s', 'agentdesk-site-operations' ), $name ) ); } $cap_check = Agentdesk_Security::validate_capability( $tool ); if ( is_wp_error( $cap_check ) ) { Agentdesk_Audit::log( $name, $args, 'blocked', $cap_check->get_error_message() ); return $cap_check; } $mode_check = Agentdesk_Security::validate_tool_mode( $tool ); if ( is_wp_error( $mode_check ) ) { Agentdesk_Audit::log( $name, $args, 'blocked', $mode_check->get_error_message() ); return $mode_check; } if ( $dry_run && ! empty( $tool['supports_dry_run'] ) ) { return array( 'dry_run' => true, 'tool' => $name, 'message' => __( 'Dry run — no changes applied.', 'agentdesk-site-operations' ), 'args' => $args, ); } $start = microtime( true ); try { $result = call_user_func( $tool['callback'], $args ); } catch ( Throwable $e ) { $duration = (int) round( ( microtime( true ) - $start ) * 1000 ); Agentdesk_Audit::log( $name, $args, 'error', $e->getMessage(), $duration ); return new WP_Error( 'agentdesk_tool_exception', $e->getMessage() ); } $duration = (int) round( ( microtime( true ) - $start ) * 1000 ); if ( is_wp_error( $result ) ) { Agentdesk_Audit::log( $name, $args, 'error', $result->get_error_message(), $duration ); return $result; } Agentdesk_Audit::log( $name, $args, 'success', '', $duration ); return $result; } /** * Run batch of tools. * * @param array $steps Steps with name/arguments. * @param bool $stop_on_error Stop on first error. * @return array */ public function batch( $steps, $stop_on_error = true ) { $results = array(); // Cap batch size: one authenticated request must not be able to run an // unbounded number of operations against a per-minute budget. $steps = array_slice( (array) $steps, 0, 25 ); foreach ( $steps as $index => $step ) { $name = isset( $step['name'] ) ? (string) $step['name'] : ''; $args = isset( $step['arguments'] ) && is_array( $step['arguments'] ) ? $step['arguments'] : array(); $dry_run = ! empty( $step['dry_run'] ); $result = $this->run( $name, $args, $dry_run ); $results[] = array( 'index' => $index, 'name' => $name, 'result' => is_wp_error( $result ) ? array( 'error' => $result->get_error_message() ) : $result, ); if ( is_wp_error( $result ) && $stop_on_error ) { break; } } return array( 'results' => $results ); } /** * Format tool for MCP tools/list response. * * @param array $tool Tool definition. * @return array */ public function format_tool_for_mcp( $tool ) { return array( 'name' => $tool['name'], 'description' => $tool['description'] ?? '', 'inputSchema' => $tool['inputSchema'] ?? array( 'type' => 'object' ), ); } /** * Get unique domains. * * @return array */ private function get_domains() { $domains = array(); foreach ( $this->tools as $tool ) { $domains[] = $tool['domain'] ?? 'core'; } return array_values( array_unique( $domains ) ); } /** * Register gateway meta-tools. */ private function register_gateway_tools() { // Gateway tools are registered in includes/tools/core/gateway.php via hooks. } }