mcp-1.x-dev/src/Plugin/Mcp/DrushCaller.php

src/Plugin/Mcp/DrushCaller.php
<?php

namespace Drupal\mcp\Plugin\Mcp;

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\mcp\Attribute\Mcp;
use Drupal\mcp\Plugin\McpPluginBase;
use Drupal\mcp\ServerFeatures\Tool;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

/**
 * Plugin implementation of the drush command caller.
 */
#[Mcp(
  id: 'drush',
  name: new TranslatableMarkup('Drush Commands'),
  description: new TranslatableMarkup(
    'A plugin that allows you to call Drush commands. This is only for development purposes. Use with caution.'
  ),
)]
class DrushCaller extends McpPluginBase implements ContainerFactoryPluginInterface {

  /**
   * {@inheritDoc}
   */
  public static function create(
    ContainerInterface $container,
    array $configuration,
    $plugin_id,
    $plugin_definition,
  ) {
    $instance = parent::create(
      $container,
      $configuration,
      $plugin_id,
      $plugin_definition,
    );

    return $instance;
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration(): array {
    $config = parent::defaultConfiguration();
    $config['enabled'] = FALSE;

    $tools = $this->getTools();
    foreach ($tools as $tool) {
      $config['tools'][$tool->name] = [
        'enabled' => FALSE,
        'roles' => [],
        'description' => '',
      ];
    }

    return $config;
  }

  /**
   * {@inheritDoc}
   */
  public function checkRequirements(): bool {
    try {
      // Check if drush is available and can execute commands.
      $process = new Process(['drush', 'version']);
      $process->setTimeout(10);
      $process->run();

      if (!$process->isSuccessful()) {
        return FALSE;
      }

      return TRUE;
    }
    catch (\Exception $e) {
      return FALSE;
    }
  }

  /**
   * {@inheritDoc}
   */
  public function getRequirementsDescription(): string {
    if (!$this->checkRequirements()) {
      return $this->t('Drush must be installed and accessible from the command line.');
    }
    return '';
  }

  /**
   * Get the list of Drush commands.
   */
  private function getDrushCommands() {
    $process = new Process(['drush', 'list', '--format=json']);
    $process->setTimeout(60);
    $process->mustRun();
    $output = $process->getOutput();

    return json_decode($output, TRUE);
  }

  /**
   * Check if a command is allowed based on tool configuration.
   */
  private function isCommandAllowed(string $command): bool {
    $config = $this->getConfiguration();
    $tools_config = $config['tools'] ?? [];

    if (empty($tools_config)) {
      return FALSE;
    }

    if (isset($tools_config[$command])) {
      return !empty($tools_config[$command]['enabled']);
    }

    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function getTools(): array {
    $tools = [];
    $parsed_output = $this->getDrushCommands();
    if (isset($parsed_output['commands'])
      && is_array(
        $parsed_output['commands']
      )
    ) {
      foreach ($parsed_output['commands'] as $command) {
        if (isset($command['hidden']) && $command['hidden']) {
          continue;
        }

        $usage = implode(' ', $command['usage']);
        $description
          = "Description: $command[description]; Help: $command[help]; Usage: $usage";
        $schema = $this->createJsonSchema($command);
        $tool = new Tool(
          name: $command['name'],
          description: $description,
          inputSchema: empty($schema['properties']) ? [
            'type'       => 'object',
            'properties' => new \stdClass(),
          ] : $schema,
        );

        $tools[] = $tool;
      }
    }

    return $tools;
  }

  /**
   * Creates a JSON schema for a Drush command.
   */
  protected function createJsonSchema(array $command) {
    $schema = [
      'type'       => 'object',
      'properties' => [],
      'required'   => [],
    ];

    if (isset($command['definition']['arguments'])
      && is_array(
        $command['definition']['arguments']
      )
    ) {
      foreach ($command['definition']['arguments'] as $arg_name => $arg_details) {
        $schema['properties'][$arg_name] = [
          'type'        => 'string',
          'title'       => $arg_name,
          'description' => $arg_details['description'] ?? "Argument: $arg_name",
        ];
        if (isset($arg_details['is_required']) && $arg_details['is_required']) {
          $schema['required'][] = $arg_name;
        }
      }
    }

    if (isset($command['definition']['options'])
      && is_array(
        $command['definition']['options']
      )
    ) {
      foreach ($command['definition']['options'] as $opt_name => $opt_details) {
        if (in_array($opt_name, [
          'help',
          'silent',
          'quiet',
          'verbose',
          'version',
          'ansi',
          'no-ansi',
          'no-interaction',
          'yes',
          'no',
          'root',
          'uri',
          'simulate',
          'define',
          'xdebug',
        ])
        ) {
          continue;
        }

        $type = 'string';
        if (isset($opt_details['accept_value'])
          && !$opt_details['accept_value']
        ) {
          $type = 'boolean';
        }

        $schema['properties'][$opt_name] = [
          'type'        => $type,
          'title'       => $opt_name,
          'description' => $opt_details['description'] ?? "Option: --$opt_name",
        ];

        if (isset($opt_details['is_required']) && $opt_details['is_required']) {
          $schema['required'][] = $opt_name;
        }
      }
    }

    return $schema;
  }

  /**
   * {@inheritDoc}
   */
  public function executeTool(string $toolId, mixed $arguments): array {
    $commnds = $this->getDrushCommands();
    $commandName = NULL;
    foreach ($commnds['commands'] as $command) {
      $sanitizedName = $this->sanitizeToolName($command['name']);
      if ($sanitizedName === $toolId || md5($command['name']) === $toolId) {
        $commandName = $command['name'];
        break;
      }
    }

    if (!$commandName) {
      $commandName = $toolId;
    }

    // Validate command is allowed.
    if (!$this->isCommandAllowed($commandName)) {
      return [
        [
          'type' => 'text',
          'text' => sprintf(
            'Error: Command "%s" is not allowed. Please enable it in the MCP Dev Tools configuration.',
            $commandName
          ),
        ],
      ];
    }

    $cmd = ['drush', escapeshellarg($commandName)];
    $process = new Process(['drush', 'help', escapeshellarg($commandName), '--format=json']);
    try {
      $process->run();
      $help = json_decode($process->getOutput(), TRUE);

      if (isset($help['arguments']) && is_array($help['arguments'])) {
        foreach ($help['arguments'] as $arg_name => $arg_details) {
          if (isset($arguments[$arg_name]) && is_scalar($arguments[$arg_name])) {
            $cmd[] = escapeshellarg($arguments[$arg_name]);
            unset($arguments[$arg_name]);
          }
        }
      }

      foreach ($arguments as $key => $value) {
        if (is_bool($value)) {
          if ($value) {
            $cmd[] = "--" . escapeshellarg($key);
          }
        }
        elseif (!empty($value) && is_scalar($value)) {
          $cmd[] = "--" . escapeshellarg($key) . "=" . escapeshellarg($value);
        }
      }
    }
    catch (\Exception $e) {
      foreach ($arguments as $key => $value) {
        if (is_bool($value)) {
          if ($value) {
            $cmd[] = "--" . escapeshellarg($key);
          }
        }
        elseif (is_scalar($value)) {
          $cmd[] = escapeshellarg($value);
        }
      }
    }

    $cmd[] = '--yes';
    $cmd[] = '--no-interaction';

    $process = new Process($cmd);
    $process->setTimeout(3600);

    try {
      $process->mustRun();

      return [
        [
          'type' => 'text',
          'text' => $process->getOutput() ?? 'Command run successfully.',
        ],
      ];
    }
    catch (ProcessFailedException $exception) {
      throw new \Exception($exception->getMessage());
    }
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc