# elementor/4.3.2/modules/mcp/abilities/manage-component-ability.php

Elementor Website Builder – more than just a page builder, version 4.3.2. 779 lines.

- Page: https://pluginprobe.com/plugins/elementor/4.3.2/code/modules/mcp/abilities/manage-component-ability.php
- Raw: https://pluginprobe.com/plugins/elementor/4.3.2/raw/modules/mcp/abilities/manage-component-ability.php
- Modified: 2026-09-17T09:04:38+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/elementor/4.3.2/code/modules/mcp/abilities/manage-component-ability.php#L10-L20`.

```php
<?php

namespace Elementor\Modules\Mcp\Abilities;

use Elementor\Core\Base\Document;
use Elementor\Core\Utils\Collection;
use Elementor\Core\Utils\Document\Document_Mutator;
use Elementor\Modules\Components\Circular_Dependency_Validator;
use Elementor\Modules\Components\Components_Access_Controller;
use Elementor\Modules\Components\Components_Repository;
use Elementor\Modules\Components\Documents\Component as Component_Document;
use Elementor\Modules\Components\Non_Atomic_Widget_Validator;
use Elementor\Modules\Components\Save_Components_Validator;
use Elementor\Modules\Mcp\Abilities\Utils\Composition_Compiler;
use Elementor\Modules\Mcp\Abilities\Utils\Insufficient_Permissions_Error;
use Elementor\Modules\Mcp\Abilities\Utils\Overridable_Props_Builder;
use Elementor\Modules\Mcp\Abilities\Utils\Prompt_Loader;
use Elementor\Modules\Mcp\Events\Mcp_Event_Dispatcher;
use Elementor\Plugin;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class Manage_Component_Ability extends Abstract_Ability {

	const ACTION_CREATE = 'create';
	const ACTION_UPDATE = 'update';
	const ACTION_RENAME = 'rename';
	const ACTION_ARCHIVE = 'archive';
	const ACTION_PUBLISH = 'publish';

	private ?Components_Repository $repository;

	public function __construct( ?Components_Repository $repository = null ) {
		$this->repository = $repository;
	}

	protected function get_ability_id(): string {
		return 'elementor/manage-component';
	}

	protected function get_definition(): Ability_Definition {
		return new Ability_Definition(
			__( 'Manage Elementor Component', 'elementor' ),
			Prompt_Loader::load( 'manage-component' ),
			'elementor',
			$this->get_output_schema(),
			[
				'annotations' => [
					'readonly' => false,
					'idempotent' => false,
					'destructive' => true,
				],
			],
			fn() => current_user_can( 'manage_options' ),
			$this->get_input_schema()
		);
	}

	public function execute( $input = [] ) {
		$input = is_array( $input ) ? $input : [];

		switch ( $input['action'] ?? null ) {
			case self::ACTION_CREATE:
				return $this->handle_create( $input );
			case self::ACTION_UPDATE:
				return $this->handle_update( $input );
			case self::ACTION_RENAME:
				return $this->handle_rename( $input );
			case self::ACTION_ARCHIVE:
				return $this->handle_archive( $input );
			case self::ACTION_PUBLISH:
				return $this->handle_publish( $input );
		}

		return $this->invalid_input( __( 'action must be one of: create, update, rename, archive, publish.', 'elementor' ) );
	}

	private function handle_create( array $input ) {
		$gate = $this->check_access( self::ACTION_CREATE, fn() => Components_Access_Controller::can_create() );
		if ( $gate ) {
			return $gate;
		}

		$title = isset( $input['title'] ) ? trim( (string) $input['title'] ) : '';
		if ( strlen( $title ) < 2 ) {
			return $this->invalid_input( __( 'create requires a title of at least 2 characters.', 'elementor' ) );
		}

		$source_result = $this->resolve_create_elements( $input );
		if ( is_wp_error( $source_result ) ) {
			return $source_result;
		}
		[ 'elements' => $elements, 'warnings' => $warnings ] = $source_result;
		$source_id_map = $source_result['source_id_map'] ?? [];

		$settings = [];
		$overridable_error = $this->apply_overridable_props( $elements, $input, $settings, $source_id_map );
		if ( is_wp_error( $overridable_error ) ) {
			return $overridable_error;
		}

		$uid = $this->generate_uid();
		$item = [
			'uid' => $uid,
			'title' => $title,
			'elements' => $elements,
			'settings' => $settings,
		];
		$items = Collection::make( [ $item ] );

		$save_validation = Save_Components_Validator::make( $this->get_repository()->all() )->validate( $items );
		if ( ! $save_validation['success'] ) {
			return new \WP_Error(
				'components_validation_failed',
				__( 'Validation failed: ', 'elementor' ) . implode( ' ', $save_validation['messages'] ),
				[ 'status' => \WP_Http::UNPROCESSABLE_ENTITY ]
			);
		}

		$circular_error = $this->validate_circular_dependency(
			fn() => Circular_Dependency_Validator::make()->validate_new_components( $items )
		);
		if ( $circular_error ) {
			return $circular_error;
		}

		$non_atomic_error = $this->validate_atomic_elements( $elements );
		if ( $non_atomic_error ) {
			return $non_atomic_error;
		}

		try {
			$component_id = $this->get_repository()->create( $title, $elements, $this->resolve_status( $input ), $uid, $settings );
		} catch ( \Exception $e ) {
			return new \WP_Error( 'create_failed', $e->getMessage(), [ 'status' => \WP_Http::UNPROCESSABLE_ENTITY ] );
		}

		$component = $this->get_repository()->get( $component_id, false );

		$this->emit_component_created_event( $title, $component_id, $elements );

		$response = [
			'success' => true,
			'component_id' => $component_id,
			'uid' => $uid,
		] + $this->document_links( $component );

		if ( ! empty( $warnings ) ) {
			$response['warnings'] = $warnings;
		}

		return $response;
	}

	private function handle_update( array $input ) {
		$gate = $this->check_access( self::ACTION_UPDATE, fn() => Components_Access_Controller::can_edit() );
		if ( $gate ) {
			return $gate;
		}

		$component_id = isset( $input['component_id'] ) ? (int) $input['component_id'] : 0;
		if ( $component_id <= 0 ) {
			return $this->invalid_input( __( 'update requires component_id.', 'elementor' ) );
		}

		$component = $this->get_repository()->get_for_edit( $component_id, $this->resolve_status( $input ) );
		if ( ! $component ) {
			return $this->not_found( __( 'Component not found.', 'elementor' ) );
		}

		$has_xml = ! empty( $input['xml_structure'] ) && is_string( $input['xml_structure'] );

		return $has_xml
			? $this->update_with_xml( $component, $input )
			: $this->update_overridable_props_only( $component, $input );
	}

	private function handle_rename( array $input ) {
		$gate = $this->check_access( self::ACTION_RENAME, fn() => Components_Access_Controller::can_rename() );
		if ( $gate ) {
			return $gate;
		}

		$component_id = isset( $input['component_id'] ) ? (int) $input['component_id'] : 0;
		$title = isset( $input['title'] ) ? trim( (string) $input['title'] ) : '';

		if ( $component_id <= 0 || strlen( $title ) < 2 ) {
			return $this->invalid_input( __( 'rename requires component_id and a title of at least 2 characters.', 'elementor' ) );
		}

		$success = $this->get_repository()->update_title( $component_id, $title, $this->resolve_status( $input ) );
		if ( ! $success ) {
			return $this->not_found( __( 'Component not found or title could not be updated.', 'elementor' ) );
		}

		return [
			'success' => true,
			'component_id' => $component_id,
		];
	}

	private function handle_archive( array $input ) {
		$gate = $this->check_access( self::ACTION_ARCHIVE, fn() => Components_Access_Controller::can_delete() );
		if ( $gate ) {
			return $gate;
		}

		$component_ids = $this->normalize_ids( $input['component_ids'] ?? [] );
		if ( empty( $component_ids ) ) {
			return $this->invalid_input( __( 'archive requires a non-empty component_ids array of positive integers.', 'elementor' ) );
		}

		$result = $this->get_repository()->archive( $component_ids, $this->resolve_status( $input ) );

		return [
			'success' => empty( $result['failedIds'] ),
			'success_ids' => $result['successIds'],
			'failed_ids' => $result['failedIds'],
		];
	}

	private function handle_publish( array $input ) {
		$gate = $this->check_access( self::ACTION_PUBLISH, fn() => Components_Access_Controller::can_publish() );
		if ( $gate ) {
			return $gate;
		}

		$component_id = isset( $input['component_id'] ) ? (int) $input['component_id'] : 0;
		if ( $component_id <= 0 ) {
			return $this->invalid_input( __( 'publish requires component_id.', 'elementor' ) );
		}

		$component = $this->get_repository()->get( $component_id );
		if ( ! $component ) {
			return $this->not_found( __( 'Component not found.', 'elementor' ) );
		}

		if ( ! $this->get_repository()->publish_component( $component ) ) {
			return new \WP_Error(
				'publish_failed',
				__( 'Failed to publish component.', 'elementor' ),
				[ 'status' => \WP_Http::INTERNAL_SERVER_ERROR ]
			);
		}

		$published = $this->get_repository()->get( $component->get_main_id(), false );

		return [
			'success' => true,
			'component_id' => $component->get_main_id(),
		] + $this->document_links( $published );
	}

	private function update_overridable_props_only( Component_Document $component, array $input ) {
		if ( ! is_array( $input['overridable_props'] ?? null ) || empty( $input['overridable_props'] ) ) {
			return $this->invalid_input( __( 'update without xml_structure requires non-empty overridable_props.', 'elementor' ) );
		}

		$elements = $component->get_elements_data();
		$settings = [];

		$overridable_error = $this->apply_overridable_props( $elements, $input, $settings );
		if ( is_wp_error( $overridable_error ) ) {
			return $overridable_error;
		}

		return $this->save_component( $component, $elements, $settings );
	}

	private function update_with_xml( Component_Document $component, array $input ) {
		$compiled = $this->compile_composition( $input, $component );
		if ( is_wp_error( $compiled ) ) {
			return $compiled;
		}

		$elements = $this->assign_element_ids( $compiled['elements'] );
		$warnings = $compiled['warnings'];

		$non_atomic_error = $this->validate_atomic_elements( $elements );
		if ( $non_atomic_error ) {
			return $non_atomic_error;
		}

		$settings = [];
		$overridable_error = $this->apply_overridable_props( $elements, $input, $settings );
		if ( is_wp_error( $overridable_error ) ) {
			return $overridable_error;
		}

		$circular_error = $this->validate_circular_dependency(
			fn() => Circular_Dependency_Validator::make()->validate( $component->get_main_id(), $elements )
		);
		if ( $circular_error ) {
			return $circular_error;
		}

		$result = $this->save_component( $component, $elements, $settings );
		if ( is_wp_error( $result ) || empty( $warnings ) ) {
			return $result;
		}

		return $result + [ 'warnings' => $warnings ];
	}

	private function save_component( Component_Document $component, array $elements, array $settings ) {
		try {
			$saved = $component->save( [
				'elements' => $elements,
				'settings' => $settings,
			] );
		} catch ( \Exception $e ) {
			if ( str_contains( $e->getMessage(), 'components that contain each other' ) ) {
				return new \WP_Error(
					'circular_dependency_detected',
					__( "Can't add this component - components that contain each other can't be nested.", 'elementor' ),
					[ 'status' => \WP_Http::UNPROCESSABLE_ENTITY ]
				);
			}

			return new \WP_Error( 'save_failed', $e->getMessage(), [ 'status' => \WP_Http::UNPROCESSABLE_ENTITY ] );
		}

		if ( ! $saved ) {
			return new \WP_Error( 'save_failed', __( 'Failed to save component.', 'elementor' ), [ 'status' => \WP_Http::INTERNAL_SERVER_ERROR ] );
		}

		return [
			'success' => true,
			'component_id' => $component->get_main_id(),
		] + $this->document_links( $component );
	}

	private function validate_circular_dependency( callable $validate ): ?\WP_Error {
		$circular_validation = $validate();

		if ( $circular_validation['success'] ) {
			return null;
		}

		return new \WP_Error(
			'circular_dependency_detected',
			__( "Can't add this component - components that contain each other can't be nested.", 'elementor' ),
			[
				'status' => \WP_Http::UNPROCESSABLE_ENTITY,
				'caused_by' => $circular_validation['messages'],
			]
		);
	}

	private function validate_atomic_elements( array $elements ): ?\WP_Error {
		$validation = Non_Atomic_Widget_Validator::make()->validate( $elements );

		if ( $validation['success'] ) {
			return null;
		}

		return new \WP_Error(
			Non_Atomic_Widget_Validator::ERROR_CODE,
			__( 'Components require atomic elements only. Remove widgets to create this component.', 'elementor' ),
			[
				'status' => \WP_Http::UNPROCESSABLE_ENTITY,
				'non_atomic_elements' => $validation['non_atomic_elements'],
			]
		);
	}

	/**
	 * @return array{elements: array[], warnings: string[], source_id_map?: array<string,string>}|\WP_Error
	 */
	private function resolve_create_elements( array $input ) {
		$has_xml = ! empty( $input['xml_structure'] ) && is_string( $input['xml_structure'] );
		$has_source_element = ! empty( $input['source_post_id'] ) && ! empty( $input['element_id'] );

		if ( $has_xml && $has_source_element ) {
			return $this->invalid_input( __( 'create accepts either xml_structure or source_post_id/element_id, not both.', 'elementor' ) );
		}

		if ( $has_xml ) {
			return $this->compile_elements_from_xml( $input );
		}

		if ( $has_source_element ) {
			return $this->copy_elements_from_source( $input );
		}

		return [
			'elements' => [],
			'warnings' => [],
		];
	}

	/**
	 * @return array{elements: array[], warnings: string[]}|\WP_Error
	 */
	private function compile_elements_from_xml( array $input ) {
		$compiled = $this->compile_composition( $input );

		if ( is_wp_error( $compiled ) ) {
			return $compiled;
		}

		return [
			'elements' => $this->assign_element_ids( $compiled['elements'] ),
			'warnings' => $compiled['warnings'],
		];
	}

	/**
	 * @return array{elements: array[], warnings: string[], dom: \DOMDocument, xml_parser: \Elementor\Modules\Mcp\Abilities\Build_Composition\Xml_Parser}|\WP_Error
	 */
	private function compile_composition( array $input, ?Document $document = null ) {
		$compiled = Composition_Compiler::make()->compile(
			$input,
			$document,
			[],
			Composition_Compiler::COMPONENT_PARENT_ID
		);
		if ( is_wp_error( $compiled ) ) {
			return $compiled;
		}

		if ( 1 !== count( $compiled['elements'] ) ) {
			return new \WP_Error(
				'component_requires_single_root',
				__( 'A component composition must contain exactly one root element.', 'elementor' ),
				[ 'status' => \WP_Http::UNPROCESSABLE_ENTITY ]
			);
		}

		return $compiled;
	}

	/**
	 * @return array{elements: array[], warnings: string[], source_id_map: array<string,string>}|\WP_Error
	 */
	private function copy_elements_from_source( array $input ) {
		$source_post_id = (int) $input['source_post_id'];
		$element_id = (string) $input['element_id'];

		if ( ! current_user_can( 'edit_post', $source_post_id ) ) {
			return new \WP_Error(
				'elementor_forbidden',
				__( 'You do not have permission to read source_post_id.', 'elementor' ),
				[ 'status' => \WP_Http::FORBIDDEN ]
			);
		}

		$document = Plugin::$instance->documents->get_doc_or_auto_save( $source_post_id, get_current_user_id() )
			?? Plugin::$instance->documents->get( $source_post_id );

		if ( ! $document ) {
			return $this->not_found( __( 'source_post_id not found.', 'elementor' ) );
		}

		$found = Document_Mutator::instance()->find_by_id( $document->get_elements_data(), $element_id );
		if ( null === $found ) {
			return $this->not_found( __( 'element_id was not found on source_post_id.', 'elementor' ) );
		}

		$source_id_map = [];
		$elements = $this->assign_element_ids_recording_source_ids( [ $found ], $source_id_map );

		return [
			'elements' => $elements,
			'warnings' => [],
			'source_id_map' => $source_id_map,
		];
	}

	/**
	 * Builds the native overridable-props payload and stamps `overridable` envelopes
	 * onto the referenced element settings. Validation and persistence of the native
	 * payload itself is left to the component document's save hook, which already
	 * parses and persists it for every component save.
	 *
	 * @param array[]              $elements
	 * @param array                $input
	 * @param array                $settings
	 * @param array<string,string> $source_id_map old-source-id => new-machine-id, when the tree
	 *                                            came from `copy_elements_from_source`. Used to
	 *                                            let callers address targets by the ids they
	 *                                            saw on the source document, before id regeneration.
	 *
	 * @return \WP_Error|null
	 */
	private function apply_overridable_props( array &$elements, array $input, array &$settings, array $source_id_map = [] ) {
		if ( ! is_array( $input['overridable_props'] ?? null ) || empty( $input['overridable_props'] ) ) {
			return null;
		}

		$definitions = $this->remap_overridable_targets( $input['overridable_props'], $source_id_map );

		$builder_result = Overridable_Props_Builder::make( $this->get_repository() )->build( $elements, $definitions );
		if ( is_wp_error( $builder_result ) ) {
			return $builder_result;
		}

		$settings['overridable_props'] = $builder_result;

		return null;
	}

	/**
	 * Rewrites `overridable_props[*].target` from source-document ids to the freshly assigned
	 * ids, so callers can keep referencing the elements by the ids they observed on the source.
	 * Unknown targets pass through untouched so the xml_structure `configuration-id` path (which
	 * resolves via `editor_settings.title` in `Overridable_Props_Builder::find_element_ref`) and
	 * regular id targets still work.
	 *
	 * @param array                $definitions
	 * @param array<string,string> $source_id_map
	 */
	private function remap_overridable_targets( array $definitions, array $source_id_map ): array {
		if ( empty( $source_id_map ) ) {
			return $definitions;
		}

		foreach ( $definitions as $override_key => &$definition ) {
			if ( ! is_array( $definition ) ) {
				continue;
			}
			$target = $definition['target'] ?? null;
			if ( is_string( $target ) && isset( $source_id_map[ $target ] ) ) {
				$definition['target'] = $source_id_map[ $target ];
			}
		}

		return $definitions;
	}

	private function assign_element_ids( array $elements ): array {
		return array_map( fn( array $element ) => $this->assign_element_id( $element ), $elements );
	}

	private function assign_element_id( array $element ): array {
		$element['id'] = Document_Mutator::instance()->generate_id();

		if ( ! empty( $element['elements'] ) && is_array( $element['elements'] ) ) {
			$element['elements'] = array_map( fn( array $child ) => $this->assign_element_id( $child ), $element['elements'] );
		}

		return $element;
	}

	/**
	 * Regenerates ids like `assign_element_ids`, but records the mapping from each element's
	 * old id to its new id so callers can keep addressing the tree by pre-regeneration ids.
	 *
	 * @param array[]              $elements
	 * @param array<string,string> $source_id_map Populated by reference.
	 */
	private function assign_element_ids_recording_source_ids( array $elements, array &$source_id_map ): array {
		$result = [];
		foreach ( $elements as $element ) {
			$result[] = $this->assign_element_id_recording_source_id( $element, $source_id_map );
		}
		return $result;
	}

	private function assign_element_id_recording_source_id( array $element, array &$source_id_map ): array {
		$old_id = isset( $element['id'] ) ? (string) $element['id'] : '';

		$element['id'] = Document_Mutator::instance()->generate_id();

		if ( '' !== $old_id ) {
			$source_id_map[ $old_id ] = $element['id'];
		}

		if ( ! empty( $element['elements'] ) && is_array( $element['elements'] ) ) {
			$element['elements'] = $this->assign_element_ids_recording_source_ids( $element['elements'], $source_id_map );
		}

		return $element;
	}

	private function document_links( Component_Document $component ): array {
		$editor_url = $component->get_edit_url();

		return [
			'edit_url' => $editor_url,
			'llm_instructions' => sprintf(
				/* translators: %s: Component editor URL. */
				__( 'You MUST show the user this link to review the component: %s', 'elementor' ),
				$editor_url
			),
		];
	}

	private function resolve_status( array $input ): string {
		return 'draft' === ( $input['publish_status'] ?? 'publish' ) ? Document::STATUS_DRAFT : Document::STATUS_PUBLISH;
	}

	private function generate_uid(): string {
		return 'component-' . wp_generate_uuid4();
	}

	private function normalize_ids( $raw ): array {
		if ( ! is_array( $raw ) ) {
			return [];
		}

		$ids = [];
		foreach ( $raw as $value ) {
			$id = (int) $value;
			if ( $id > 0 ) {
				$ids[ $id ] = $id;
			}
		}

		return array_values( $ids );
	}

	private function check_access( string $action, callable $can ): ?\WP_Error {
		return $can() ? null : Insufficient_Permissions_Error::for_action( $action );
	}

	private function invalid_input( string $message ): \WP_Error {
		return new \WP_Error( 'invalid_input', $message, [ 'status' => \WP_Http::BAD_REQUEST ] );
	}

	private function not_found( string $message ): \WP_Error {
		return new \WP_Error( 'elementor_not_found', $message, [ 'status' => \WP_Http::NOT_FOUND ] );
	}

	private function get_repository(): Components_Repository {
		if ( ! $this->repository ) {
			$this->repository = new Components_Repository();
		}

		return $this->repository;
	}

	private function get_output_schema(): array {
		return [
			'type' => 'object',
			'properties' => [
				'success' => [ 'type' => 'boolean' ],
				'component_id' => [ 'type' => 'integer' ],
				'uid' => [
					'type' => 'string',
					'description' => 'Server-generated component uid, only present for create.',
				],
				'success_ids' => [
					'type' => 'array',
					'items' => [ 'type' => 'integer' ],
					'description' => 'archive only.',
				],
				'failed_ids' => [
					'type' => 'array',
					'items' => [ 'type' => 'integer' ],
					'description' => 'archive only.',
				],
				'edit_url' => [
					'type' => 'string',
					'description' => 'Component documents have no public permalink; this is the editor URL to review the change.',
				],
				'llm_instructions' => [
					'type' => 'string',
					'description' => 'Mandatory next step: include this text (with the editor link) in your reply to the user.',
				],
				'warnings' => [
					'type' => 'array',
					'items' => [ 'type' => 'string' ],
					'description' => 'Non-fatal notices from XML compilation, e.g. props skipped or CSS that fell back to custom_css.',
				],
			],
		];
	}

	private function get_input_schema(): array {
		return [
			'type' => 'object',
			'required' => [ 'action' ],
			'properties' => [
				'action' => [
					'type' => 'string',
					'enum' => [ self::ACTION_CREATE, self::ACTION_UPDATE, self::ACTION_RENAME, self::ACTION_ARCHIVE, self::ACTION_PUBLISH ],
				],
				'title' => [
					'type' => 'string',
					'minLength' => 2,
					'maxLength' => 200,
					'description' => 'create: component title. rename: the new title.',
				],
				'source_post_id' => [
					'type' => 'integer',
					'description' => 'create: WordPress post id to copy an existing element subtree from. Requires element_id. Mutually exclusive with xml_structure.',
				],
				'element_id' => [
					'type' => 'string',
					'description' => 'create: id of the element (within source_post_id, from elementor/get-page-structure) to copy as the component root.',
				],
				'xml_structure' => [
					'type' => 'string',
					'description' => 'create/update: same tag language as elementor/build-composition, with exactly one root element. Mutually exclusive with source_post_id/element_id.',
				],
				'element_config' => [
					'type' => 'object',
					'default' => (object) [],
					'description' => 'Same shape as elementor/build-composition element_config. Only used with xml_structure.',
				],
				'classes' => [
					'type' => 'object',
					'default' => (object) [],
					'description' => 'Same shape as elementor/build-composition classes. Only used with xml_structure.',
				],
				'style' => [
					'type' => 'object',
					'default' => (object) [],
					'description' => 'Same shape as elementor/build-composition style. Only used with xml_structure.',
				],
				'interactions' => [
					'type' => 'object',
					'default' => (object) [],
					'description' => 'Same shape as elementor/build-composition interactions. Only used with xml_structure.',
				],
				'overridable_props' => [
					'type' => 'object',
					'description' => 'Caller-supplied record mapping override-key → { target, prop_key, label, group? }. target identifies the element to expose: for xml_structure, use the same configuration-id you set on that element; for source_post_id/element_id (create) or an update without xml_structure, use the real element id (from elementor/get-page-structure). Group IDs and origin values are generated server-side.',
				],
				'publish_status' => [
					'type' => 'string',
					'enum' => [ 'publish', 'draft' ],
					'default' => 'publish',
					'description' => 'create: initial document status. update/rename/archive: "draft" edits an autosave copy instead of the live document; the change is not visible on pages using this component until you publish it.',
				],
				'component_id' => [
					'type' => 'integer',
					'description' => 'update/rename/publish target component id.',
				],
				'component_ids' => [
					'type' => 'array',
					'items' => [ 'type' => 'integer' ],
					'description' => 'archive targets.',
				],
			],
		];
	}

	private function emit_component_created_event( string $title, int $component_id, array $elements ): void {
		[ 'elements_count' => $nested_elements_count, 'components_count' => $nested_components_count ] = $this->count_nested_elements( $elements );
		$top_element_type = $elements[0]['elType'] ?? ( $elements[0]['widgetType'] ?? '' );

		Mcp_Event_Dispatcher::emit( 'component_created', [
			'id'                      => (string) $component_id,
			'name'                    => $title,
			'nested_elements_count'   => $nested_elements_count,
			'nested_components_count' => $nested_components_count,
			'top_element_type'        => $top_element_type,
		] );
	}

	private function count_nested_elements( array $elements ): array {
		$elements_count   = count( $elements );
		$components_count = 0;

		foreach ( $elements as $element ) {
			if ( 'e-component' === ( $element['widgetType'] ?? '' ) ) {
				$components_count++;
			}

			$children = $element['elements'] ?? [];

			if ( ! empty( $children ) ) {
				$child_counts      = $this->count_nested_elements( $children );
				$elements_count   += $child_counts['elements_count'];
				$components_count += $child_counts['components_count'];
			}
		}

		return [
			'elements_count'   => $elements_count,
			'components_count' => $components_count,
		];
	}
}

```
