@elementor/editor-canvas 4.2.0-beta2 → 4.3.0-1000

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/index.d.mts +1 -1
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +448 -1318
  4. package/dist/index.mjs +313 -1196
  5. package/package.json +22 -22
  6. package/src/index.ts +1 -1
  7. package/src/legacy/__tests__/init-legacy-views.test.ts +1 -1
  8. package/src/mcp/canvas-mcp.ts +4 -2
  9. package/src/mcp/resources/__tests__/available-widgets-resource.test.ts +76 -0
  10. package/src/mcp/resources/__tests__/dynamic-tags-resource.test.ts +27 -37
  11. package/src/mcp/resources/__tests__/widgets-schema-resource.test.ts +79 -76
  12. package/src/mcp/resources/available-widgets-resource.ts +31 -40
  13. package/src/mcp/resources/best-practices-resource.ts +34 -0
  14. package/src/mcp/resources/dynamic-tags-resource.ts +28 -29
  15. package/src/mcp/resources/widgets-schema-resource.ts +34 -177
  16. package/src/mcp/tools/build-composition/tool.ts +107 -218
  17. package/src/sync/element-added-event.ts +8 -0
  18. package/src/utils/__tests__/grid-outline-utils.test.ts +39 -0
  19. package/src/utils/grid-outline-utils.ts +13 -2
  20. package/src/composition-builder/__tests__/composition-builder.test.ts +0 -352
  21. package/src/composition-builder/composition-builder.ts +0 -350
  22. package/src/composition-builder/utils/__tests__/required-children-enforcer.test.ts +0 -79
  23. package/src/composition-builder/utils/__tests__/required-default-child-tags.test.ts +0 -35
  24. package/src/composition-builder/utils/required-children-enforcer.ts +0 -56
  25. package/src/composition-builder/utils/required-default-child-tags.ts +0 -22
  26. package/src/mcp/resources/best-practices.ts +0 -159
  27. package/src/mcp/resources/build-llm-guidance.ts +0 -110
  28. package/src/mcp/tools/build-composition/__tests__/xml-leaf-wrapper.test.ts +0 -117
  29. package/src/mcp/tools/build-composition/prompt.ts +0 -167
  30. package/src/mcp/tools/build-composition/schema.ts +0 -42
  31. package/src/mcp/tools/build-composition/xml-leaf-wrapper.ts +0 -68
  32. package/src/mcp/utils/__tests__/get-composition-target-container.test.ts +0 -59
  33. package/src/mcp/utils/get-composition-target-container.ts +0 -15
@@ -1,29 +1,24 @@
1
1
  import { type MCPRegistryEntry } from '@elementor/editor-mcp';
2
- import { type PropType, Schema } from '@elementor/editor-props';
3
-
4
- import {
5
- type AtomicDynamicTag,
6
- getAtomicDynamicTags,
7
- OMITTED_DYNAMIC_SETTING_KEYS,
8
- } from '../utils/resolve-dynamic-tag';
2
+ import { type HttpResponse, httpService } from '@elementor/http-client';
9
3
 
10
4
  export const DYNAMIC_TAGS_URI = 'elementor://dynamic-tags';
11
5
 
12
- const settingsSchema = ( propsSchema: AtomicDynamicTag[ 'props_schema' ] ): Record< string, unknown > => {
13
- return Object.fromEntries(
14
- Object.entries( propsSchema ?? {} )
15
- .filter( ( [ key ] ) => ! ( OMITTED_DYNAMIC_SETTING_KEYS as readonly string[] ).includes( key ) )
16
- .map( ( [ key, propType ] ) => [ key, Schema.propTypeToJsonSchema( propType as PropType ) ] )
17
- );
6
+ const MCP_PROXY_URL = 'elementor/v1/mcp-proxy';
7
+
8
+ type DynamicTagEntry = {
9
+ name: string;
10
+ label: string;
11
+ categories: string[];
12
+ settings: Record< string, unknown >;
18
13
  };
19
14
 
20
- const buildDynamicTagsList = () => {
21
- return Object.values( getAtomicDynamicTags() ).map( ( tag ) => ( {
22
- name: tag.name,
23
- label: tag.label,
24
- categories: tag.categories,
25
- settings: settingsSchema( tag.props_schema ),
26
- } ) );
15
+ const fetchDynamicTags = async (): Promise< DynamicTagEntry[] > => {
16
+ const { data } = await httpService().post< HttpResponse< DynamicTagEntry[] > >( MCP_PROXY_URL, {
17
+ tool: 'list-dynamic-tags',
18
+ input: {},
19
+ } );
20
+
21
+ return data.data ?? [];
27
22
  };
28
23
 
29
24
  export const initDynamicTagsResource = ( reg: MCPRegistryEntry ) => {
@@ -39,14 +34,18 @@ export const initDynamicTagsResource = ( reg: MCPRegistryEntry ) => {
39
34
  'name appears in that property\'s allowed list, and populate "settings" per the tag entry here.',
40
35
  mimeType: 'application/json',
41
36
  },
42
- async ( uri: URL ) => ( {
43
- contents: [
44
- {
45
- uri: uri.href,
46
- mimeType: 'application/json',
47
- text: JSON.stringify( buildDynamicTagsList() ),
48
- },
49
- ],
50
- } )
37
+ async ( uri: URL ) => {
38
+ const tags = await fetchDynamicTags();
39
+
40
+ return {
41
+ contents: [
42
+ {
43
+ uri: uri.href,
44
+ mimeType: 'application/json',
45
+ text: JSON.stringify( tags ),
46
+ },
47
+ ],
48
+ };
49
+ }
51
50
  );
52
51
  };
@@ -1,109 +1,51 @@
1
- import { getWidgetsCache } from '@elementor/editor-elements';
2
1
  import { type MCPRegistryEntry, ResourceTemplate } from '@elementor/editor-mcp';
3
- import {
4
- type ArrayPropType,
5
- type ObjectPropType,
6
- type PropType,
7
- Schema,
8
- type TransformablePropType,
9
- type UnionPropType,
10
- } from '@elementor/editor-props';
11
-
12
- import { hasV3Controls, isWidgetAvailableForLLM } from '../utils/element-data-util';
13
- import { buildLlmGuidance, enrichPropertiesWithBaseSettingsHints } from './build-llm-guidance';
14
-
15
- const V3_LAYOUT_CONTROL_TYPES = new Set( [ 'section', 'tab', 'tabs' ] );
16
-
17
- type V3ControlMetadataEntry = {
18
- default?: unknown;
19
- type?: string;
20
- options?: unknown;
21
- };
22
-
23
- function extractV3ControlsMetadata( controls: unknown ): Record< string, V3ControlMetadataEntry > {
24
- if ( ! hasV3Controls( controls ) ) {
25
- return {};
26
- }
27
- const result: Record< string, V3ControlMetadataEntry > = {};
28
- for ( const [ controlKey, raw ] of Object.entries( controls as Record< string, unknown > ) ) {
29
- if ( ! raw || typeof raw !== 'object' ) {
30
- continue;
31
- }
32
- const control = raw as Record< string, unknown >;
33
- const controlType = typeof control.type === 'string' ? control.type : undefined;
34
- if ( controlType && V3_LAYOUT_CONTROL_TYPES.has( controlType ) ) {
35
- continue;
36
- }
37
- const entry: V3ControlMetadataEntry = {};
38
- if ( Object.prototype.hasOwnProperty.call( control, 'default' ) ) {
39
- entry.default = control.default;
40
- }
41
- if ( controlType ) {
42
- entry.type = controlType;
43
- }
44
- if ( Object.prototype.hasOwnProperty.call( control, 'options' ) && control.options !== undefined ) {
45
- const options = control.options;
46
- if ( options && typeof options === 'object' && ! Array.isArray( options ) ) {
47
- entry.options = Object.keys( options as Record< string, unknown > );
48
- } else {
49
- entry.options = options;
50
- }
51
- }
52
- result[ controlKey ] = entry;
53
- }
54
- return result;
55
- }
2
+ import { type HttpResponse, httpService } from '@elementor/http-client';
56
3
 
57
4
  export const CANVAS_SERVER_NAME = 'editor-canvas';
58
5
 
59
6
  export const WIDGET_SCHEMA_URI = 'elementor://widgets/schema/{widgetType}';
60
7
  export const WIDGET_SCHEMA_FULL_URI = `${ CANVAS_SERVER_NAME }_${ WIDGET_SCHEMA_URI }`;
61
8
  export const STYLE_SCHEMA_URI = 'elementor://styles/schema/{category}';
62
- export const BEST_PRACTICES_URI = 'elementor://styles/best-practices';
9
+ export const BEST_PRACTICES_URI = 'elementor://style/best-practices';
63
10
  export const BEST_PRACTICES_FULL_URI = `${ CANVAS_SERVER_NAME }_${ BEST_PRACTICES_URI }`;
64
11
 
65
- export const initWidgetsSchemaResource = ( reg: MCPRegistryEntry ) => {
66
- const { resource } = reg;
12
+ const MCP_PROXY_URL = 'elementor/v1/mcp-proxy';
67
13
 
68
- resource(
69
- 'styles-best-practices',
70
- BEST_PRACTICES_URI,
71
- {
72
- description: 'Styling best practices',
73
- },
74
- async () => {
75
- return {
76
- contents: [
77
- {
78
- uri: BEST_PRACTICES_URI,
79
- text: `# Styling best practices
80
- Prefer using "em" and "rem" values for text-related sizes, padding and spacing. Use percentages for dynamic sizing relative to parent containers.
81
- This flexboxes are by default "flex" with "stretch" alignment. To ensure proper layout, define the "justify-content" and "align-items" as in the schema.
14
+ type WidgetSummary = {
15
+ type: string;
16
+ version: 'v3' | 'v4';
17
+ description?: string;
18
+ };
82
19
 
83
- Styling is provided as raw CSS. The css string must follow standard CSS syntax, with properties and values separated by semicolons, no selectors, or nesting rules allowed.
20
+ const listWidgetTypes = async (): Promise< string[] > => {
21
+ const { data } = await httpService().post< HttpResponse< WidgetSummary[] > >( MCP_PROXY_URL, {
22
+ tool: 'list-widgets',
23
+ input: {},
24
+ } );
84
25
 
85
- ** CRITICAL - VARIABLES **
86
- When using global variables, ensure that the variables are defined in the ${ 'elementor://global-variables' } resource.
87
- Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
26
+ return ( data.data ?? [] ).map( ( widget ) => widget.type );
27
+ };
88
28
 
89
- `,
90
- },
91
- ],
92
- };
93
- }
94
- );
29
+ const fetchWidgetSchema = async ( widgetType: string ): Promise< Record< string, unknown > > => {
30
+ const { data } = await httpService().post< HttpResponse< Record< string, unknown > > >( MCP_PROXY_URL, {
31
+ tool: 'get-widget-schema',
32
+ input: { widget_type: widgetType },
33
+ } );
34
+
35
+ return data.data ?? {};
36
+ };
37
+
38
+ export const initWidgetsSchemaResource = ( reg: MCPRegistryEntry ) => {
39
+ const { resource } = reg;
95
40
 
96
41
  resource(
97
42
  'widget-schema-by-type',
98
43
  new ResourceTemplate( WIDGET_SCHEMA_URI, {
99
- list: () => {
100
- const cache = getWidgetsCache() || {};
101
- const availableWidgets = Object.keys( cache ).filter( ( widgetType ) =>
102
- isWidgetAvailableForLLM( cache[ widgetType ] )
103
- );
44
+ list: async () => {
45
+ const widgetTypes = await listWidgetTypes();
104
46
 
105
47
  return {
106
- resources: availableWidgets.map( ( widgetType ) => ( {
48
+ resources: widgetTypes.map( ( widgetType ) => ( {
107
49
  uri: `elementor://widgets/schema/${ widgetType }`,
108
50
  name: 'Widget schema for ' + widgetType,
109
51
  } ) ),
@@ -116,107 +58,22 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
116
58
  async ( uri, variables ) => {
117
59
  const widgetType =
118
60
  typeof variables.widgetType === 'string' ? variables.widgetType : variables.widgetType?.[ 0 ];
119
- const widgetData = getWidgetsCache()?.[ widgetType ];
120
- if ( ! widgetData ) {
121
- throw new Error( `No prop schema found for element type: ${ widgetType }` );
122
- }
123
- const propSchema = widgetData.atomic_props_schema;
124
- if ( ! propSchema ) {
125
- if ( ! hasV3Controls( widgetData.controls ) ) {
126
- throw new Error( `No prop schema found for element type: ${ widgetType }` );
127
- }
128
- const controlMetadata = extractV3ControlsMetadata( widgetData.controls );
129
- return {
130
- contents: [
131
- {
132
- uri: uri.toString(),
133
- mimeType: 'application/json',
134
- text: JSON.stringify( {
135
- widget_version: 'v3',
136
- message:
137
- 'This widget exists in the editor but has no atomic props schema (V4). Use control_metadata as non-authoritative hints from legacy controls.',
138
- fields_note: 'All settings are optional; there is no JSON schema for this widget type.',
139
- properties: controlMetadata,
140
- } ),
141
- },
142
- ],
143
- };
144
- }
145
- const baseSettingsKeys = Object.keys( widgetData?.base_settings ?? {} );
146
-
147
- const asJson = enrichPropertiesWithBaseSettingsHints(
148
- Object.fromEntries(
149
- Object.entries( propSchema )
150
- .filter( ( [ key, propType ] ) => Schema.isPropKeyConfigurable( key, propType as PropType ) )
151
- .map( ( [ key, propType ] ) => [ key, Schema.propTypeToJsonSchema( propType ) ] )
152
- ),
153
- baseSettingsKeys
154
- );
155
61
 
156
- const description =
157
- typeof widgetData?.meta?.description === 'string' ? widgetData.meta.description : undefined;
62
+ if ( ! widgetType ) {
63
+ throw new Error( 'No widget type provided.' );
64
+ }
158
65
 
159
- const allWidgets = getWidgetsCache() || {};
160
- const llmGuidance = buildLlmGuidance( widgetData, widgetType, allWidgets );
66
+ const schema = await fetchWidgetSchema( widgetType );
161
67
 
162
68
  return {
163
69
  contents: [
164
70
  {
165
71
  uri: uri.toString(),
166
72
  mimeType: 'application/json',
167
- text: JSON.stringify( {
168
- type: 'object',
169
- properties: asJson,
170
- description,
171
- llm_guidance: llmGuidance,
172
- } ),
73
+ text: JSON.stringify( schema ),
173
74
  },
174
75
  ],
175
76
  };
176
77
  }
177
78
  );
178
79
  };
179
-
180
- function cleanupPropSchema( propSchema: Record< string, PropType > ): Record< string, PropType > {
181
- const result: Record< string, Partial< PropType > > = {};
182
- Object.keys( propSchema ).forEach( ( propName ) => {
183
- result[ propName ] = cleanupPropType( propSchema[ propName ] );
184
- } );
185
- return result as Record< string, PropType >;
186
- }
187
- function cleanupPropType( propType: PropType & { key?: string } ): Partial< PropType > {
188
- const result: Partial< PropType > = {};
189
- Object.keys( propType ).forEach( ( property ) => {
190
- switch ( property ) {
191
- case 'key':
192
- case 'kind':
193
- ( result as Record< string, unknown > )[ property ] = propType[ property ];
194
- break;
195
- case 'meta':
196
- case 'settings':
197
- {
198
- if ( Object.keys( propType[ property ] || {} ).length > 0 ) {
199
- ( result as Record< string, unknown > )[ property ] = propType[ property ];
200
- }
201
- }
202
- break;
203
- }
204
- } );
205
- if ( result.kind === 'plain' ) {
206
- Object.defineProperty( result, 'kind', { value: 'string' } );
207
- } else if ( result.kind === 'array' ) {
208
- result.item_prop_type = cleanupPropType( ( propType as ArrayPropType ).item_prop_type ) as PropType;
209
- } else if ( result.kind === 'object' ) {
210
- const shape = ( propType as ObjectPropType ).shape as Record< string, PropType >;
211
- const cleanedShape = cleanupPropSchema( shape );
212
- result.shape = cleanedShape;
213
- } else if ( result.kind === 'union' ) {
214
- const propTypes = ( propType as UnionPropType ).prop_types;
215
- const cleanedPropTypes: Record< string, Partial< PropType > > = {};
216
- Object.keys( propTypes ).forEach( ( key ) => {
217
- cleanedPropTypes[ key ] = cleanupPropType( propTypes[ key ] );
218
- } );
219
- result.prop_types = cleanedPropTypes as Record< string, TransformablePropType >;
220
- }
221
- return result;
222
- }
@@ -1,244 +1,133 @@
1
- import { getCurrentDocument } from '@elementor/editor-documents';
2
- import {
3
- createElement,
4
- deleteElement,
5
- getContainer,
6
- getWidgetsCache,
7
- type V1Element,
8
- type V1ElementData,
9
- } from '@elementor/editor-elements';
1
+ import { getCurrentDocument, reloadCurrentDocument } from '@elementor/editor-documents';
2
+ import { getContainer, selectElement } from '@elementor/editor-elements';
10
3
  import { type MCPRegistryEntry } from '@elementor/editor-mcp';
11
- import { dispatchMcpStylesAppliedEvent } from '@elementor/editor-mcp';
12
-
13
- import { CompositionBuilder } from '../../../composition-builder/composition-builder';
14
- import { trackCanvasEvent } from '../../../utils/tracking';
15
- import { AVAILABLE_WIDGETS_URI_V4 } from '../../resources/available-widgets-resource';
16
- import { DYNAMIC_TAGS_URI } from '../../resources/dynamic-tags-resource';
17
- import { BEST_PRACTICES_URI, WIDGET_SCHEMA_URI } from '../../resources/widgets-schema-resource';
18
- import { convertStyleBlocksToAtomic } from '../../utils/convert-css-to-atomic';
19
- import { isWidgetAvailableForLLM } from '../../utils/element-data-util';
20
- import { getCompositionTargetContainer } from '../../utils/get-composition-target-container';
21
- import { BUILD_COMPOSITIONS_GUIDE_URI, generatePrompt } from './prompt';
22
- import { inputSchema as schema, outputSchema } from './schema';
23
- import { adaptLeafRootParams } from './xml-leaf-wrapper';
24
-
25
- export type ElementAddedEvent = {
26
- element: V1ElementData;
27
- executedBy: 'mcp_tool' | 'user';
4
+ import { AxiosError, type HttpResponse, httpService } from '@elementor/http-client';
5
+ import { z } from '@elementor/schema';
6
+
7
+ const MCP_PROXY_URL = 'elementor/v1/mcp-proxy';
8
+
9
+ type BuildCompositionResponse = {
10
+ success: boolean;
11
+ post_id: number;
12
+ root_element_ids: string[];
13
+ preview_url: string;
14
+ version: string;
15
+ resolved_xml: string;
16
+ llm_instructions: string;
17
+ warnings?: string[];
28
18
  };
29
19
 
30
- export const ELEMENT_ADDED_EVENT = 'elementor/canvas/element-added';
31
-
32
- export const initBuildCompositionsTool = ( reg: MCPRegistryEntry ) => {
33
- const { addTool, resource } = reg;
20
+ export const initBuildCompositionTool = ( reg: MCPRegistryEntry ) => {
21
+ const { addTool } = reg;
34
22
 
35
- resource(
36
- 'build-compositions-guide',
37
- BUILD_COMPOSITIONS_GUIDE_URI,
38
- {
39
- title: 'Build Compositions Guide',
40
- description: 'Detailed guide for using the build-compositions tool',
41
- mimeType: 'text/plain',
23
+ addTool( {
24
+ name: 'build-composition',
25
+ description:
26
+ 'Build a V4 element composition on the Elementor canvas via the server-side MCP ability. ' +
27
+ 'Pass the raw XML tags directly as xmlStructure — do NOT wrap the value in <![CDATA[ ... ]]>, ' +
28
+ 'code fences, or quotes. The document is saved as a draft. Reload the editor after calling ' +
29
+ 'this tool to see the result.',
30
+ schema: {
31
+ xmlStructure: z
32
+ .string()
33
+ .describe(
34
+ 'Valid XML structure with custom Elementor widget tags. Every element MUST have a unique ' +
35
+ 'configuration-id attribute (e.g. <e-heading configuration-id="hero-title"></e-heading>). ' +
36
+ 'No attributes, classes, IDs, or text nodes in XML. Pass raw XML — do not wrap in CDATA.'
37
+ ),
38
+ elementConfig: z
39
+ .record(
40
+ z.string().describe( 'configuration-id' ),
41
+ z.record( z.string().describe( 'property name' ), z.any().describe( 'PropValue' ) )
42
+ )
43
+ .optional()
44
+ .describe( 'Map configuration-id → widget PropValues ($$type + value).' ),
45
+ style: z
46
+ .record(
47
+ z.string().describe( 'configuration-id' ),
48
+ z.record( z.string().describe( 'CSS property name' ), z.string().describe( 'CSS value' ) )
49
+ )
50
+ .optional()
51
+ .describe(
52
+ 'Map configuration-id → raw CSS declarations (property → value strings; no selectors). ' +
53
+ 'Server converts to native styles; unconvertible declarations become the element custom CSS.'
54
+ ),
55
+ parentId: z
56
+ .string()
57
+ .optional()
58
+ .describe( "ID of the parent container. Omit or pass 'document' to insert at document root." ),
59
+ dryRun: z
60
+ .boolean()
61
+ .optional()
62
+ .describe( 'If true, validate and return the resolved tree without persisting.' ),
63
+ },
64
+ outputSchema: {
65
+ rootElementIds: z.array( z.string() ),
66
+ previewUrl: z.string(),
67
+ version: z.string(),
68
+ resolvedXml: z.string(),
69
+ llmInstructions: z.string(),
70
+ warnings: z.array( z.string() ).optional(),
42
71
  },
43
- async ( uri: URL ) => ( {
44
- contents: [ { uri: uri.href, mimeType: 'text/plain', text: generatePrompt() } ],
45
- } )
46
- );
72
+ handler: async ( { xmlStructure, elementConfig, style, parentId, dryRun } ) => {
73
+ const document = getCurrentDocument();
47
74
 
48
- addTool( {
49
- name: 'build-compositions',
50
- description: 'Build V4 element compositions on the Elementor canvas. Read the guide resource before use.',
51
- schema,
52
- requiredResources: [
53
- { description: 'Build compositions guide', uri: BUILD_COMPOSITIONS_GUIDE_URI },
54
- { description: 'Widgets schema', uri: WIDGET_SCHEMA_URI },
55
- { description: 'Global Classes', uri: 'elementor://global-classes' },
56
- { description: 'Global Variables', uri: 'elementor://global-variables' },
57
- { description: 'Styles best practices', uri: BEST_PRACTICES_URI },
58
- { description: 'Available widgets for this tool', uri: AVAILABLE_WIDGETS_URI_V4 },
59
- { description: 'Dynamic tags catalog', uri: DYNAMIC_TAGS_URI },
60
- ],
61
- outputSchema,
62
- handler: async ( rawParams ) => {
63
- assertCompositionXmlUsesV4WidgetsOnly( rawParams.xmlStructure );
64
- const { stylesConfig: convertedStyles, customCSS } = await convertCompositionStyles( rawParams.style );
65
- const { xmlStructure, elementConfig, stylesConfig } = adaptLeafRootParams( {
66
- ...rawParams,
67
- stylesConfig: convertedStyles,
68
- widgetsCache: getWidgetsCache() ?? {},
69
- } );
75
+ if ( ! document?.id ) {
76
+ throw new Error( 'No active document found.' );
77
+ }
70
78
 
71
- let generatedXML: string = '';
72
- const errors: Error[] = [];
73
- const rootContainers: V1Element[] = [];
74
- const documentContainer = getContainer( 'document' ) as unknown as V1Element;
75
- const currentDocument = getCurrentDocument();
76
- const targetContainer = getCompositionTargetContainer( documentContainer, currentDocument?.type.value );
77
79
  try {
78
- const compositionBuilder = CompositionBuilder.fromXMLString( xmlStructure, {
79
- createElement,
80
- deleteElement,
81
- getWidgetsCache,
80
+ const { data } = await httpService().post< HttpResponse< BuildCompositionResponse > >( MCP_PROXY_URL, {
81
+ tool: 'build-composition',
82
+ input: {
83
+ post_id: document.id,
84
+ xml_structure: xmlStructure,
85
+ element_config: elementConfig ?? {},
86
+ style: style ?? {},
87
+ parent_id: parentId ?? 'document',
88
+ dry_run: dryRun ?? false,
89
+ },
82
90
  } );
83
- compositionBuilder.setElementConfig( elementConfig );
84
- compositionBuilder.setStylesConfig( stylesConfig );
85
- compositionBuilder.setCustomCSS( customCSS );
86
-
87
- const {
88
- configErrors,
89
- formErrors,
90
- rootContainers: generatedRootContainers,
91
- } = await compositionBuilder.build( targetContainer );
92
-
93
- rootContainers.push( ...generatedRootContainers );
94
- generatedXML = new XMLSerializer().serializeToString( compositionBuilder.getXML() );
95
91
 
96
- rootContainers.forEach( ( container ) => {
97
- const elementData = container.model?.toJSON();
92
+ if ( ! dryRun ) {
93
+ await reloadCurrentDocument();
98
94
 
99
- if ( elementData ) {
100
- onElementAdded( elementData as V1ElementData );
95
+ const [ firstRootId ] = data.data.root_element_ids;
96
+ if ( firstRootId ) {
97
+ selectElement( firstRootId );
98
+ getContainer( firstRootId )?.view?.el?.scrollIntoView( {
99
+ behavior: 'smooth',
100
+ block: 'center',
101
+ } );
101
102
  }
102
- } );
103
-
104
- Object.values( stylesConfig ).forEach( ( styleValue ) => {
105
- dispatchMcpStylesAppliedEvent( { styleValue } );
106
- } );
107
-
108
- if ( configErrors.length ) {
109
- errors.push( ...configErrors.map( ( msg ) => new Error( msg ) ) );
110
103
  }
111
104
 
112
- if ( formErrors.length ) {
113
- errors.push( ...formErrors.map( ( msg ) => new Error( msg ) ) );
114
- }
105
+ return {
106
+ rootElementIds: data.data.root_element_ids,
107
+ previewUrl: data.data.preview_url,
108
+ version: data.data.version,
109
+ resolvedXml: data.data.resolved_xml,
110
+ llmInstructions: data.data.llm_instructions,
111
+ warnings: data.data.warnings,
112
+ };
115
113
  } catch ( error ) {
116
- errors.push( error as Error );
117
- }
118
-
119
- if ( errors.length ) {
120
- rootContainers.forEach( ( rootContainer ) => {
121
- deleteElement( {
122
- container: rootContainer,
123
- options: { useHistory: false },
124
- } );
125
- } );
126
-
127
- const errorMessages = errors
128
- .map( ( e ) => {
129
- if ( typeof e === 'string' ) {
130
- return e;
131
- }
132
- if ( e instanceof Error ) {
133
- return e.message || String( e );
134
- }
135
-
136
- if ( typeof e === 'object' && e !== null ) {
137
- return JSON.stringify( e );
138
- }
139
- return String( e );
140
- } )
141
- .filter(
142
- ( msg ) => msg && msg.trim() !== '' && msg !== '{}' && msg !== 'null' && msg !== 'undefined'
143
- );
144
-
145
- if ( errorMessages.length === 0 ) {
146
- throw new Error(
147
- 'Failed to build composition: Unknown error occurred. No error details available.'
148
- );
149
- }
150
-
151
- const errorText = `Failed to build composition with the following errors:\n\n${ errorMessages.join(
152
- '\n\n'
153
- ) }`;
154
- throw new Error( errorText );
114
+ throw new Error( getErrorMessage( error ) );
155
115
  }
156
- return {
157
- xmlStructure: generatedXML,
158
- errors: errors?.length
159
- ? errors.map( ( e ) => ( typeof e === 'string' ? e : e.message ) ).join( '\n\n' )
160
- : undefined,
161
- llm_instructions: `The composition was built successfully with element IDs embedded in the XML.
162
-
163
- **CRITICAL NEXT STEPS** (Follow in order):
164
- 1. **Apply Global Classes**: Use "apply-global-class" tool to apply the global classes you created BEFORE building this composition
165
- - Check the created element IDs in the returned XML
166
- - Apply semantic classes (heading-primary, button-cta, etc.) to appropriate elements
167
-
168
- 2. **Fine-tune if needed**: Use "configure-element" tool only for element-specific adjustments that don't warrant global classes
169
-
170
- Remember: Global classes ensure design consistency and reusability. Don't skip applying them!
171
- `,
172
- };
173
116
  },
174
117
  } );
175
118
  };
176
119
 
177
- async function convertCompositionStyles( style: Record< string, Record< string, string > > ) {
178
- const stylesConfig: Record< string, Record< string, unknown > > = {};
179
- const customCSS: Record< string, string > = {};
180
-
181
- if ( ! style || Object.keys( style ).length === 0 ) {
182
- return { stylesConfig, customCSS };
183
- }
184
-
185
- const results = await convertStyleBlocksToAtomic( style );
186
-
187
- for ( const [ configId, { props, customCss } ] of Object.entries( results ) ) {
188
- stylesConfig[ configId ] = props;
189
- if ( customCss ) {
190
- customCSS[ configId ] = customCss;
120
+ function getErrorMessage( error: unknown ): string {
121
+ if ( error instanceof AxiosError ) {
122
+ const data = error.response?.data as { message?: string; code?: string } | undefined;
123
+ if ( data?.message ) {
124
+ return data.code ? `${ data.code }: ${ data.message }` : data.message;
191
125
  }
192
126
  }
193
127
 
194
- return { stylesConfig, customCSS };
195
- }
196
-
197
- function assertCompositionXmlUsesV4WidgetsOnly( xmlStructure: string ) {
198
- const doc = new DOMParser().parseFromString( xmlStructure, 'application/xml' );
199
- if ( doc.querySelector( 'parsererror' ) ) {
200
- throw new Error( 'Failed to parse XML string: ' + doc );
128
+ if ( error instanceof Error ) {
129
+ return error.message;
201
130
  }
202
- const widgetsCache = getWidgetsCache() ?? {};
203
- for ( const node of doc.querySelectorAll( '*' ) ) {
204
- const type = node.tagName;
205
- const widgetData = widgetsCache[ type ];
206
131
 
207
- if ( ! widgetData ) {
208
- continue;
209
- }
210
- if ( widgetData.elType !== 'widget' ) {
211
- continue;
212
- }
213
- if ( ! isWidgetAvailableForLLM( widgetData ) || ! widgetData.atomic_props_schema ) {
214
- throw new Error( `This tool does not support element type: ${ type }` );
215
- }
216
- }
217
- }
218
-
219
- function onElementAdded( element: V1ElementData ) {
220
- const elType = element.elType ?? '';
221
- const widgetType = element.widgetType ?? '';
222
- const elementName = elType === 'widget' ? widgetType : elType;
223
-
224
- trackCanvasEvent( {
225
- eventName: 'add_element',
226
- executed_by: 'mcp_tool',
227
- element_name: elementName,
228
- element_type: elType,
229
- widget_type: widgetType,
230
- } );
231
-
232
- const event: ElementAddedEvent = {
233
- element,
234
- executedBy: 'mcp_tool',
235
- };
236
-
237
- window.dispatchEvent( new CustomEvent( ELEMENT_ADDED_EVENT, { detail: event } ) );
238
-
239
- if ( element.elements?.length ) {
240
- element.elements?.forEach( ( childElement ) => {
241
- onElementAdded( childElement );
242
- } );
243
- }
132
+ return 'build-composition failed with an unknown error.';
244
133
  }