From 5f07888a7bfcef86e6ff8468ac8f702fd4d1f781 Mon Sep 17 00:00:00 2001 From: Marco Chiesi <2298248+marcochiesi@users.noreply.github.com> Date: Sun, 16 Mar 2025 17:07:24 +0100 Subject: [PATCH 1/7] Added WP CLI commands as tools --- composer.json | 4 + src/AiCommand.php | 196 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+) diff --git a/composer.json b/composer.json index ac98c4f..82c8413 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,10 @@ "authors": [], "require": { "php": "^8.2", + "wp-cli/cache-command": "^2.1", + "wp-cli/maintenance-mode-command": "^2.1", + "wp-cli/profile-command": "^2.1", + "wp-cli/rewrite-command": "^2.0", "wp-cli/wp-cli": "^2.11" }, "require-dev": { diff --git a/src/AiCommand.php b/src/AiCommand.php index 1ae30cf..b6a79a9 100644 --- a/src/AiCommand.php +++ b/src/AiCommand.php @@ -4,6 +4,8 @@ use WP_CLI; use WP_CLI_Command; +use WP_CLI\Dispatcher; +use WP_CLI\SynopsisParser; use WP_Community_Events; use WP_Error; @@ -273,6 +275,200 @@ private function register_tools($server, $client) { ] ); + // Expose WP-CLI commands as tools + $commands = [ + 'cache', + 'config', + 'core', + 'maintenance-mode', + 'profile', + 'rewrite', + ]; + + foreach ( $commands as $command ) { + $command_to_run = WP_CLI::get_runner()->find_command_to_run( [ $command ] ); + list( $command ) = $command_to_run; + + if ( ! is_object( $command ) ) { + continue; + } + + $command_name = $command->get_name(); + + if ( ! $command->can_have_subcommands() ) { + + $command_desc = $command->get_shortdesc() ?? "Runs WP-CLI command: $command_name"; + $command_synopsis = $command->get_synopsis(); + $synopsis_spec = SynopsisParser::parse( $command_synopsis ); + + $properties = []; + $required = []; + + $properties['dummy'] = [ + 'type' => 'string', + 'description' => 'Dummy parameter', + ]; + + WP_CLI::debug("Synopsis for command: " . $command_name . " - " . print_r($command_synopsis, true), 'ai'); + + foreach ( $command_synopsis as $arg ) { + if ($arg['type'] === 'positional' || $arg['type'] === 'assoc') { + $prop_name = str_replace('-', '_', $arg['name']); + $properties[ $prop_name ] = [ + 'type' => 'string', + 'description' => $arg['description'] ?? "Parameter {$arg['name']}" + ]; + + if (!isset($arg['optional']) || !$arg['optional']) { + $required[] = $prop_name; + } + } + } + + $server->register_tool([ + 'name' => 'wp_' . str_replace(' ', '_', $command_name), + 'description' => $command_desc, + 'inputSchema' => [ + 'type' => 'object', + 'properties' => $properties, + 'required' => $required + ], + 'callable' => function($params) use ($command_name) { + $args = []; + $assoc_args = []; + + // Process positional arguments first + foreach ($synopsis_spec as $arg) { + if ($arg['type'] === 'positional') { + $prop_name = str_replace('-', '_', $arg['name']); + if (isset($params[$prop_name])) { + $args[] = $params[$prop_name]; + } + } + } + + // Process associative arguments and flags + foreach ($params as $key => $value) { + // Skip positional args and dummy param + if ($key === 'dummy') { + continue; + } + + // Check if this is an associative argument + foreach ($synopsis_spec as $arg) { + if (($arg['type'] === 'assoc' || $arg['type'] === 'flag') && + str_replace('-', '_', $arg['name']) === $key) { + $assoc_args[str_replace('_', '-', $key)] = $value; + break; + } + } + } + + ob_start(); + WP_CLI::run_command(array_merge(explode(' ', $command_name), $args), $assoc_args); + return ob_get_clean(); + } + ]); + } else { + + \WP_CLI::debug($command_name . " subcommands: " . print_r($command->get_subcommands(), true), 'ai'); + + foreach ( $command->get_subcommands() as $subcommand ) { + + if ( WP_CLI::get_runner()->is_command_disabled( $subcommand ) ) { + continue; + } + + $subcommand_name = $subcommand->get_name(); + $subcommand_desc = $subcommand->get_shortdesc() ?? "Runs WP-CLI command: $subcommand_name"; + $subcommand_synopsis = $subcommand->get_synopsis(); + $synopsis_spec = SynopsisParser::parse( $subcommand_synopsis ); + + $properties = []; + $required = []; + + $properties['dummy'] = [ + 'type' => 'string', + 'description' => 'Dummy parameter', + ]; + + foreach ( $synopsis_spec as $arg ) { + if ($arg['type'] === 'positional' || $arg['type'] === 'assoc') { + $prop_name = str_replace('-', '_', $arg['name']); + $properties[ $prop_name ] = [ + 'type' => 'string', + 'description' => $arg['description'] ?? "Parameter {$arg['name']}" + ]; + + } + /* + // Handle flag type parameters (boolean) + if ($arg['type'] === 'flag') { + $prop_name = str_replace('-', '_', $arg['name']); + $properties[ $prop_name ] = [ + 'type' => 'boolean', + 'description' => $arg['description'] ?? "Flag {$arg['name']}", + 'default' => false + ]; + }*/ + + if (!isset($arg['optional']) || !$arg['optional']) { + $required[] = $prop_name; + } + + } + $server->register_tool([ + 'name' => 'wp_' . str_replace(' ', '_', $command_name) . '_' . str_replace(' ', '_', $subcommand_name), + 'description' => $subcommand_desc, + 'inputSchema' => [ + 'type' => 'object', + 'properties' => $properties, + 'required' => $required + ], + 'callable' => function($params) use ($command_name, $subcommand_name, $synopsis_spec) { + + \WP_CLI::debug("Subcommand: " . $subcommand_name . " - Received params: " . print_r($params, true), 'ai'); + + $args = []; + $assoc_args = []; + + // Process positional arguments first + foreach ($synopsis_spec as $arg) { + if ($arg['type'] === 'positional') { + $prop_name = str_replace('-', '_', $arg['name']); + if (isset($params[$prop_name])) { + $args[] = $params[$prop_name]; + } + } + } + + // Process associative arguments and flags + foreach ($params as $key => $value) { + // Skip positional args and dummy param + if ($key === 'dummy') { + continue; + } + + // Check if this is an associative argument + foreach ($synopsis_spec as $arg) { + if (($arg['type'] === 'assoc' || $arg['type'] === 'flag') && + str_replace('-', '_', $arg['name']) === $key) { + $assoc_args[str_replace('_', '-', $key)] = $value; + break; + } + } + } + + ob_start(); + WP_CLI::run_command( array_merge([ $command_name, $subcommand_name], $args), $assoc_args); + return ob_get_clean(); + + } + ]); + } + } + } + } // Register resources for AI access From 2edc822013e928e304328b2df915f864e82b5407 Mon Sep 17 00:00:00 2001 From: Marco Chiesi <2298248+marcochiesi@users.noreply.github.com> Date: Sun, 16 Mar 2025 17:41:21 +0100 Subject: [PATCH 2/7] Moved WP CLI tools to separate class --- src/AiCommand.php | 341 ++++++++++---------------------------------- src/MapCLItoMCP.php | 210 +++++++++++++++++++++++++++ 2 files changed, 287 insertions(+), 264 deletions(-) create mode 100644 src/MapCLItoMCP.php diff --git a/src/AiCommand.php b/src/AiCommand.php index 0a41326..8e35e8e 100644 --- a/src/AiCommand.php +++ b/src/AiCommand.php @@ -6,8 +6,6 @@ use WP_CLI\AiCommand\Tools\URLTools; use WP_CLI; use WP_CLI_Command; -use WP_CLI\Dispatcher; -use WP_CLI\SynopsisParser; use WP_Community_Events; use WP_Error; @@ -50,11 +48,11 @@ class AiCommand extends WP_CLI_Command { */ public function __invoke( $args, $assoc_args ) { $server = new MCP\Server(); - $client = new MCP\Client($server); + $client = new MCP\Client( $server ); - $this->register_tools($server, $client); + $this->register_tools( $server, $client ); - $this->register_resources($server); + $this->register_resources( $server ); $result = $client->call_ai_service_with_prompt( $args[0] ); @@ -62,28 +60,28 @@ public function __invoke( $args, $assoc_args ) { } // Register tools for AI processing - private function register_tools($server, $client) { + private function register_tools( $server, $client ) { $server->register_tool( [ 'name' => 'list_tools', 'description' => 'Lists all available tools with their descriptions.', 'inputSchema' => [ - 'type' => 'object', // Object type for input - 'properties' => [ - 'placeholder' => [ - 'type' => 'integer', - 'description' => '', - ] + 'type' => 'object', // Object type for input + 'properties' => [ + 'placeholder' => [ + 'type' => 'integer', + 'description' => '', ], - 'required' => [], // No required fields + ], + 'required' => [], // No required fields ], - 'callable' => function () use ($server) { + 'callable' => function () use ( $server ) { // Get all capabilities $capabilities = $server->get_capabilities(); // Prepare a list of tools with their descriptions $tool_list = 'Return this to the user as a bullet list with each tool name and description on a new line. \n\n'; - $tool_list .= print_r($capabilities['methods'], true); + $tool_list .= print_r( $capabilities['methods'], true ); // Return the formatted string of tools with descriptions return $tool_list; @@ -94,6 +92,9 @@ private function register_tools($server, $client) { $map_rest_to_mcp = new MapRESTtoMCP(); $map_rest_to_mcp->map_rest_to_mcp( $server ); + $map_cli_to_mcp = new MapCLItoMCP(); + $map_cli_to_mcp->map_cli_to_mcp( $server ); + $server->register_tool( [ 'name' => 'generate_image', @@ -116,19 +117,19 @@ private function register_tools($server, $client) { $server->register_tool( [ - 'name' => 'fetch_wp_community_events', - 'description' => 'Fetches upcoming WordPress community events near a specified city or the user\'s current location. If no events are found in the exact location, nearby events within a specific radius will be considered.', - 'inputSchema' => [ - 'type' => 'object', - 'properties' => [ - 'location' => [ - 'type' => 'string', - 'description' => 'City name or "near me" for auto-detected location. If no events are found in the exact location, the tool will also consider nearby events within a specified radius (default: 100 km).', - ], - ], - 'required' => [ 'location' ], // We only require the location + 'name' => 'fetch_wp_community_events', + 'description' => 'Fetches upcoming WordPress community events near a specified city or the user\'s current location. If no events are found in the exact location, nearby events within a specific radius will be considered.', + 'inputSchema' => [ + 'type' => 'object', + 'properties' => [ + 'location' => [ + 'type' => 'string', + 'description' => 'City name or "near me" for auto-detected location. If no events are found in the exact location, the tool will also consider nearby events within a specified radius (default: 100 km).', + ], ], - 'callable' => function ( $params ) { + 'required' => [ 'location' ], // We only require the location + ], + 'callable' => function ( $params ) { // Default user ID is 0 $user_id = 0; @@ -136,287 +137,99 @@ private function register_tools($server, $client) { $location_input = strtolower( trim( $params['location'] ) ); // Manually include the WP_Community_Events class if it's not loaded - if ( ! class_exists( 'WP_Community_Events' ) ) { - require_once ABSPATH . 'wp-admin/includes/class-wp-community-events.php'; - } + if ( ! class_exists( 'WP_Community_Events' ) ) { + require_once ABSPATH . 'wp-admin/includes/class-wp-community-events.php'; + } // Determine location for the WP_Community_Events class $location = null; - if ( $location_input !== 'near me' ) { - // Provide city name (WP will resolve coordinates) - $location = [ - 'description' => $location_input, - ]; - } + if ( $location_input !== 'near me' ) { + // Provide city name (WP will resolve coordinates) + $location = [ + 'description' => $location_input, + ]; + } // Instantiate WP_Community_Events with user ID (0) and optional location $events_instance = new WP_Community_Events( $user_id, $location ); // Get events from WP_Community_Events - $events = $events_instance->get_events($location_input); + $events = $events_instance->get_events( $location_input ); // Check for WP_Error - if ( is_wp_error( $events ) ) { - return [ 'error' => $events->get_error_message() ]; - } + if ( is_wp_error( $events ) ) { + return [ 'error' => $events->get_error_message() ]; + } // If no events found - if ( empty( $events['events'] ) ) { - return [ 'message' => 'No events found near ' . ( $location_input === 'near me' ? 'your location' : $location_input ) ]; - } + if ( empty( $events['events'] ) ) { + return [ 'message' => 'No events found near ' . ( $location_input === 'near me' ? 'your location' : $location_input ) ]; + } // Format and return the events correctly - $formatted_events = array_map( function ( $event ) { - // Log event details to ensure properties are accessible - error_log( 'Event details: ' . print_r( $event, true ) ); + $formatted_events = array_map( + function ( $event ) { + // Log event details to ensure properties are accessible + error_log( 'Event details: ' . print_r( $event, true ) ); - // Initialize a formatted event string - $formatted_event = ''; + // Initialize a formatted event string + $formatted_event = ''; - // Format event title - if ( isset( $event['title'] ) ) { + // Format event title + if ( isset( $event['title'] ) ) { $formatted_event .= $event['title'] . "\n"; - } + } - // Format the date nicely - $formatted_event .= ' - Date: ' . ( isset( $event['date'] ) ? date( 'F j, Y g:i A', strtotime( $event['date'] ) ) : 'No date available' ) . "\n"; + // Format the date nicely + $formatted_event .= ' - Date: ' . ( isset( $event['date'] ) ? date( 'F j, Y g:i A', strtotime( $event['date'] ) ) : 'No date available' ) . "\n"; - // Format the location - if ( isset( $event['location']['location'] ) ) { - $formatted_event .= ' - Location: ' . $event['location']['location'] . "\n"; - } + // Format the location + if ( isset( $event['location']['location'] ) ) { + $formatted_event .= ' - Location: ' . $event['location']['location'] . "\n"; + } - // Format the event URL - $formatted_event .= isset( $event['url'] ) ? ' - URL: ' . $event['url'] . "\n" : ''; + // Format the event URL + $formatted_event .= isset( $event['url'] ) ? ' - URL: ' . $event['url'] . "\n" : ''; - return $formatted_event; - }, $events['events'] ); + return $formatted_event; + }, + $events['events'] + ); // Combine the formatted events into a single string - $formatted_events_output = implode("\n", $formatted_events); + $formatted_events_output = implode( "\n", $formatted_events ); // Return the formatted events string return [ - 'message' => "OK. I found " . count($formatted_events) . " WordPress events near " . ( $location_input === 'near me' ? 'your location' : $location_input ) . ":\n\n" . $formatted_events_output + 'message' => 'OK. I found ' . count( $formatted_events ) . ' WordPress events near ' . ( $location_input === 'near me' ? 'your location' : $location_input ) . ":\n\n" . $formatted_events_output, ]; - }, + }, ] ); - - // Expose WP-CLI commands as tools - $commands = [ - 'cache', - 'config', - 'core', - 'maintenance-mode', - 'profile', - 'rewrite', - ]; - - foreach ( $commands as $command ) { - $command_to_run = WP_CLI::get_runner()->find_command_to_run( [ $command ] ); - list( $command ) = $command_to_run; - - if ( ! is_object( $command ) ) { - continue; - } - - $command_name = $command->get_name(); - - if ( ! $command->can_have_subcommands() ) { - - $command_desc = $command->get_shortdesc() ?? "Runs WP-CLI command: $command_name"; - $command_synopsis = $command->get_synopsis(); - $synopsis_spec = SynopsisParser::parse( $command_synopsis ); - - $properties = []; - $required = []; - - $properties['dummy'] = [ - 'type' => 'string', - 'description' => 'Dummy parameter', - ]; - - WP_CLI::debug("Synopsis for command: " . $command_name . " - " . print_r($command_synopsis, true), 'ai'); - - foreach ( $command_synopsis as $arg ) { - if ($arg['type'] === 'positional' || $arg['type'] === 'assoc') { - $prop_name = str_replace('-', '_', $arg['name']); - $properties[ $prop_name ] = [ - 'type' => 'string', - 'description' => $arg['description'] ?? "Parameter {$arg['name']}" - ]; - - if (!isset($arg['optional']) || !$arg['optional']) { - $required[] = $prop_name; - } - } - } - - $server->register_tool([ - 'name' => 'wp_' . str_replace(' ', '_', $command_name), - 'description' => $command_desc, - 'inputSchema' => [ - 'type' => 'object', - 'properties' => $properties, - 'required' => $required - ], - 'callable' => function($params) use ($command_name) { - $args = []; - $assoc_args = []; - - // Process positional arguments first - foreach ($synopsis_spec as $arg) { - if ($arg['type'] === 'positional') { - $prop_name = str_replace('-', '_', $arg['name']); - if (isset($params[$prop_name])) { - $args[] = $params[$prop_name]; - } - } - } - - // Process associative arguments and flags - foreach ($params as $key => $value) { - // Skip positional args and dummy param - if ($key === 'dummy') { - continue; - } - - // Check if this is an associative argument - foreach ($synopsis_spec as $arg) { - if (($arg['type'] === 'assoc' || $arg['type'] === 'flag') && - str_replace('-', '_', $arg['name']) === $key) { - $assoc_args[str_replace('_', '-', $key)] = $value; - break; - } - } - } - - ob_start(); - WP_CLI::run_command(array_merge(explode(' ', $command_name), $args), $assoc_args); - return ob_get_clean(); - } - ]); - } else { - - \WP_CLI::debug($command_name . " subcommands: " . print_r($command->get_subcommands(), true), 'ai'); - - foreach ( $command->get_subcommands() as $subcommand ) { - - if ( WP_CLI::get_runner()->is_command_disabled( $subcommand ) ) { - continue; - } - - $subcommand_name = $subcommand->get_name(); - $subcommand_desc = $subcommand->get_shortdesc() ?? "Runs WP-CLI command: $subcommand_name"; - $subcommand_synopsis = $subcommand->get_synopsis(); - $synopsis_spec = SynopsisParser::parse( $subcommand_synopsis ); - - $properties = []; - $required = []; - - $properties['dummy'] = [ - 'type' => 'string', - 'description' => 'Dummy parameter', - ]; - - foreach ( $synopsis_spec as $arg ) { - if ($arg['type'] === 'positional' || $arg['type'] === 'assoc') { - $prop_name = str_replace('-', '_', $arg['name']); - $properties[ $prop_name ] = [ - 'type' => 'string', - 'description' => $arg['description'] ?? "Parameter {$arg['name']}" - ]; - - } - /* - // Handle flag type parameters (boolean) - if ($arg['type'] === 'flag') { - $prop_name = str_replace('-', '_', $arg['name']); - $properties[ $prop_name ] = [ - 'type' => 'boolean', - 'description' => $arg['description'] ?? "Flag {$arg['name']}", - 'default' => false - ]; - }*/ - - if (!isset($arg['optional']) || !$arg['optional']) { - $required[] = $prop_name; - } - - } - $server->register_tool([ - 'name' => 'wp_' . str_replace(' ', '_', $command_name) . '_' . str_replace(' ', '_', $subcommand_name), - 'description' => $subcommand_desc, - 'inputSchema' => [ - 'type' => 'object', - 'properties' => $properties, - 'required' => $required - ], - 'callable' => function($params) use ($command_name, $subcommand_name, $synopsis_spec) { - - \WP_CLI::debug("Subcommand: " . $subcommand_name . " - Received params: " . print_r($params, true), 'ai'); - - $args = []; - $assoc_args = []; - - // Process positional arguments first - foreach ($synopsis_spec as $arg) { - if ($arg['type'] === 'positional') { - $prop_name = str_replace('-', '_', $arg['name']); - if (isset($params[$prop_name])) { - $args[] = $params[$prop_name]; - } - } - } - - // Process associative arguments and flags - foreach ($params as $key => $value) { - // Skip positional args and dummy param - if ($key === 'dummy') { - continue; - } - - // Check if this is an associative argument - foreach ($synopsis_spec as $arg) { - if (($arg['type'] === 'assoc' || $arg['type'] === 'flag') && - str_replace('-', '_', $arg['name']) === $key) { - $assoc_args[str_replace('_', '-', $key)] = $value; - break; - } - } - } - - ob_start(); - WP_CLI::run_command( array_merge([ $command_name, $subcommand_name], $args), $assoc_args); - return ob_get_clean(); - - } - ]); - } - } - } - } // Register resources for AI access - private function register_resources($server) { + private function register_resources( $server ) { // Register Users resource - $server->register_resource([ + $server->register_resource( + [ 'name' => 'users', 'uri' => 'data://users', 'description' => 'List of users', 'mimeType' => 'application/json', 'dataKey' => 'users', // Data will be fetched from 'users' - ]); + ] + ); // Register Product Catalog resource - $server->register_resource([ + $server->register_resource( + [ 'name' => 'product_catalog', 'uri' => 'file://./products.json', 'description' => 'Product catalog', 'mimeType' => 'application/json', 'filePath' => './products.json', // Data will be fetched from products.json - ]); + ] + ); } } diff --git a/src/MapCLItoMCP.php b/src/MapCLItoMCP.php new file mode 100644 index 0000000..870e31b --- /dev/null +++ b/src/MapCLItoMCP.php @@ -0,0 +1,210 @@ +find_command_to_run( [ $command ] ); + list( $command ) = $command_to_run; + + if ( ! is_object( $command ) ) { + continue; + } + + $command_name = $command->get_name(); + + if ( ! $command->can_have_subcommands() ) { + + $command_desc = $command->get_shortdesc() ?? "Runs WP-CLI command: $command_name"; + $command_synopsis = $command->get_synopsis(); + $synopsis_spec = SynopsisParser::parse( $command_synopsis ); + + $properties = []; + $required = []; + + $properties['dummy'] = [ + 'type' => 'string', + 'description' => 'Dummy parameter', + ]; + + WP_CLI::debug( 'Synopsis for command: ' . $command_name . ' - ' . print_r( $command_synopsis, true ), 'ai' ); + + foreach ( $command_synopsis as $arg ) { + if ( $arg['type'] === 'positional' || $arg['type'] === 'assoc' ) { + $prop_name = str_replace( '-', '_', $arg['name'] ); + $properties[ $prop_name ] = [ + 'type' => 'string', + 'description' => $arg['description'] ?? "Parameter {$arg['name']}", + ]; + + if ( ! isset( $arg['optional'] ) || ! $arg['optional'] ) { + $required[] = $prop_name; + } + } + } + + $server->register_tool( + [ + 'name' => 'wp_cli_' . str_replace( ' ', '_', $command_name ), + 'description' => $command_desc, + 'inputSchema' => [ + 'type' => 'object', + 'properties' => $properties, + 'required' => $required, + ], + 'callable' => function ( $params ) use ( $command_name ) { + $args = []; + $assoc_args = []; + + // Process positional arguments first + foreach ( $synopsis_spec as $arg ) { + if ( $arg['type'] === 'positional' ) { + $prop_name = str_replace( '-', '_', $arg['name'] ); + if ( isset( $params[ $prop_name ] ) ) { + $args[] = $params[ $prop_name ]; + } + } + } + + // Process associative arguments and flags + foreach ( $params as $key => $value ) { + // Skip positional args and dummy param + if ( $key === 'dummy' ) { + continue; + } + + // Check if this is an associative argument + foreach ( $synopsis_spec as $arg ) { + if ( ( $arg['type'] === 'assoc' || $arg['type'] === 'flag' ) && + str_replace( '-', '_', $arg['name'] ) === $key ) { + $assoc_args[ str_replace( '_', '-', $key ) ] = $value; + break; + } + } + } + + ob_start(); + WP_CLI::run_command( array_merge( explode( ' ', $command_name ), $args ), $assoc_args ); + return ob_get_clean(); + }, + ] + ); + } else { + + \WP_CLI::debug( $command_name . ' subcommands: ' . print_r( $command->get_subcommands(), true ), 'ai' ); + + foreach ( $command->get_subcommands() as $subcommand ) { + + if ( WP_CLI::get_runner()->is_command_disabled( $subcommand ) ) { + continue; + } + + $subcommand_name = $subcommand->get_name(); + $subcommand_desc = $subcommand->get_shortdesc() ?? "Runs WP-CLI command: $subcommand_name"; + $subcommand_synopsis = $subcommand->get_synopsis(); + $synopsis_spec = SynopsisParser::parse( $subcommand_synopsis ); + + $properties = []; + $required = []; + + $properties['dummy'] = [ + 'type' => 'string', + 'description' => 'Dummy parameter', + ]; + + foreach ( $synopsis_spec as $arg ) { + if ( $arg['type'] === 'positional' || $arg['type'] === 'assoc' ) { + $prop_name = str_replace( '-', '_', $arg['name'] ); + $properties[ $prop_name ] = [ + 'type' => 'string', + 'description' => $arg['description'] ?? "Parameter {$arg['name']}", + ]; + + } + /* + // Handle flag type parameters (boolean) + if ($arg['type'] === 'flag') { + $prop_name = str_replace('-', '_', $arg['name']); + $properties[ $prop_name ] = [ + 'type' => 'boolean', + 'description' => $arg['description'] ?? "Flag {$arg['name']}", + 'default' => false + ]; + }*/ + + if ( ! isset( $arg['optional'] ) || ! $arg['optional'] ) { + $required[] = $prop_name; + } + } + $server->register_tool( + [ + 'name' => 'wp_cli_' . str_replace( ' ', '_', $command_name ) . '_' . str_replace( ' ', '_', $subcommand_name ), + 'description' => $subcommand_desc, + 'inputSchema' => [ + 'type' => 'object', + 'properties' => $properties, + 'required' => $required, + ], + 'callable' => function ( $params ) use ( $command_name, $subcommand_name, $synopsis_spec ) { + + \WP_CLI::debug( 'Subcommand: ' . $subcommand_name . ' - Received params: ' . print_r( $params, true ), 'ai' ); + + $args = []; + $assoc_args = []; + + // Process positional arguments first + foreach ( $synopsis_spec as $arg ) { + if ( $arg['type'] === 'positional' ) { + $prop_name = str_replace( '-', '_', $arg['name'] ); + if ( isset( $params[ $prop_name ] ) ) { + $args[] = $params[ $prop_name ]; + } + } + } + + // Process associative arguments and flags + foreach ( $params as $key => $value ) { + // Skip positional args and dummy param + if ( $key === 'dummy' ) { + continue; + } + + // Check if this is an associative argument + foreach ( $synopsis_spec as $arg ) { + if ( ( $arg['type'] === 'assoc' || $arg['type'] === 'flag' ) && + str_replace( '-', '_', $arg['name'] ) === $key ) { + $assoc_args[ str_replace( '_', '-', $key ) ] = $value; + break; + } + } + } + + ob_start(); + WP_CLI::run_command( array_merge( [ $command_name, $subcommand_name ], $args ), $assoc_args ); + return ob_get_clean(); + }, + ] + ); + } + } + } + } +} From 57ca8329343d85fd917bbf932234035ea7dd6994 Mon Sep 17 00:00:00 2001 From: Marco Chiesi <2298248+marcochiesi@users.noreply.github.com> Date: Sun, 16 Mar 2025 17:49:34 +0100 Subject: [PATCH 3/7] Added post-type and taxonomy commands --- src/MapCLItoMCP.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/MapCLItoMCP.php b/src/MapCLItoMCP.php index 870e31b..23212f0 100644 --- a/src/MapCLItoMCP.php +++ b/src/MapCLItoMCP.php @@ -17,8 +17,10 @@ public function map_cli_to_mcp( Server $server ) { 'config', 'core', 'maintenance-mode', + 'post-type', 'profile', 'rewrite', + 'taxonomy', ]; foreach ( $commands as $command ) { From 33eafe2f2274af3a1ab5bc3d1790c2d98b2ba656 Mon Sep 17 00:00:00 2001 From: Marco Chiesi <2298248+marcochiesi@users.noreply.github.com> Date: Sun, 16 Mar 2025 20:32:11 +0100 Subject: [PATCH 4/7] Revert changes to composer.json --- composer.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/composer.json b/composer.json index 82c8413..ac98c4f 100644 --- a/composer.json +++ b/composer.json @@ -7,10 +7,6 @@ "authors": [], "require": { "php": "^8.2", - "wp-cli/cache-command": "^2.1", - "wp-cli/maintenance-mode-command": "^2.1", - "wp-cli/profile-command": "^2.1", - "wp-cli/rewrite-command": "^2.0", "wp-cli/wp-cli": "^2.11" }, "require-dev": { From e3e68feefcbbfb338370a746b97d522a4b2ae09c Mon Sep 17 00:00:00 2001 From: Marco Chiesi <2298248+marcochiesi@users.noreply.github.com> Date: Sun, 16 Mar 2025 23:11:18 +0100 Subject: [PATCH 5/7] WP CLI tools refactored to use Tools collection --- ai-command.php | 4 +++- src/MapCLItoMCP.php | 21 ++++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/ai-command.php b/ai-command.php index 8df738a..f4c90a4 100644 --- a/ai-command.php +++ b/ai-command.php @@ -26,9 +26,11 @@ $all_tools = [ ...(new ImageTools($client))->get_tools(), ...(new MapRESTtoMCP())->map_rest_to_mcp(), + ...(new MapCLItoMCP())->map_cli_to_mcp(), + ]; - foreach ($all_tools as $tool) { + foreach ($all_tools as $tool) { $tools->add($tool); } diff --git a/src/MapCLItoMCP.php b/src/MapCLItoMCP.php index 23212f0..6d967b8 100644 --- a/src/MapCLItoMCP.php +++ b/src/MapCLItoMCP.php @@ -3,13 +3,13 @@ namespace WP_CLI\AiCommand; use WP_CLI; -use WP_CLI\AiCommand\MCP\Server; +use WP_CLI\AiCommand\Entity\Tool; use WP_CLI\Dispatcher; use WP_CLI\SynopsisParser; class MapCLItoMCP { - public function map_cli_to_mcp( Server $server ) { + public function map_cli_to_mcp() : array { // Expose WP-CLI commands as tools $commands = [ @@ -23,6 +23,8 @@ public function map_cli_to_mcp( Server $server ) { 'taxonomy', ]; + $tools = []; + foreach ( $commands as $command ) { $command_to_run = WP_CLI::get_runner()->find_command_to_run( [ $command ] ); list( $command ) = $command_to_run; @@ -63,8 +65,7 @@ public function map_cli_to_mcp( Server $server ) { } } - $server->register_tool( - [ + $tool = new Tool([ 'name' => 'wp_cli_' . str_replace( ' ', '_', $command_name ), 'description' => $command_desc, 'inputSchema' => [ @@ -72,7 +73,7 @@ public function map_cli_to_mcp( Server $server ) { 'properties' => $properties, 'required' => $required, ], - 'callable' => function ( $params ) use ( $command_name ) { + 'callable' => function ( $params ) use ( $command_name, $synopsis_spec ) { $args = []; $assoc_args = []; @@ -109,6 +110,9 @@ public function map_cli_to_mcp( Server $server ) { }, ] ); + + $tools[] = $tool; + } else { \WP_CLI::debug( $command_name . ' subcommands: ' . print_r( $command->get_subcommands(), true ), 'ai' ); @@ -156,8 +160,7 @@ public function map_cli_to_mcp( Server $server ) { $required[] = $prop_name; } } - $server->register_tool( - [ + $tool = new Tool([ 'name' => 'wp_cli_' . str_replace( ' ', '_', $command_name ) . '_' . str_replace( ' ', '_', $subcommand_name ), 'description' => $subcommand_desc, 'inputSchema' => [ @@ -205,8 +208,12 @@ public function map_cli_to_mcp( Server $server ) { }, ] ); + + $tools[] = $tool; } } } + + return $tools; } } From d502fc52d068c29f7bb833a03d234762726c5f40 Mon Sep 17 00:00:00 2001 From: Marco Chiesi <2298248+marcochiesi@users.noreply.github.com> Date: Mon, 17 Mar 2025 07:41:25 +0100 Subject: [PATCH 6/7] Removed post-type and taxonomy commands (covered by REST) --- src/MapCLItoMCP.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/MapCLItoMCP.php b/src/MapCLItoMCP.php index 6d967b8..1bbf0b8 100644 --- a/src/MapCLItoMCP.php +++ b/src/MapCLItoMCP.php @@ -17,10 +17,8 @@ public function map_cli_to_mcp() : array { 'config', 'core', 'maintenance-mode', - 'post-type', 'profile', 'rewrite', - 'taxonomy', ]; $tools = []; From 8f389da1e5d909e341c5f5afe14b1b0962d2144e Mon Sep 17 00:00:00 2001 From: Marco Chiesi <2298248+marcochiesi@users.noreply.github.com> Date: Mon, 17 Mar 2025 10:30:16 +0100 Subject: [PATCH 7/7] Moved CLI class to Tools folder --- ai-command.php | 1 + src/{ => Tools}/MapCLItoMCP.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) rename src/{ => Tools}/MapCLItoMCP.php (99%) diff --git a/ai-command.php b/ai-command.php index 4908455..aa20479 100644 --- a/ai-command.php +++ b/ai-command.php @@ -8,6 +8,7 @@ use WP_CLI\AiCommand\Tools\URLTools; use WP_CLI\AiCommand\Tools\CommunityEvents; use WP_CLI\AiCommand\Tools\MapRESTtoMCP; +use WP_CLI\AiCommand\Tools\MapCLItoMCP; use WP_CLI; if ( ! class_exists( '\WP_CLI' ) ) { diff --git a/src/MapCLItoMCP.php b/src/Tools/MapCLItoMCP.php similarity index 99% rename from src/MapCLItoMCP.php rename to src/Tools/MapCLItoMCP.php index 1bbf0b8..db65b3d 100644 --- a/src/MapCLItoMCP.php +++ b/src/Tools/MapCLItoMCP.php @@ -1,6 +1,6 @@