mcp-1.x-dev/modules/mcp_studio/src/Form/ToolForm.php

modules/mcp_studio/src/Form/ToolForm.php
<?php

namespace Drupal\mcp_studio\Form;

use Drupal\Core\Url;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Form for adding/editing a Studio tool.
 */
class ToolForm extends FormBase {

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config.factory')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'mcp_studio_tool_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state, $tool_id = NULL) {
    $config = $this->configFactory()->getEditable('mcp_studio.settings');
    $tools = $config->get('tools') ?? [];
    $tool = NULL;

    if ($tool_id !== NULL && isset($tools[$tool_id])) {
      $tool = $tools[$tool_id];
      $form_state->set('tool_id', $tool_id);
    }

    // Set form title based on whether we're creating or editing.
    $form['#title'] = $tool_id !== NULL ? $this->t('Update the MCP Tool') : $this->t('Create new MCP Tool');

    // Attach the CodeMirror library to the entire form.
    $form['#attached']['library'][] = 'mcp_studio/codemirror';

    $form['name'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Tool name'),
      '#description' => $this->t('The name of the tool as it will appear in MCP.'),
      '#required' => TRUE,
      '#default_value' => $tool['name'] ?? '',
    ];

    $form['description'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Description'),
      '#description' => $this->t('A detailed description of what this tool does.'),
      '#required' => TRUE,
      '#default_value' => $tool['description'] ?? '',
      '#rows' => 5,
    ];

    $form['input_schema'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Input schema (JSON Schema)'),
      '#description' => $this->t('JSON Schema defining the tool"s input parameters. Leave empty or use {} for no parameters.'),
      '#default_value' => $tool['input_schema'] ?? '{}',
      '#rows' => 10,
      '#attributes' => [
        'class' => ['codemirror-editor', 'json-schema-editor'],
        'data-mode' => 'json',
        'placeholder' => '{
  "type": "object",
  "properties": {
    "message": {
      "type": "string"
    }
  }
}',
      ],
    ];

    // Container for output fields.
    $form['output_wrapper'] = [
      '#type' => 'container',
      '#attributes' => ['class' => ['output-field-group']],
      '#prefix' => '<div class="output-section">',
      '#suffix' => '</div>',
    ];

    // Mode selector.
    $form['output_wrapper']['output_mode'] = [
      '#type' => 'select',
      '#title' => $this->t('Output format'),
      '#options' => [
        'text' => $this->t('Plain Text'),
        'json' => $this->t('JSON'),
        'twig' => $this->t('TWIG Template'),
      ],
      '#default_value' => $tool['output_mode'] ?? 'text',
      '#attributes' => ['class' => ['output-mode-selector']],
    ];

    // Output textarea.
    $form['output_wrapper']['output'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Output content'),
      '#description' => $this->t('The response this tool will return.'),
      '#required' => TRUE,
      '#default_value' => $tool['output'] ?? '',
      '#rows' => 15,
      '#attributes' => [
        'class' => ['codemirror-editor'],
        'data-mode' => $tool['output_mode'] ?? 'text',
      ],
    ];

    $form['actions'] = [
      '#type' => 'actions',
    ];

    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $tool_id !== NULL ? $this->t('Save tool') : $this->t('Add tool'),
      '#button_type' => 'primary',
    ];

    $form['actions']['cancel'] = [
      '#type' => 'link',
      '#title' => $this->t('Cancel'),
      '#url' => Url::fromRoute('mcp_studio.settings'),
      '#attributes' => [
        'class' => ['button'],
      ],
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    // Validate input schema.
    $input_schema = $form_state->getValue('input_schema');
    if (!empty($input_schema) && $input_schema !== '{}') {
      $decoded = json_decode($input_schema, TRUE);
      if (json_last_error() !== JSON_ERROR_NONE) {
        $form_state->setErrorByName('input_schema', $this->t('Input schema must be valid JSON. Error: @error', [
          '@error' => json_last_error_msg(),
        ]));
      }
      else {
        // Validate it's a proper JSON Schema.
        if (!is_array($decoded)) {
          $form_state->setErrorByName('input_schema', $this->t('Input schema must be a JSON object.'));
        }
        else {
          // Check for valid JSON Schema structure.
          if (!isset($decoded['type']) && !isset($decoded['$ref']) && !isset($decoded['$schema'])) {
            $form_state->setErrorByName('input_schema', $this->t('Input schema must be a valid JSON Schema. At minimum, it should have a "type" property.'));
          }

          // Validate common JSON Schema types.
          if (isset($decoded['type'])) {
            $valid_types = [
              'null',
              'boolean',
              'object',
              'array',
              'number',
              'string',
              'integer',
            ];
            if (!in_array($decoded['type'], $valid_types)) {
              $form_state->setErrorByName('input_schema', $this->t('Invalid JSON Schema type "@type". Valid types are: @types', [
                '@type' => $decoded['type'],
                '@types' => implode(', ', $valid_types),
              ]));
            }

            // Additional validation for object types.
            if ($decoded['type'] === 'object' && isset($decoded['properties'])) {
              if (!is_array($decoded['properties'])) {
                $form_state->setErrorByName('input_schema', $this->t('JSON Schema "properties" must be an object.'));
              }
              else {
                // Validate each property has a type.
                foreach ($decoded['properties'] as $prop_name => $prop_schema) {
                  if (is_array($prop_schema) && !isset($prop_schema['type']) && !isset($prop_schema['$ref'])) {
                    $form_state->setErrorByName('input_schema', $this->t('Property "@prop" must have a "type" defined.', [
                      '@prop' => $prop_name,
                    ]));
                  }
                }
              }
            }

            // Validate required array.
            if (isset($decoded['required']) && !is_array($decoded['required'])) {
              $form_state->setErrorByName('input_schema', $this->t('JSON Schema "required" must be an array of property names.'));
            }
          }
        }
      }
    }

    // Validate output based on mode.
    $output_mode = $form_state->getValue('output_mode');
    $output = $form_state->getValue('output');

    if (!empty($output)) {
      switch ($output_mode) {
        case 'json':
          // Validate JSON syntax.
          $decoded = json_decode($output);
          if (json_last_error() !== JSON_ERROR_NONE) {
            $form_state->setErrorByName('output', $this->t('Output must be valid JSON when JSON mode is selected. Error: @error', [
              '@error' => json_last_error_msg(),
            ]));
          }
          break;

        case 'twig':
          // Comprehensive TWIG validation.
          // Check for balanced delimiters.
          $open_tags = substr_count($output, '{%');
          $close_tags = substr_count($output, '%}');
          if ($open_tags !== $close_tags) {
            $form_state->setErrorByName('output', $this->t('TWIG syntax error: Unbalanced {% %} tags. Found @open opening and @close closing tags.', [
              '@open' => $open_tags,
              '@close' => $close_tags,
            ]));
          }

          $open_vars = substr_count($output, '{{');
          $close_vars = substr_count($output, '}}');
          if ($open_vars !== $close_vars) {
            $form_state->setErrorByName('output', $this->t('TWIG syntax error: Unbalanced {{ }} variables. Found @open opening and @close closing variables.', [
              '@open' => $open_vars,
              '@close' => $close_vars,
            ]));
          }

          $open_comments = substr_count($output, '{#');
          $close_comments = substr_count($output, '#}');
          if ($open_comments !== $close_comments) {
            $form_state->setErrorByName('output', $this->t('TWIG syntax error: Unbalanced {# #} comments. Found @open opening and @close closing comments.', [
              '@open' => $open_comments,
              '@close' => $close_comments,
            ]));
          }

          // Check if there's at least one TWIG construct.
          if ($open_tags === 0 && $open_vars === 0 && $open_comments === 0) {
            // Allow plain text in TWIG mode.
            if (strpos($output, '{') !== FALSE || strpos($output, '}') !== FALSE) {
              $form_state->setErrorByName('output', $this->t('Invalid TWIG syntax: Found incomplete TWIG delimiters. Use {{ }} for variables, {% %} for tags, or {# #} for comments.'));
            }
          }

          // Check for common TWIG syntax errors.
          if (preg_match('/\{\{[^}]*\{[{%]/', $output)) {
            $form_state->setErrorByName('output', $this->t('TWIG syntax error: Nested opening delimiters detected.'));
          }

          if (preg_match('/[}%]\}[^{]*\}\}/', $output)) {
            $form_state->setErrorByName('output', $this->t('TWIG syntax error: Nested closing delimiters detected.'));
          }

          // Validate common TWIG structures.
          if (preg_match_all('/\{%\s*(\w+)/', $output, $matches)) {
            $valid_tags = [
              'if',
              'else',
              'elseif',
              'endif',
              'for',
              'endfor',
              'set',
              'block',
              'endblock',
              'extends',
              'include',
              'import',
              'from',
              'macro',
              'endmacro',
            ];

            foreach ($matches[1] as $tag) {
              if (!in_array($tag, $valid_tags)) {
                $form_state->setErrorByName('output', $this->t('TWIG syntax error: Unknown tag "@tag". Common tags are: if, for, set, block, include.', [
                  '@tag' => $tag,
                ]));
              }
            }
          }
          break;
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $config = $this->configFactory()->getEditable('mcp_studio.settings');
    $tools = $config->get('tools') ?? [];

    $tool_data = [
      'name' => $form_state->getValue('name'),
      'description' => $form_state->getValue('description'),
      'input_schema' => $form_state->getValue('input_schema'),
      'output' => $form_state->getValue('output'),
      'output_mode' => $form_state->getValue('output_mode'),
    ];

    $tool_id = $form_state->get('tool_id');
    if ($tool_id !== NULL) {
      $tools[$tool_id] = $tool_data;
      $this->messenger()->addStatus($this->t('Tool updated successfully.'));
    }
    else {
      $tools[] = $tool_data;
      $this->messenger()->addStatus($this->t('Tool added successfully.'));
    }

    $config->set('tools', $tools)->save();
    $form_state->setRedirectUrl(Url::fromRoute('mcp_studio.settings'));
  }

}

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

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