diff --git a/ai-command.php b/ai-command.php index 3faad4e..fc2f8c7 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' ) ) { @@ -32,9 +33,11 @@ ...(new MiscTools($server))->get_tools(), ...(new URLTools($server))->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/AiCommand.php b/src/AiCommand.php index 16e4374..8ae9dd0 100644 --- a/src/AiCommand.php +++ b/src/AiCommand.php @@ -89,24 +89,26 @@ private function register_tools($server) : void { * A) it does not belong here * B) it is not used* */ - 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/Tools/MapCLItoMCP.php b/src/Tools/MapCLItoMCP.php new file mode 100644 index 0000000..db65b3d --- /dev/null +++ b/src/Tools/MapCLItoMCP.php @@ -0,0 +1,217 @@ +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; + } + } + } + + $tool = new Tool([ + 'name' => 'wp_cli_' . str_replace( ' ', '_', $command_name ), + 'description' => $command_desc, + 'inputSchema' => [ + 'type' => 'object', + 'properties' => $properties, + 'required' => $required, + ], + 'callable' => function ( $params ) use ( $command_name, $synopsis_spec ) { + $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(); + }, + ] + ); + + $tools[] = $tool; + + } 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; + } + } + $tool = new 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(); + }, + ] + ); + + $tools[] = $tool; + } + } + } + + return $tools; + } +}