@elementor/editor-canvas 4.3.0-999 → 4.3.0-beta1

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 (49) hide show
  1. package/dist/index.d.mts +3 -1
  2. package/dist/index.d.ts +3 -1
  3. package/dist/index.js +457 -371
  4. package/dist/index.mjs +438 -348
  5. package/package.json +20 -20
  6. package/src/init-settings-transformers.ts +4 -0
  7. package/src/init-style-transformers.ts +2 -2
  8. package/src/init.tsx +5 -4
  9. package/src/legacy/__tests__/create-templated-element-type.test.ts +50 -0
  10. package/src/legacy/__tests__/list-type.test.ts +89 -0
  11. package/src/legacy/create-nested-templated-element-type.ts +3 -1
  12. package/src/legacy/create-templated-element-type.ts +5 -2
  13. package/src/legacy/list-type.ts +63 -0
  14. package/src/legacy/replacements/inline-editing/__tests__/inline-editing-eligibility.test.ts +7 -7
  15. package/src/legacy/replacements/inline-editing/inline-editing-elements.tsx +22 -9
  16. package/src/legacy/replacements/inline-editing/inline-editing-eligibility.ts +12 -3
  17. package/src/legacy/tabs-model-extensions.ts +2 -5
  18. package/src/legacy/twig-rendering-utils.ts +11 -2
  19. package/src/legacy/types.ts +4 -0
  20. package/src/mcp/canvas-mcp.ts +2 -4
  21. package/src/mcp/mcp-description.ts +18 -30
  22. package/src/mcp/resources/__tests__/available-widgets-resource.test.ts +22 -13
  23. package/src/mcp/resources/__tests__/dynamic-tags-resource.test.ts +22 -30
  24. package/src/mcp/resources/__tests__/widgets-schema-resource.test.ts +11 -6
  25. package/src/mcp/resources/available-widgets-resource.ts +13 -10
  26. package/src/mcp/resources/dynamic-tags-resource.ts +6 -15
  27. package/src/mcp/resources/widgets-schema-resource.ts +8 -5
  28. package/src/mcp/tools/configure-element/__tests__/tool.test.ts +130 -0
  29. package/src/mcp/tools/configure-element/prompt.ts +3 -6
  30. package/src/mcp/tools/configure-element/schema.ts +6 -0
  31. package/src/mcp/tools/configure-element/tool.ts +11 -1
  32. package/src/mcp/tools/get-page-structure/tool.ts +73 -0
  33. package/src/mcp/utils/__tests__/do-update-element-property.test.ts +27 -1
  34. package/src/mcp/utils/do-update-element-property.ts +18 -6
  35. package/src/mcp/utils/get-mcp-error-message.ts +16 -0
  36. package/src/renderers/__tests__/compute-html-tag.test.ts +55 -0
  37. package/src/renderers/__tests__/create-dom-renderer.test.ts +60 -0
  38. package/src/renderers/__tests__/fixtures/html-tag-computer-cases.json +87 -0
  39. package/src/renderers/compute-html-tag.ts +73 -0
  40. package/src/renderers/create-dom-renderer.ts +13 -22
  41. package/src/transformers/settings/escaped-html-transformer.ts +6 -0
  42. package/src/transformers/shared/__tests__/icon-transformer.test.ts +74 -0
  43. package/src/transformers/shared/icon-transformer.ts +122 -0
  44. package/src/transformers/shared/process-svg-content.ts +26 -0
  45. package/src/transformers/shared/svg-src-transformer.ts +1 -26
  46. package/src/utils/__tests__/sanitize-escaped-html.test.ts +105 -0
  47. package/src/utils/sanitize-escaped-html.ts +32 -0
  48. package/src/mcp/tools/build-composition/tool.ts +0 -133
  49. package/src/mcp/tools/get-element-config/tool.ts +0 -114
@@ -3,7 +3,7 @@ import { WIDGET_SCHEMA_URI } from './resources/widgets-schema-resource';
3
3
  const ELEMENT_SCHEMA_URI = WIDGET_SCHEMA_URI.replace( '{widgetType}', 'element-schema' );
4
4
 
5
5
  export const mcpDescription = `Elementor Canvas MCP
6
- This MCP enables creation, configuration, and styling of elements on the Elementor canvas using the build_composition tool.
6
+ This MCP enables configuration and styling of existing V4 elements on the Elementor canvas using the configure-element tool.
7
7
 
8
8
  # Core Concepts
9
9
 
@@ -22,66 +22,54 @@ The \`$$type\` defines how Elementor interprets the value. Providing the correct
22
22
  - **Global Classes**: Reusable style sets that can be applied to elements (\`elementor://global-classes\`)
23
23
  - **Widget Schemas**: Configuration options for each widget type (\`${ WIDGET_SCHEMA_URI }\`)
24
24
 
25
- # Building Compositions with build_composition
25
+ # Configuring Elements with configure-element
26
26
 
27
- The \`build_composition\` tool is the primary way to create elements. It accepts structure (XML), configuration, and styling in a single operation.
27
+ The \`configure-element\` tool updates settings and styles on existing V4 elements. Read the configure-element guide resource before use.
28
28
 
29
29
  ## Complete Workflow
30
30
 
31
31
  ### 1. Parse User Requirements
32
- Understand what needs to be built: structure, content, and styling.
32
+ Understand what needs to change: content, settings, or styling on existing elements.
33
33
 
34
34
  ### 2. Check Global Resources FIRST
35
- Always check existing resources before building:
35
+ Always check existing resources before styling:
36
36
  - List \`elementor://global-variables\` for available variables (colors, sizes, fonts)
37
37
  - List \`elementor://global-classes\` for available style sets
38
38
  - **Always prefer using existing global resources over creating inline styles**
39
39
 
40
40
  ### 3. Retrieve Widget Schemas
41
- For each widget you'll use:
41
+ For each element you will configure:
42
42
  - List \`${ WIDGET_SCHEMA_URI }\` to see available widgets
43
43
  - Retrieve configuration schema from \`${ ELEMENT_SCHEMA_URI }\` for each widget
44
- - Check the \`llm_guidance\` property for container nesting, \`default_styles\`, and \`default_settings\` (omit default_settings from elementConfig unless the user asks to change them)
44
+ - Check the \`llm_guidance\` property for container nesting, \`default_styles\`, and \`default_settings\`
45
45
 
46
- ### 4. Build XML Structure
47
- Create valid XML with configuration-ids:
48
- - Each element must have a unique \`configuration-id\` attribute
49
- - No text nodes, classes, or IDs in XML - structure only
50
- - Example:
51
- \`\`\`xml
52
- <e-container configuration-id="container-1">
53
- <e-heading configuration-id="heading-1" />
54
- <e-text configuration-id="text-1" />
55
- </e-container>
56
- \`\`\`
46
+ ### 4. Get Current Element State
47
+ Use page structure and element configuration resources to find element IDs and current values.
57
48
 
58
- ### 5. Create elementConfig
59
- Map each configuration-id to its widget properties using PropValues:
49
+ ### 5. Create propertiesToChange
50
+ Map property names to PropValues using the widget schema:
60
51
  - Use correct \`$$type\` matching the widget's schema
61
52
  - Use global variables in PropValues where applicable
62
53
  - Example:
63
54
  \`\`\`json
64
55
  {
65
- "heading-1": {
66
- "text": { "$$type": "string", "value": "Welcome" },
67
- "tag": { "$$type": "string", "value": "h1" }
68
- }
56
+ "text": { "$$type": "string", "value": "Welcome" },
57
+ "tag": { "$$type": "string", "value": "h1" }
69
58
  }
70
59
  \`\`\`
71
60
 
72
61
  ### 6. Create style
73
- Map each configuration-id to raw CSS declarations (property → value strings). The server converts them to native styles and stores any unconvertible declarations as the element custom CSS.
62
+ Provide raw CSS declarations (property → value strings). The server converts them to native styles and stores any unconvertible declarations as the element custom CSS.
74
63
  - Example:
75
64
  \`\`\`json
76
65
  {
77
- "heading-1": "color: #1a1a1a; font-size: 2rem;"
78
- }
66
+ "color": "#1a1a1a",
67
+ "font-size": "2rem"
79
68
  }
80
69
  \`\`\`
81
70
 
82
- ### 7. Execute build_composition
83
- Call the tool with your XML structure, elementConfig, and style. The response will contain the created element IDs.
84
- At the response you will also find llm_instructions for you to do afterwards, read and follow them!
71
+ ### 7. Execute configure-element
72
+ Call the tool with elementId, elementType, propertiesToChange, and style as needed.
85
73
 
86
74
  ## Key Points
87
75
 
@@ -28,14 +28,16 @@ describe( 'available-widgets-resource', () => {
28
28
  jest.clearAllMocks();
29
29
  } );
30
30
 
31
- it( 'fetches all widgets via list-widgets without a version filter', async () => {
31
+ it( 'fetches all widgets via list-widget-schemas with summary=true', async () => {
32
32
  // Arrange
33
33
  const post = jest.fn().mockResolvedValue( {
34
34
  data: {
35
- data: [
36
- { type: 'e-heading', version: 'v4' },
37
- { type: 'legacy-icon', version: 'v3' },
38
- ],
35
+ data: {
36
+ widgets: [
37
+ { type: 'e-heading', description: 'A heading widget' },
38
+ { type: 'e-button', description: 'A button widget' },
39
+ ],
40
+ },
39
41
  },
40
42
  } );
41
43
  mockedHttpService.mockReturnValue( { post } as never );
@@ -45,16 +47,21 @@ describe( 'available-widgets-resource', () => {
45
47
  const result = await allHandler();
46
48
 
47
49
  // Assert
48
- expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', { tool: 'list-widgets', input: {} } );
50
+ expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
51
+ tool: 'list-widget-schemas',
52
+ input: { summary: true },
53
+ } );
49
54
  expect( JSON.parse( result.contents[ 0 ].text ) ).toEqual( [
50
- { type: 'e-heading', version: 'v4' },
51
- { type: 'legacy-icon', version: 'v3' },
55
+ { type: 'e-heading', description: 'A heading widget' },
56
+ { type: 'e-button', description: 'A button widget' },
52
57
  ] );
53
58
  } );
54
59
 
55
- it( 'fetches only v4 widgets via list-widgets with a version filter', async () => {
60
+ it( 'fetches v4 widgets via list-widget-schemas with summary=true', async () => {
56
61
  // Arrange
57
- const post = jest.fn().mockResolvedValue( { data: { data: [ { type: 'e-heading', version: 'v4' } ] } } );
62
+ const post = jest.fn().mockResolvedValue( {
63
+ data: { data: { widgets: [ { type: 'e-heading', description: 'A heading widget' } ] } },
64
+ } );
58
65
  mockedHttpService.mockReturnValue( { post } as never );
59
66
  const { v4Handler } = captureHandlers();
60
67
 
@@ -63,10 +70,12 @@ describe( 'available-widgets-resource', () => {
63
70
 
64
71
  // Assert
65
72
  expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
66
- tool: 'list-widgets',
67
- input: { version: 'v4' },
73
+ tool: 'list-widget-schemas',
74
+ input: { summary: true },
68
75
  } );
69
- expect( JSON.parse( result.contents[ 0 ].text ) ).toEqual( [ { type: 'e-heading', version: 'v4' } ] );
76
+ expect( JSON.parse( result.contents[ 0 ].text ) ).toEqual( [
77
+ { type: 'e-heading', description: 'A heading widget' },
78
+ ] );
70
79
  } );
71
80
 
72
81
  it( 'exposes the expected resource URIs', () => {
@@ -19,7 +19,7 @@ const captureHandler = (): ResourceHandler => {
19
19
  const readCatalog = async () => {
20
20
  const handler = captureHandler();
21
21
  const result = await handler( new URL( DYNAMIC_TAGS_URI ) );
22
- return JSON.parse( result.contents[ 0 ].text );
22
+ return result.contents[ 0 ].text;
23
23
  };
24
24
 
25
25
  describe( 'dynamic-tags-resource', () => {
@@ -27,31 +27,9 @@ describe( 'dynamic-tags-resource', () => {
27
27
  jest.clearAllMocks();
28
28
  } );
29
29
 
30
- it( 'returns the flat tag list fetched from the server', async () => {
30
+ it( 'returns the json string fetched from the server', async () => {
31
31
  // Arrange
32
- const post = jest.fn().mockResolvedValue( {
33
- data: {
34
- data: [
35
- {
36
- name: 'post-custom-field',
37
- label: 'Post Custom Field',
38
- categories: [ 'text', 'url' ],
39
- settings: { key: { mocked: true }, before: { mocked: true } },
40
- },
41
- ],
42
- },
43
- } );
44
- mockedHttpService.mockReturnValue( { post } as never );
45
-
46
- // Act
47
- const catalog = await readCatalog();
48
-
49
- // Assert
50
- expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
51
- tool: 'list-dynamic-tags',
52
- input: {},
53
- } );
54
- expect( catalog ).toEqual( [
32
+ const serverJson = JSON.stringify( [
55
33
  {
56
34
  name: 'post-custom-field',
57
35
  label: 'Post Custom Field',
@@ -59,17 +37,31 @@ describe( 'dynamic-tags-resource', () => {
59
37
  settings: { key: { mocked: true }, before: { mocked: true } },
60
38
  },
61
39
  ] );
40
+
41
+ const get = jest.fn().mockResolvedValue( {
42
+ data: { data: serverJson },
43
+ } );
44
+ mockedHttpService.mockReturnValue( { get } as never );
45
+
46
+ // Act
47
+ const text = await readCatalog();
48
+
49
+ // Assert
50
+ expect( get ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
51
+ params: { uri: DYNAMIC_TAGS_URI },
52
+ } );
53
+ expect( text ).toBe( serverJson );
62
54
  } );
63
55
 
64
- it( 'returns an empty list when the server returns no data', async () => {
56
+ it( 'returns an empty json array string when the server returns no data', async () => {
65
57
  // Arrange
66
- const post = jest.fn().mockResolvedValue( { data: {} } );
67
- mockedHttpService.mockReturnValue( { post } as never );
58
+ const get = jest.fn().mockResolvedValue( { data: {} } );
59
+ mockedHttpService.mockReturnValue( { get } as never );
68
60
 
69
61
  // Act
70
- const catalog = await readCatalog();
62
+ const text = await readCatalog();
71
63
 
72
64
  // Assert
73
- expect( catalog ).toEqual( [] );
65
+ expect( text ).toBe( '[]' );
74
66
  } );
75
67
  } );
@@ -40,14 +40,16 @@ describe( 'widgets-schema-resource', () => {
40
40
  jest.clearAllMocks();
41
41
  } );
42
42
 
43
- it( 'lists widget types fetched from the server via the list-widgets tool', async () => {
43
+ it( 'lists widget types fetched from the server via list-widget-schemas with summary=true', async () => {
44
44
  // Arrange
45
45
  const post = jest.fn().mockResolvedValue( {
46
46
  data: {
47
- data: [
48
- { type: 'e-heading', version: 'v4' },
49
- { type: 'e-button', version: 'v4' },
50
- ],
47
+ data: {
48
+ widgets: [
49
+ { type: 'e-heading', description: 'A heading widget' },
50
+ { type: 'e-button', description: 'A button widget' },
51
+ ],
52
+ },
51
53
  },
52
54
  } );
53
55
  mockedHttpService.mockReturnValue( { post } as never );
@@ -57,7 +59,10 @@ describe( 'widgets-schema-resource', () => {
57
59
  const result = await list();
58
60
 
59
61
  // Assert
60
- expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', { tool: 'list-widgets', input: {} } );
62
+ expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
63
+ tool: 'list-widget-schemas',
64
+ input: { summary: true },
65
+ } );
61
66
  expect( result.resources ).toEqual( [
62
67
  { uri: 'elementor://widgets/schema/e-heading', name: 'Widget schema for e-heading' },
63
68
  { uri: 'elementor://widgets/schema/e-button', name: 'Widget schema for e-button' },
@@ -8,21 +8,24 @@ export const AVAILABLE_WIDGETS_URI_V4 = 'elementor://context/available-widgets/v
8
8
 
9
9
  type WidgetSummary = {
10
10
  type: string;
11
- version: 'v3' | 'v4';
12
11
  description?: string;
13
12
  };
14
13
 
15
- const fetchWidgets = async ( version?: WidgetSummary[ 'version' ] ): Promise< WidgetSummary[] > => {
16
- const { data } = await httpService().post< HttpResponse< WidgetSummary[] > >( MCP_PROXY_URL, {
17
- tool: 'list-widgets',
18
- input: version ? { version } : {},
14
+ type WidgetSummaryResponse = {
15
+ widgets: WidgetSummary[];
16
+ };
17
+
18
+ const fetchWidgets = async (): Promise< WidgetSummary[] > => {
19
+ const { data } = await httpService().post< HttpResponse< WidgetSummaryResponse > >( MCP_PROXY_URL, {
20
+ tool: 'list-widget-schemas',
21
+ input: { summary: true },
19
22
  } );
20
23
 
21
- return data.data ?? [];
24
+ return data.data?.widgets ?? [];
22
25
  };
23
26
 
24
- const buildContents = async ( uri: string, version?: WidgetSummary[ 'version' ] ) => {
25
- const widgets = await fetchWidgets( version );
27
+ const buildContents = async ( uri: string ) => {
28
+ const widgets = await fetchWidgets();
26
29
 
27
30
  return {
28
31
  contents: [
@@ -44,14 +47,14 @@ export const initAvailableWidgetsResource = ( reg: MCPRegistryEntry ) => {
44
47
  {
45
48
  description: 'All registered v4 version widgets',
46
49
  },
47
- async () => buildContents( AVAILABLE_WIDGETS_URI_V4, 'v4' )
50
+ async () => buildContents( AVAILABLE_WIDGETS_URI_V4 )
48
51
  );
49
52
 
50
53
  resource(
51
54
  'available-widgets',
52
55
  AVAILABLE_WIDGETS_URI,
53
56
  {
54
- description: 'All registered widget types with v3/v4 version metadata and description.',
57
+ description: 'All registered v4 widget types with description.',
55
58
  },
56
59
  async () => buildContents( AVAILABLE_WIDGETS_URI )
57
60
  );
@@ -5,20 +5,13 @@ export const DYNAMIC_TAGS_URI = 'elementor://dynamic-tags';
5
5
 
6
6
  const MCP_PROXY_URL = 'elementor/v1/mcp-proxy';
7
7
 
8
- type DynamicTagEntry = {
9
- name: string;
10
- label: string;
11
- categories: string[];
12
- settings: Record< string, unknown >;
13
- };
14
-
15
- const fetchDynamicTags = async (): Promise< DynamicTagEntry[] > => {
16
- const { data } = await httpService().post< HttpResponse< DynamicTagEntry[] > >( MCP_PROXY_URL, {
17
- tool: 'list-dynamic-tags',
18
- input: {},
8
+ // NOTE: JSON-encoded array of dynamic tag entries; shape defined by List_Dynamic_Tags_Ability::execute() in modules/mcp/abilities/list-dynamic-tags-ability.php
9
+ const fetchDynamicTags = async (): Promise< string > => {
10
+ const { data } = await httpService().get< HttpResponse< string > >( MCP_PROXY_URL, {
11
+ params: { uri: DYNAMIC_TAGS_URI },
19
12
  } );
20
13
 
21
- return data.data ?? [];
14
+ return data.data ?? '[]';
22
15
  };
23
16
 
24
17
  export const initDynamicTagsResource = ( reg: MCPRegistryEntry ) => {
@@ -35,14 +28,12 @@ export const initDynamicTagsResource = ( reg: MCPRegistryEntry ) => {
35
28
  mimeType: 'application/json',
36
29
  },
37
30
  async ( uri: URL ) => {
38
- const tags = await fetchDynamicTags();
39
-
40
31
  return {
41
32
  contents: [
42
33
  {
43
34
  uri: uri.href,
44
35
  mimeType: 'application/json',
45
- text: JSON.stringify( tags ),
36
+ text: await fetchDynamicTags(),
46
37
  },
47
38
  ],
48
39
  };
@@ -13,17 +13,20 @@ const MCP_PROXY_URL = 'elementor/v1/mcp-proxy';
13
13
 
14
14
  type WidgetSummary = {
15
15
  type: string;
16
- version: 'v3' | 'v4';
17
16
  description?: string;
18
17
  };
19
18
 
19
+ type WidgetSummaryResponse = {
20
+ widgets: WidgetSummary[];
21
+ };
22
+
20
23
  const listWidgetTypes = async (): Promise< string[] > => {
21
- const { data } = await httpService().post< HttpResponse< WidgetSummary[] > >( MCP_PROXY_URL, {
22
- tool: 'list-widgets',
23
- input: {},
24
+ const { data } = await httpService().post< HttpResponse< WidgetSummaryResponse > >( MCP_PROXY_URL, {
25
+ tool: 'list-widget-schemas',
26
+ input: { summary: true },
24
27
  } );
25
28
 
26
- return ( data.data ?? [] ).map( ( widget ) => widget.type );
29
+ return ( data.data?.widgets ?? [] ).map( ( widget ) => widget.type );
27
30
  };
28
31
 
29
32
  const fetchWidgetSchema = async ( widgetType: string ): Promise< Record< string, unknown > > => {
@@ -0,0 +1,130 @@
1
+ import { getContainer, getWidgetsCache } from '@elementor/editor-elements';
2
+
3
+ import { doUpdateElementProperty, UnsupportedPropertyError } from '../../../utils/do-update-element-property';
4
+ import { initConfigureElementTool } from '../tool';
5
+
6
+ jest.mock( '@elementor/editor-elements', () => ( {
7
+ getContainer: jest.fn(),
8
+ getWidgetsCache: jest.fn(),
9
+ } ) );
10
+
11
+ jest.mock( '@elementor/editor-mcp', () => ( {
12
+ dispatchMcpStylesAppliedEvent: jest.fn(),
13
+ } ) );
14
+
15
+ jest.mock( '@elementor/editor-props', () => ( {
16
+ Schema: { isPropKeyConfigurable: () => true },
17
+ } ) );
18
+
19
+ jest.mock( '../prompt', () => ( {
20
+ CONFIGURE_ELEMENT_GUIDE_URI: 'configure-element-guide-uri',
21
+ generatePrompt: () => '',
22
+ } ) );
23
+
24
+ jest.mock( '../../../resources/widgets-schema-resource', () => ( {
25
+ WIDGET_SCHEMA_URI: 'elementor://widgets/schema/{widgetType}',
26
+ } ) );
27
+
28
+ jest.mock( '../../../resources/dynamic-tags-resource', () => ( {
29
+ DYNAMIC_TAGS_URI: 'elementor://dynamic-tags',
30
+ } ) );
31
+
32
+ jest.mock( '../../../utils/resolve-canonical-prop-name', () => ( {
33
+ resolveCanonicalPropKeys: ( _elementType: string, props: Record< string, unknown > ) => props,
34
+ } ) );
35
+
36
+ jest.mock( '../../../utils/convert-css-to-atomic', () => ( {
37
+ convertCssToAtomic: jest.fn().mockResolvedValue( { props: {}, customCss: '' } ),
38
+ } ) );
39
+
40
+ jest.mock( '../../../utils/do-update-element-property', () => ( {
41
+ ...jest.requireActual( '../../../utils/do-update-element-property' ),
42
+ doUpdateElementProperty: jest.fn(),
43
+ } ) );
44
+
45
+ const ELEMENT_ID = 'el-1';
46
+ const ELEMENT_TYPE = 'e-youtube';
47
+
48
+ type Handler = ( params: {
49
+ elementId: string;
50
+ elementType: string;
51
+ propertiesToChange: Record< string, unknown >;
52
+ style: Record< string, string | null >;
53
+ } ) => Promise< { success: boolean; warnings?: string } >;
54
+
55
+ const getHandler = (): Handler => {
56
+ const addTool = jest.fn();
57
+ initConfigureElementTool( { addTool, resource: jest.fn() } as never );
58
+ return addTool.mock.calls[ 0 ][ 0 ].handler;
59
+ };
60
+
61
+ describe( 'configure-element handler unsupported props', () => {
62
+ beforeEach( () => {
63
+ jest.mocked( getWidgetsCache ).mockReturnValue( {
64
+ [ ELEMENT_TYPE ]: { atomic_props_schema: { title: { key: 'title' } } },
65
+ } as never );
66
+ jest.mocked( getContainer ).mockReturnValue( {
67
+ settings: { get: () => ELEMENT_TYPE },
68
+ } as never );
69
+ jest.mocked( doUpdateElementProperty ).mockReset();
70
+ } );
71
+
72
+ it( 'skips an unsupported prop, reports it in warnings, and still applies supported props', async () => {
73
+ // Arrange
74
+ jest.mocked( doUpdateElementProperty ).mockImplementation( ( { propertyName } ) => {
75
+ if ( 'link' === propertyName ) {
76
+ throw new UnsupportedPropertyError( ELEMENT_TYPE, 'link', [ 'title' ] );
77
+ }
78
+ } );
79
+ const handler = getHandler();
80
+
81
+ // Act
82
+ const result = await handler( {
83
+ elementId: ELEMENT_ID,
84
+ elementType: ELEMENT_TYPE,
85
+ propertiesToChange: { title: { $$type: 'string', value: 'Hi' }, link: { $$type: 'link', value: {} } },
86
+ style: {},
87
+ } );
88
+
89
+ // Assert
90
+ expect( result.success ).toBe( true );
91
+ expect( result.warnings ).toContain( 'link' );
92
+ expect( doUpdateElementProperty ).toHaveBeenCalledTimes( 2 );
93
+ } );
94
+
95
+ it( 'returns no warnings when every prop is supported', async () => {
96
+ // Arrange
97
+ jest.mocked( doUpdateElementProperty ).mockImplementation( () => undefined );
98
+ const handler = getHandler();
99
+
100
+ // Act
101
+ const result = await handler( {
102
+ elementId: ELEMENT_ID,
103
+ elementType: ELEMENT_TYPE,
104
+ propertiesToChange: { title: { $$type: 'string', value: 'Hi' } },
105
+ style: {},
106
+ } );
107
+
108
+ // Assert
109
+ expect( result.success ).toBe( true );
110
+ expect( result.warnings ).toBeUndefined();
111
+ } );
112
+
113
+ it( 'rethrows non-UnsupportedPropertyError failures', async () => {
114
+ // Arrange
115
+ jest.mocked( doUpdateElementProperty ).mockImplementation( () => {
116
+ throw new Error( 'Invalid PropValue' );
117
+ } );
118
+ const handler = getHandler();
119
+
120
+ // Act & Assert
121
+ await expect(
122
+ handler( {
123
+ elementId: ELEMENT_ID,
124
+ elementType: ELEMENT_TYPE,
125
+ propertiesToChange: { title: { $$type: 'string', value: 'Hi' } },
126
+ style: {},
127
+ } )
128
+ ).rejects.toThrow( /Invalid PropValue/ );
129
+ } );
130
+ } );
@@ -50,8 +50,6 @@ For all non-primitive entries in \`propertiesToChange\`, provide the schema \`ke
50
50
 
51
51
  Use the EXACT PropType schema given, and ALWAYS include the \`key\` from the schema for every property you are changing in \`propertiesToChange\`.
52
52
 
53
- Check \`llm_guidance.default_settings\` in the widget schema — include a key in \`propertiesToChange\` only when the user explicitly asks to change it.
54
-
55
53
  # Dynamic tags
56
54
  A value can be made dynamic wherever its schema exposes a variant with "$$type": "dynamic". This may be the property root OR a NESTED field: for example an image is made dynamic on its "src" (the root stays "image"), NOT on the whole "image" value.
57
55
  Put the dynamic object EXACTLY at the node whose schema offers the "dynamic" variant, in place of the static variant. The variant's "name" enumerates the tags allowed at that node.
@@ -82,7 +80,7 @@ Do NOT send "group" (it is resolved automatically). Use { "settings": {} } only
82
80
 
83
81
  configureElementToolPrompt.parameter(
84
82
  'style',
85
- 'A flat map of raw CSS declarations (property → value), e.g. { "line-height": "1.25rem", "color": "var(--primary-text, #000)" }. Set a value to null to reset that property to its default. OPTIONAL.'
83
+ 'A flat map of raw CSS declarations (property → value), e.g. { "line-height": "1.25rem", "color": "var(--primary-text, #000)" }. font-family must be a single Google Font name or a var(--label) — no fallback stacks. Set a value to null to reset that property to its default. OPTIONAL.'
86
84
  );
87
85
 
88
86
  configureElementToolPrompt.example( `
@@ -117,11 +115,10 @@ Do NOT send "group" (it is resolved automatically). Use { "settings": {} } only
117
115
  V4 only: If MCP fails, give manual steps using V4 UI.
118
116
 
119
117
  V4 Editor structure:
120
- Panel tabs: General (→ Settings section: ID, Tag, Link), Style, Interactions.
118
+ Panel tabs: General (→ Settings section: ID, Tag, and Link where the widget supports it), Style, Interactions.
121
119
  NO Advanced tab. Never mention Advanced tab.
120
+ Note: \`link\` is valid only when the element's PropType schema (which you must already have) includes a \`link\` property. Sending \`link\` to a widget whose schema lacks it is skipped and reported in the response \`warnings\` (other changes still apply) and the link is lost.
122
121
  ` );
123
122
 
124
123
  return configureElementToolPrompt.prompt();
125
124
  };
126
-
127
- export const CONFIGURE_ELEMENT_GUIDE_TEXT = generatePrompt();
@@ -36,4 +36,10 @@ export const outputSchema = {
36
36
  .describe(
37
37
  'Whether the configuration change was successful, only if propertyName and propertyValue are provided'
38
38
  ),
39
+ warnings: z
40
+ .string()
41
+ .describe(
42
+ 'Non-fatal notices. Present when some props were skipped because they are not in the element schema (e.g. a "link" on a widget with no link prop). Other changes were still applied.'
43
+ )
44
+ .optional(),
39
45
  };
@@ -6,7 +6,7 @@ import { type PropValue, Schema } from '@elementor/editor-props';
6
6
  import { DYNAMIC_TAGS_URI } from '../../resources/dynamic-tags-resource';
7
7
  import { WIDGET_SCHEMA_URI } from '../../resources/widgets-schema-resource';
8
8
  import { convertCssToAtomic } from '../../utils/convert-css-to-atomic';
9
- import { doUpdateElementProperty } from '../../utils/do-update-element-property';
9
+ import { doUpdateElementProperty, UnsupportedPropertyError } from '../../utils/do-update-element-property';
10
10
  import { resolveCanonicalPropKeys } from '../../utils/resolve-canonical-prop-name';
11
11
  import { CONFIGURE_ELEMENT_GUIDE_URI, generatePrompt } from './prompt';
12
12
  import { inputSchema as schema, outputSchema } from './schema';
@@ -61,6 +61,7 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
61
61
  }
62
62
  const propertiesToUpdate = resolveCanonicalPropKeys( elementType, propertiesToChange );
63
63
  const toUpdate = Object.entries( propertiesToUpdate );
64
+ const skippedProps: string[] = [];
64
65
  for ( const [ propertyName, propertyValue ] of toUpdate as [ string, PropValue ][] ) {
65
66
  if ( ! Schema.isPropKeyConfigurable( propertyName ) ) {
66
67
  throw new Error( `Not allowed to update ${ propertyName }` );
@@ -73,6 +74,10 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
73
74
  propertyValue,
74
75
  } );
75
76
  } catch ( error ) {
77
+ if ( error instanceof UnsupportedPropertyError ) {
78
+ skippedProps.push( error.propertyName );
79
+ continue;
80
+ }
76
81
  const errorMessage = createUpdateErrorMessage( {
77
82
  propertyName,
78
83
  elementId,
@@ -86,6 +91,11 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
86
91
  await applyStyleFromCss( { elementId, elementType, style } );
87
92
  return {
88
93
  success: true,
94
+ warnings: skippedProps.length
95
+ ? `Skipped unsupported props (not in the "${ elementType }" schema; other changes were applied): ${ skippedProps.join(
96
+ ', '
97
+ ) }.`
98
+ : undefined,
89
99
  };
90
100
  },
91
101
  } );