@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.
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +448 -1318
- package/dist/index.mjs +313 -1196
- package/package.json +22 -22
- package/src/index.ts +1 -1
- package/src/legacy/__tests__/init-legacy-views.test.ts +1 -1
- package/src/mcp/canvas-mcp.ts +4 -2
- package/src/mcp/resources/__tests__/available-widgets-resource.test.ts +76 -0
- package/src/mcp/resources/__tests__/dynamic-tags-resource.test.ts +27 -37
- package/src/mcp/resources/__tests__/widgets-schema-resource.test.ts +79 -76
- package/src/mcp/resources/available-widgets-resource.ts +31 -40
- package/src/mcp/resources/best-practices-resource.ts +34 -0
- package/src/mcp/resources/dynamic-tags-resource.ts +28 -29
- package/src/mcp/resources/widgets-schema-resource.ts +34 -177
- package/src/mcp/tools/build-composition/tool.ts +107 -218
- package/src/sync/element-added-event.ts +8 -0
- package/src/utils/__tests__/grid-outline-utils.test.ts +39 -0
- package/src/utils/grid-outline-utils.ts +13 -2
- package/src/composition-builder/__tests__/composition-builder.test.ts +0 -352
- package/src/composition-builder/composition-builder.ts +0 -350
- package/src/composition-builder/utils/__tests__/required-children-enforcer.test.ts +0 -79
- package/src/composition-builder/utils/__tests__/required-default-child-tags.test.ts +0 -35
- package/src/composition-builder/utils/required-children-enforcer.ts +0 -56
- package/src/composition-builder/utils/required-default-child-tags.ts +0 -22
- package/src/mcp/resources/best-practices.ts +0 -159
- package/src/mcp/resources/build-llm-guidance.ts +0 -110
- package/src/mcp/tools/build-composition/__tests__/xml-leaf-wrapper.test.ts +0 -117
- package/src/mcp/tools/build-composition/prompt.ts +0 -167
- package/src/mcp/tools/build-composition/schema.ts +0 -42
- package/src/mcp/tools/build-composition/xml-leaf-wrapper.ts +0 -68
- package/src/mcp/utils/__tests__/get-composition-target-container.test.ts +0 -59
- 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
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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://
|
|
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
|
-
|
|
66
|
-
const { resource } = reg;
|
|
12
|
+
const MCP_PROXY_URL = 'elementor/v1/mcp-proxy';
|
|
67
13
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
157
|
-
|
|
62
|
+
if ( ! widgetType ) {
|
|
63
|
+
throw new Error( 'No widget type provided.' );
|
|
64
|
+
}
|
|
158
65
|
|
|
159
|
-
const
|
|
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 {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
36
|
-
'build-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 (
|
|
44
|
-
|
|
45
|
-
} )
|
|
46
|
-
);
|
|
72
|
+
handler: async ( { xmlStructure, elementConfig, style, parentId, dryRun } ) => {
|
|
73
|
+
const document = getCurrentDocument();
|
|
47
74
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
97
|
-
|
|
92
|
+
if ( ! dryRun ) {
|
|
93
|
+
await reloadCurrentDocument();
|
|
98
94
|
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|