@elementor/editor-canvas 4.4.0-1124 → 4.4.0-1125
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.js +219 -57
- package/dist/index.mjs +174 -12
- package/package.json +20 -20
- package/src/mcp/canvas-mcp.ts +5 -0
- package/src/mcp/icon-library-experiment.ts +5 -0
- package/src/mcp/tools/configure-element/prompt.ts +47 -1
- package/src/mcp/tools/find-icons/__tests__/tool.test.ts +143 -0
- package/src/mcp/tools/find-icons/prompt.ts +43 -0
- package/src/mcp/tools/find-icons/tool.ts +153 -0
package/dist/index.js
CHANGED
|
@@ -195,7 +195,7 @@ var convertCssToAtomic = async (style) => {
|
|
|
195
195
|
|
|
196
196
|
// src/init.tsx
|
|
197
197
|
var import_editor = require("@elementor/editor");
|
|
198
|
-
var
|
|
198
|
+
var import_editor_mcp5 = require("@elementor/editor-mcp");
|
|
199
199
|
|
|
200
200
|
// src/components/classes-rename.tsx
|
|
201
201
|
var import_react = require("react");
|
|
@@ -2572,11 +2572,11 @@ function getVal2(val) {
|
|
|
2572
2572
|
var transformOriginTransformer = createTransformer((value) => {
|
|
2573
2573
|
const x = getVal2(value.x);
|
|
2574
2574
|
const y = getVal2(value.y);
|
|
2575
|
-
const
|
|
2576
|
-
if (x === DEFAULT_XY && y === DEFAULT_XY &&
|
|
2575
|
+
const z4 = getVal2(value.z);
|
|
2576
|
+
if (x === DEFAULT_XY && y === DEFAULT_XY && z4 === DEFAULT_Z) {
|
|
2577
2577
|
return null;
|
|
2578
2578
|
}
|
|
2579
|
-
return `${x} ${y} ${
|
|
2579
|
+
return `${x} ${y} ${z4}`;
|
|
2580
2580
|
});
|
|
2581
2581
|
|
|
2582
2582
|
// src/transformers/styles/transform-rotate-transformer.ts
|
|
@@ -4185,6 +4185,11 @@ function initTabsModelExtensions() {
|
|
|
4185
4185
|
// src/mcp/canvas-mcp.ts
|
|
4186
4186
|
var import_editor_props8 = require("@elementor/editor-props");
|
|
4187
4187
|
|
|
4188
|
+
// src/mcp/icon-library-experiment.ts
|
|
4189
|
+
var import_editor_v1_adapters15 = require("@elementor/editor-v1-adapters");
|
|
4190
|
+
var ICON_LIBRARY_EXPERIMENT = "e_svg_library";
|
|
4191
|
+
var isIconLibraryActive = () => (0, import_editor_v1_adapters15.isExperimentActive)(ICON_LIBRARY_EXPERIMENT);
|
|
4192
|
+
|
|
4188
4193
|
// src/mcp/resources/available-widgets-resource.ts
|
|
4189
4194
|
var import_http_client3 = require("@elementor/http-client");
|
|
4190
4195
|
var MCP_PROXY_URL2 = "elementor/v1/mcp-proxy";
|
|
@@ -4261,7 +4266,7 @@ var initBestPracticesResource = (reg) => {
|
|
|
4261
4266
|
|
|
4262
4267
|
// src/mcp/resources/document-structure-resource.ts
|
|
4263
4268
|
var import_editor_elements9 = require("@elementor/editor-elements");
|
|
4264
|
-
var
|
|
4269
|
+
var import_editor_v1_adapters16 = require("@elementor/editor-v1-adapters");
|
|
4265
4270
|
var DOCUMENT_STRUCTURE_URI = "elementor://document/structure";
|
|
4266
4271
|
var initDocumentStructureResource = (reg) => {
|
|
4267
4272
|
const { resource, sendResourceUpdated } = reg;
|
|
@@ -4274,15 +4279,15 @@ var initDocumentStructureResource = (reg) => {
|
|
|
4274
4279
|
sendResourceUpdated({ uri: DOCUMENT_STRUCTURE_URI });
|
|
4275
4280
|
}
|
|
4276
4281
|
};
|
|
4277
|
-
(0,
|
|
4282
|
+
(0, import_editor_v1_adapters16.__privateListenTo)(
|
|
4278
4283
|
[
|
|
4279
|
-
(0,
|
|
4280
|
-
(0,
|
|
4281
|
-
(0,
|
|
4282
|
-
(0,
|
|
4283
|
-
(0,
|
|
4284
|
-
(0,
|
|
4285
|
-
(0,
|
|
4284
|
+
(0, import_editor_v1_adapters16.commandEndEvent)("document/elements/create"),
|
|
4285
|
+
(0, import_editor_v1_adapters16.commandEndEvent)("document/elements/delete"),
|
|
4286
|
+
(0, import_editor_v1_adapters16.commandEndEvent)("document/elements/move"),
|
|
4287
|
+
(0, import_editor_v1_adapters16.commandEndEvent)("document/elements/copy"),
|
|
4288
|
+
(0, import_editor_v1_adapters16.commandEndEvent)("document/elements/paste"),
|
|
4289
|
+
(0, import_editor_v1_adapters16.commandEndEvent)("editor/documents/attach-preview"),
|
|
4290
|
+
(0, import_editor_v1_adapters16.commandEndEvent)("editor/documents/switch")
|
|
4286
4291
|
],
|
|
4287
4292
|
updateDocumentStructure
|
|
4288
4293
|
);
|
|
@@ -4385,7 +4390,7 @@ var initDynamicTagsResource = (reg) => {
|
|
|
4385
4390
|
};
|
|
4386
4391
|
|
|
4387
4392
|
// src/mcp/resources/editor-state-resource.ts
|
|
4388
|
-
var
|
|
4393
|
+
var import_editor_v1_adapters17 = require("@elementor/editor-v1-adapters");
|
|
4389
4394
|
var CURRENTLY_VIEWED_SCREEN = "The user is currently viewing the Elementor editor";
|
|
4390
4395
|
var PAGE_CONTENT_CHARACTER_LIMIT = 500;
|
|
4391
4396
|
var PREVIEW_TEXT_NODE_MIN_LENGTH = 2;
|
|
@@ -4406,8 +4411,8 @@ var initEditorStateResource = (reg) => {
|
|
|
4406
4411
|
lastSerializedState = serialized;
|
|
4407
4412
|
sendResourceUpdated({ uri: EDITOR_STATE_URI });
|
|
4408
4413
|
};
|
|
4409
|
-
(0,
|
|
4410
|
-
[(0,
|
|
4414
|
+
(0, import_editor_v1_adapters17.__privateListenTo)(
|
|
4415
|
+
[(0, import_editor_v1_adapters17.commandEndEvent)("editor/documents/switch"), (0, import_editor_v1_adapters17.commandEndEvent)("editor/documents/attach-preview")],
|
|
4411
4416
|
notifyIfChanged
|
|
4412
4417
|
);
|
|
4413
4418
|
lastSerializedState = JSON.stringify(buildState());
|
|
@@ -4481,7 +4486,7 @@ function getPageTitle() {
|
|
|
4481
4486
|
}
|
|
4482
4487
|
|
|
4483
4488
|
// src/mcp/resources/general-context-resource.ts
|
|
4484
|
-
var
|
|
4489
|
+
var import_editor_v1_adapters18 = require("@elementor/editor-v1-adapters");
|
|
4485
4490
|
var GENERAL_CONTEXT_URI = "elementor://context/general";
|
|
4486
4491
|
var initGeneralContextResource = (reg) => {
|
|
4487
4492
|
const { resource, sendResourceUpdated } = reg;
|
|
@@ -4542,11 +4547,11 @@ var initGeneralContextResource = (reg) => {
|
|
|
4542
4547
|
};
|
|
4543
4548
|
}
|
|
4544
4549
|
);
|
|
4545
|
-
(0,
|
|
4550
|
+
(0, import_editor_v1_adapters18.__privateListenTo)(
|
|
4546
4551
|
[
|
|
4547
|
-
(0,
|
|
4548
|
-
(0,
|
|
4549
|
-
(0,
|
|
4552
|
+
(0, import_editor_v1_adapters18.commandEndEvent)("editor/documents/switch"),
|
|
4553
|
+
(0, import_editor_v1_adapters18.commandEndEvent)("editor/documents/attach-preview"),
|
|
4554
|
+
(0, import_editor_v1_adapters18.commandEndEvent)("document/elements/settings")
|
|
4550
4555
|
],
|
|
4551
4556
|
pushUpdateIfChanged
|
|
4552
4557
|
);
|
|
@@ -4555,7 +4560,7 @@ var initGeneralContextResource = (reg) => {
|
|
|
4555
4560
|
|
|
4556
4561
|
// src/mcp/resources/selected-element-resource.ts
|
|
4557
4562
|
var import_editor_elements10 = require("@elementor/editor-elements");
|
|
4558
|
-
var
|
|
4563
|
+
var import_editor_v1_adapters19 = require("@elementor/editor-v1-adapters");
|
|
4559
4564
|
var SELECTED_ELEMENT_URI = "elementor://context/selected-element";
|
|
4560
4565
|
var initSelectedElementResource = (reg) => {
|
|
4561
4566
|
const { resource, sendResourceUpdated } = reg;
|
|
@@ -4586,11 +4591,11 @@ var initSelectedElementResource = (reg) => {
|
|
|
4586
4591
|
}
|
|
4587
4592
|
publishIfChanged(readSelectionFromEditor());
|
|
4588
4593
|
};
|
|
4589
|
-
(0,
|
|
4594
|
+
(0, import_editor_v1_adapters19.__privateListenTo)(
|
|
4590
4595
|
[
|
|
4591
|
-
(0,
|
|
4592
|
-
(0,
|
|
4593
|
-
(0,
|
|
4596
|
+
(0, import_editor_v1_adapters19.commandEndEvent)("document/elements/select"),
|
|
4597
|
+
(0, import_editor_v1_adapters19.commandEndEvent)("document/elements/deselect-all"),
|
|
4598
|
+
(0, import_editor_v1_adapters19.commandEndEvent)("document/elements/settings")
|
|
4594
4599
|
],
|
|
4595
4600
|
onCommand
|
|
4596
4601
|
);
|
|
@@ -4708,7 +4713,7 @@ var import_editor_props7 = require("@elementor/editor-props");
|
|
|
4708
4713
|
var import_editor_elements12 = require("@elementor/editor-elements");
|
|
4709
4714
|
var import_editor_props6 = require("@elementor/editor-props");
|
|
4710
4715
|
var import_editor_styles4 = require("@elementor/editor-styles");
|
|
4711
|
-
var
|
|
4716
|
+
var import_editor_v1_adapters21 = require("@elementor/editor-v1-adapters");
|
|
4712
4717
|
|
|
4713
4718
|
// src/mcp/utils/merge-custom-css.ts
|
|
4714
4719
|
var CUSTOM_CSS_SEPARATOR = "\n";
|
|
@@ -4777,11 +4782,11 @@ function resolveCanonicalPropKeys(elementType, props) {
|
|
|
4777
4782
|
}
|
|
4778
4783
|
|
|
4779
4784
|
// src/mcp/utils/resolve-dynamic-tag.ts
|
|
4780
|
-
var
|
|
4785
|
+
var import_editor_v1_adapters20 = require("@elementor/editor-v1-adapters");
|
|
4781
4786
|
var DYNAMIC_PROP_TYPE_KEY = "dynamic";
|
|
4782
4787
|
var OMITTED_DYNAMIC_SETTING_KEYS = ["fallback"];
|
|
4783
4788
|
var getAtomicDynamicTags = () => {
|
|
4784
|
-
const config = (0,
|
|
4789
|
+
const config = (0, import_editor_v1_adapters20.getElementorConfig)();
|
|
4785
4790
|
return config.atomicDynamicTags?.tags ?? {};
|
|
4786
4791
|
};
|
|
4787
4792
|
var getDynamicTagNamesByCategories = (categories) => {
|
|
@@ -4982,13 +4987,51 @@ Expected Schema: ${jsonSchema}`
|
|
|
4982
4987
|
},
|
|
4983
4988
|
withHistory: false
|
|
4984
4989
|
});
|
|
4985
|
-
(0,
|
|
4990
|
+
(0, import_editor_v1_adapters21.__privateRunCommandSync)("document/save/set-is-modified", { status: true }, { internal: true });
|
|
4986
4991
|
};
|
|
4987
4992
|
|
|
4988
4993
|
// src/mcp/tools/configure-element/prompt.ts
|
|
4989
4994
|
var import_editor_mcp2 = require("@elementor/editor-mcp");
|
|
4995
|
+
var ICONS_SECTION = `# Icons (the \`svg\` property on e-svg)
|
|
4996
|
+
The \`svg\` property accepts two variants. Prefer a library icon for iconography (carts, arrows, social marks, check marks) \u2014 it needs no upload and inherits \`color\` via \`currentColor\`. Use an uploaded file only for custom artwork such as a brand logo.
|
|
4997
|
+
|
|
4998
|
+
Resolve the icon with the \`find-icons\` tool FIRST, then copy its \`value\` and \`library\` verbatim. Never assemble those strings yourself: Font Awesome renamed many icons between major versions (\`shopping-cart\` became \`cart-shopping\`), so a plausible guess renders nothing. \`find-icons\` matches on names, aliases, human search terms, and categories, so describe the intent \u2014 "add to cart" finds \`cart-shopping\`. It takes a batch of \`queries\`, so resolve every icon you need in one call.
|
|
4999
|
+
|
|
5000
|
+
Library icon (note the nested PropValues \u2014 \`value\` and \`library\` are each a \`string\` PropValue):
|
|
5001
|
+
{ "$$type": "icon", "value": { "value": { "$$type": "string", "value": "fa-solid fa-cart-shopping" }, "library": { "$$type": "string", "value": "fa-solid" } } }
|
|
5002
|
+
|
|
5003
|
+
Uploaded SVG file:
|
|
5004
|
+
{ "$$type": "svg-src", "value": { "id": { "$$type": "image-attachment-id", "value": 123 } } }
|
|
5005
|
+
|
|
5006
|
+
Size and colour an e-svg through \`style\` (\`width\`, \`height\`, \`color\`), never through \`propertiesToChange\`.
|
|
5007
|
+
`;
|
|
5008
|
+
var ICON_EXAMPLE = `
|
|
5009
|
+
Setting a Font Awesome icon on an e-svg, after resolving it with \`find-icons\`:
|
|
5010
|
+
|
|
5011
|
+
\`\`\`json
|
|
5012
|
+
{
|
|
5013
|
+
propertiesToChange: {
|
|
5014
|
+
svg: {
|
|
5015
|
+
$$type: 'icon',
|
|
5016
|
+
value: {
|
|
5017
|
+
value: { $$type: 'string', value: 'fa-solid fa-cart-shopping' },
|
|
5018
|
+
library: { $$type: 'string', value: 'fa-solid' }
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
},
|
|
5022
|
+
style: {
|
|
5023
|
+
'width': '1.25rem',
|
|
5024
|
+
'height': '1.25rem',
|
|
5025
|
+
'color': '#1a1a1a'
|
|
5026
|
+
},
|
|
5027
|
+
elementId: 'element-id',
|
|
5028
|
+
elementType: 'e-svg'
|
|
5029
|
+
};
|
|
5030
|
+
\`\`\`
|
|
5031
|
+
`;
|
|
4990
5032
|
var CONFIGURE_ELEMENT_GUIDE_URI = "elementor://canvas/tools/configure-element-guide";
|
|
4991
5033
|
var generatePrompt = () => {
|
|
5034
|
+
const iconLibraryActive = isIconLibraryActive();
|
|
4992
5035
|
const configureElementToolPrompt = (0, import_editor_mcp2.toolPrompts)("configure-element");
|
|
4993
5036
|
configureElementToolPrompt.description(`
|
|
4994
5037
|
Configure an existing element on the page.
|
|
@@ -5046,7 +5089,8 @@ Put the dynamic object EXACTLY at the node whose schema offers the "dynamic" var
|
|
|
5046
5089
|
}
|
|
5047
5090
|
Image example: { "$$type": "image", "value": { "src": { "$$type": "dynamic", "value": { "name": "<image tag>", "settings": { ... } } } } }
|
|
5048
5091
|
Do NOT send "group" (it is resolved automatically). Use { "settings": {} } only when the tag has no settings.
|
|
5049
|
-
|
|
5092
|
+
|
|
5093
|
+
${iconLibraryActive ? ICONS_SECTION : ""}`);
|
|
5050
5094
|
configureElementToolPrompt.parameter("elementId", "The ID of the element to configure. MANDATORY.");
|
|
5051
5095
|
configureElementToolPrompt.parameter(
|
|
5052
5096
|
"elementType",
|
|
@@ -5083,6 +5127,9 @@ Do NOT send "group" (it is resolved automatically). Use { "settings": {} } only
|
|
|
5083
5127
|
};
|
|
5084
5128
|
\`\`\`
|
|
5085
5129
|
`);
|
|
5130
|
+
if (iconLibraryActive) {
|
|
5131
|
+
configureElementToolPrompt.example(ICON_EXAMPLE);
|
|
5132
|
+
}
|
|
5086
5133
|
configureElementToolPrompt.instruction(
|
|
5087
5134
|
"The $$type property is MANDATORY for every value in propertiesToChange; it is not used in the style parameter (raw CSS only)."
|
|
5088
5135
|
);
|
|
@@ -5256,8 +5303,7 @@ Provide styling as raw CSS via the "style" parameter (a flat map of CSS property
|
|
|
5256
5303
|
}`;
|
|
5257
5304
|
}
|
|
5258
5305
|
|
|
5259
|
-
// src/mcp/tools/
|
|
5260
|
-
var import_editor_documents2 = require("@elementor/editor-documents");
|
|
5306
|
+
// src/mcp/tools/find-icons/tool.ts
|
|
5261
5307
|
var import_http_client7 = require("@elementor/http-client");
|
|
5262
5308
|
var import_schema3 = require("@elementor/schema");
|
|
5263
5309
|
|
|
@@ -5276,24 +5322,137 @@ function getMcpErrorMessage(error, toolName) {
|
|
|
5276
5322
|
return `${toolName} failed with an unknown error.`;
|
|
5277
5323
|
}
|
|
5278
5324
|
|
|
5279
|
-
// src/mcp/tools/
|
|
5325
|
+
// src/mcp/tools/find-icons/prompt.ts
|
|
5326
|
+
var import_editor_mcp4 = require("@elementor/editor-mcp");
|
|
5327
|
+
var findIconsToolPrompt = (0, import_editor_mcp4.toolPrompts)("find-icons");
|
|
5328
|
+
findIconsToolPrompt.description(
|
|
5329
|
+
"Finds icons for the `svg` property of an `e-svg` element, across Font Awesome and any icon packs uploaded to this site. Returns the exact `value` and `library` pair to write - never assemble those strings yourself, and never guess an icon name. Call this before placing or configuring an `e-svg`. Nothing has to be uploaded to the Media Library for an icon to work."
|
|
5330
|
+
);
|
|
5331
|
+
findIconsToolPrompt.instruction(
|
|
5332
|
+
'Call it when the user names an icon ("use the cart-shopping icon") and take the first exact `name` match, and also when the user describes a page and you decide icons belong in it ("create a product page") - in that case pass one query per icon you intend to place, in a single call.'
|
|
5333
|
+
);
|
|
5334
|
+
findIconsToolPrompt.instruction(
|
|
5335
|
+
'Write queries the way a person would describe the icon ("shopping cart", "credit card", "phone"). Icons are matched on their name, aliases, Font Awesome search terms and category, so plain language works better than guessed icon names.'
|
|
5336
|
+
);
|
|
5337
|
+
findIconsToolPrompt.instruction(
|
|
5338
|
+
'Results are ordered best first; `matched_on: "name"` is a stronger signal than `matched_on: "term"`. Check each result\'s `license` and only use `free` icons; skip any `pro` entry. Prefer `fa-solid` for UI iconography and keep one style across a page; brand marks only exist in `fa-brands`. An empty `matches` array means no match was found: broaden the phrase instead of inventing a value.'
|
|
5339
|
+
);
|
|
5340
|
+
findIconsToolPrompt.instruction(
|
|
5341
|
+
'Pass the chosen pair to `configure-element` as the `svg` property: { "$$type": "icon", "value": { "value": { "$$type": "string", "value": "fa-solid fa-cart-shopping" }, "library": { "$$type": "string", "value": "fa-solid" } } }. An `e-svg` has no colour or size of its own, so set `width`, `height` and `color` in the same `configure-element` call.'
|
|
5342
|
+
);
|
|
5343
|
+
findIconsToolPrompt.example(`Icon named by the user:
|
|
5344
|
+
{ "queries": ["cart-shopping"] }`);
|
|
5345
|
+
findIconsToolPrompt.example(`Every icon a product page needs, in one call:
|
|
5346
|
+
{ "queries": ["add to cart", "free shipping", "secure payment", "instagram"] }`);
|
|
5347
|
+
findIconsToolPrompt.example(`Browse a category, one style only:
|
|
5348
|
+
{ "category": "shopping", "library": "fa-solid", "perPage": 25 }`);
|
|
5349
|
+
|
|
5350
|
+
// src/mcp/tools/find-icons/tool.ts
|
|
5280
5351
|
var MCP_PROXY_URL5 = "elementor/v1/mcp-proxy";
|
|
5352
|
+
var MAX_QUERIES = 10;
|
|
5353
|
+
var MAX_PER_PAGE = 25;
|
|
5354
|
+
var initFindIconsTool = (reg) => {
|
|
5355
|
+
const { addTool } = reg;
|
|
5356
|
+
addTool({
|
|
5357
|
+
name: "find-icons",
|
|
5358
|
+
description: findIconsToolPrompt.prompt(),
|
|
5359
|
+
schema: {
|
|
5360
|
+
queries: import_schema3.z.array(import_schema3.z.string()).max(MAX_QUERIES).optional().describe(
|
|
5361
|
+
'One plain-language phrase per icon you need, for example ["add to cart", "free shipping"]. Batch every icon a page needs into a single call.'
|
|
5362
|
+
),
|
|
5363
|
+
library: import_schema3.z.string().optional().describe(
|
|
5364
|
+
'Restrict results to one library key reported in "libraries", for example "fa-solid". Omit to search every library installed on the site.'
|
|
5365
|
+
),
|
|
5366
|
+
category: import_schema3.z.string().optional().describe(
|
|
5367
|
+
'Restrict results to one Font Awesome category reported in "categories", for example "shopping". May be used without a query to browse that category.'
|
|
5368
|
+
),
|
|
5369
|
+
page: import_schema3.z.number().int().min(1).optional(),
|
|
5370
|
+
perPage: import_schema3.z.number().int().min(1).max(MAX_PER_PAGE).optional()
|
|
5371
|
+
},
|
|
5372
|
+
outputSchema: {
|
|
5373
|
+
results: import_schema3.z.array(
|
|
5374
|
+
import_schema3.z.object({
|
|
5375
|
+
query: import_schema3.z.string(),
|
|
5376
|
+
matches: import_schema3.z.array(
|
|
5377
|
+
import_schema3.z.object({
|
|
5378
|
+
value: import_schema3.z.string(),
|
|
5379
|
+
library: import_schema3.z.string(),
|
|
5380
|
+
name: import_schema3.z.string(),
|
|
5381
|
+
label: import_schema3.z.string(),
|
|
5382
|
+
matched_on: import_schema3.z.enum(["name", "alias", "term", "category", "library"]),
|
|
5383
|
+
license: import_schema3.z.enum(["free", "pro"])
|
|
5384
|
+
})
|
|
5385
|
+
),
|
|
5386
|
+
total: import_schema3.z.number(),
|
|
5387
|
+
page: import_schema3.z.number(),
|
|
5388
|
+
per_page: import_schema3.z.number(),
|
|
5389
|
+
truncated: import_schema3.z.boolean()
|
|
5390
|
+
})
|
|
5391
|
+
).describe(
|
|
5392
|
+
'One entry per query, each with ranked "matches". Every match carries the "value" and "library" pair to write on the e-svg icon prop, plus "matched_on" and "license".'
|
|
5393
|
+
),
|
|
5394
|
+
libraries: import_schema3.z.array(import_schema3.z.string()).optional().describe(
|
|
5395
|
+
"Icon library keys installed on this site. Only sent when nothing matched or when browsing."
|
|
5396
|
+
),
|
|
5397
|
+
customLibraries: import_schema3.z.record(import_schema3.z.string(), import_schema3.z.string()).optional().describe(
|
|
5398
|
+
"Custom (uploaded) icon libraries keyed by library. Only sent when nothing matched or when browsing."
|
|
5399
|
+
),
|
|
5400
|
+
categories: import_schema3.z.array(import_schema3.z.string()).optional().describe(
|
|
5401
|
+
'Icon categories for the "category" filter. Only sent when nothing matched or when browsing.'
|
|
5402
|
+
),
|
|
5403
|
+
truncatedCustomLibraries: import_schema3.z.array(import_schema3.z.string()).optional().describe("Custom libraries with more than 250 icons; only their first 250 icons are searchable."),
|
|
5404
|
+
fontAwesomeVersion: import_schema3.z.string().nullable().describe("Font Awesome version the site renders icons from."),
|
|
5405
|
+
llmInstructions: import_schema3.z.string().optional().describe("Present only when nothing matched, explaining what to do next.")
|
|
5406
|
+
},
|
|
5407
|
+
handler: async ({ queries, library, category, page, perPage }) => {
|
|
5408
|
+
try {
|
|
5409
|
+
const { data } = await (0, import_http_client7.httpService)().post(MCP_PROXY_URL5, {
|
|
5410
|
+
tool: "find-icons",
|
|
5411
|
+
input: {
|
|
5412
|
+
...queries ? { queries } : {},
|
|
5413
|
+
...library ? { library } : {},
|
|
5414
|
+
...category ? { category } : {},
|
|
5415
|
+
...page ? { page } : {},
|
|
5416
|
+
...perPage ? { per_page: perPage } : {}
|
|
5417
|
+
}
|
|
5418
|
+
});
|
|
5419
|
+
return {
|
|
5420
|
+
results: data.data.results,
|
|
5421
|
+
...data.data.libraries ? { libraries: data.data.libraries } : {},
|
|
5422
|
+
...data.data.custom_libraries ? { customLibraries: data.data.custom_libraries } : {},
|
|
5423
|
+
...data.data.categories ? { categories: data.data.categories } : {},
|
|
5424
|
+
...data.data.truncated_custom_libraries ? { truncatedCustomLibraries: data.data.truncated_custom_libraries } : {},
|
|
5425
|
+
fontAwesomeVersion: data.data.font_awesome_version,
|
|
5426
|
+
...data.data.llm_instructions ? { llmInstructions: data.data.llm_instructions } : {}
|
|
5427
|
+
};
|
|
5428
|
+
} catch (error) {
|
|
5429
|
+
throw new Error(getMcpErrorMessage(error, "find-icons"));
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5432
|
+
});
|
|
5433
|
+
};
|
|
5434
|
+
|
|
5435
|
+
// src/mcp/tools/get-page-structure/tool.ts
|
|
5436
|
+
var import_editor_documents2 = require("@elementor/editor-documents");
|
|
5437
|
+
var import_http_client8 = require("@elementor/http-client");
|
|
5438
|
+
var import_schema4 = require("@elementor/schema");
|
|
5439
|
+
var MCP_PROXY_URL6 = "elementor/v1/mcp-proxy";
|
|
5281
5440
|
var initGetPageStructureTool = (reg) => {
|
|
5282
5441
|
const { addTool } = reg;
|
|
5283
5442
|
addTool({
|
|
5284
5443
|
name: "get-page-structure",
|
|
5285
5444
|
description: "Returns a lean Elementor element tree skeleton (id, elType, widgetType, title, nested elements) for a post or page. If no postId is provided, uses the currently open document. Optionally scope to a subtree with elementId. Set includeContent=true (requires elementId) to also return each node's settings and styles.",
|
|
5286
5445
|
schema: {
|
|
5287
|
-
postId:
|
|
5446
|
+
postId: import_schema4.z.number().optional().describe(
|
|
5288
5447
|
"WordPress post ID of the Elementor document. If omitted, uses the currently open document."
|
|
5289
5448
|
),
|
|
5290
|
-
elementId:
|
|
5291
|
-
includeContent:
|
|
5449
|
+
elementId: import_schema4.z.string().optional().describe("If provided, returns only the subtree rooted at that element id."),
|
|
5450
|
+
includeContent: import_schema4.z.boolean().optional().describe(
|
|
5292
5451
|
"If true, includes each node's settings and styles (same shape build-composition accepts as input). Requires elementId."
|
|
5293
5452
|
)
|
|
5294
5453
|
},
|
|
5295
5454
|
outputSchema: {
|
|
5296
|
-
elements:
|
|
5455
|
+
elements: import_schema4.z.array(import_schema4.z.any()).describe(
|
|
5297
5456
|
"Skeleton of Elementor elements (id, elType, widgetType, title, nested elements). When includeContent is true, each node also includes settings and styles."
|
|
5298
5457
|
)
|
|
5299
5458
|
},
|
|
@@ -5303,7 +5462,7 @@ var initGetPageStructureTool = (reg) => {
|
|
|
5303
5462
|
throw new Error("No post ID provided and no active document found.");
|
|
5304
5463
|
}
|
|
5305
5464
|
try {
|
|
5306
|
-
const { data } = await (0,
|
|
5465
|
+
const { data } = await (0, import_http_client8.httpService)().post(MCP_PROXY_URL6, {
|
|
5307
5466
|
tool: "get-page-structure",
|
|
5308
5467
|
input: {
|
|
5309
5468
|
post_id: resolvedPostId,
|
|
@@ -5333,6 +5492,9 @@ var initCanvasMcp = (reg) => {
|
|
|
5333
5492
|
initGeneralContextResource(reg);
|
|
5334
5493
|
initBestPracticesResource(reg);
|
|
5335
5494
|
initConfigureElementTool(reg);
|
|
5495
|
+
if (isIconLibraryActive()) {
|
|
5496
|
+
initFindIconsTool(reg);
|
|
5497
|
+
}
|
|
5336
5498
|
initGetPageStructureTool(reg);
|
|
5337
5499
|
initBreakpointsResource(reg);
|
|
5338
5500
|
};
|
|
@@ -5436,14 +5598,14 @@ Note: The "size" property controls image resolution/loading, not visual size. Se
|
|
|
5436
5598
|
// src/prevent-link-in-link-commands.ts
|
|
5437
5599
|
var import_editor_elements14 = require("@elementor/editor-elements");
|
|
5438
5600
|
var import_editor_notifications3 = require("@elementor/editor-notifications");
|
|
5439
|
-
var
|
|
5601
|
+
var import_editor_v1_adapters22 = require("@elementor/editor-v1-adapters");
|
|
5440
5602
|
var import_i18n4 = require("@wordpress/i18n");
|
|
5441
5603
|
function initLinkInLinkPrevention() {
|
|
5442
|
-
(0,
|
|
5604
|
+
(0, import_editor_v1_adapters22.blockCommand)({
|
|
5443
5605
|
command: "document/elements/paste",
|
|
5444
5606
|
condition: blockLinkInLinkPaste
|
|
5445
5607
|
});
|
|
5446
|
-
(0,
|
|
5608
|
+
(0, import_editor_v1_adapters22.blockCommand)({
|
|
5447
5609
|
command: "document/elements/move",
|
|
5448
5610
|
condition: blockLinkInLinkMove
|
|
5449
5611
|
});
|
|
@@ -5519,7 +5681,7 @@ function shouldBlock(sourceElements, targetElements) {
|
|
|
5519
5681
|
// src/style-commands/paste-style.ts
|
|
5520
5682
|
var import_editor_elements17 = require("@elementor/editor-elements");
|
|
5521
5683
|
var import_editor_props10 = require("@elementor/editor-props");
|
|
5522
|
-
var
|
|
5684
|
+
var import_editor_v1_adapters24 = require("@elementor/editor-v1-adapters");
|
|
5523
5685
|
|
|
5524
5686
|
// src/utils/command-utils.ts
|
|
5525
5687
|
var import_editor_elements15 = require("@elementor/editor-elements");
|
|
@@ -5566,9 +5728,9 @@ function getTitleForContainers(containers) {
|
|
|
5566
5728
|
// src/style-commands/undoable-actions/paste-element-style.ts
|
|
5567
5729
|
var import_editor_elements16 = require("@elementor/editor-elements");
|
|
5568
5730
|
var import_editor_styles_repository4 = require("@elementor/editor-styles-repository");
|
|
5569
|
-
var
|
|
5731
|
+
var import_editor_v1_adapters23 = require("@elementor/editor-v1-adapters");
|
|
5570
5732
|
var import_i18n6 = require("@wordpress/i18n");
|
|
5571
|
-
var undoablePasteElementStyle = () => (0,
|
|
5733
|
+
var undoablePasteElementStyle = () => (0, import_editor_v1_adapters23.undoable)(
|
|
5572
5734
|
{
|
|
5573
5735
|
do: ({ containers, newStyle }) => {
|
|
5574
5736
|
return containers.map((container) => {
|
|
@@ -5644,12 +5806,12 @@ var undoablePasteElementStyle = () => (0, import_editor_v1_adapters22.undoable)(
|
|
|
5644
5806
|
// src/style-commands/paste-style.ts
|
|
5645
5807
|
function initPasteStyleCommand() {
|
|
5646
5808
|
const pasteElementStyleCommand = undoablePasteElementStyle();
|
|
5647
|
-
(0,
|
|
5809
|
+
(0, import_editor_v1_adapters24.blockCommand)({
|
|
5648
5810
|
command: "document/elements/paste-style",
|
|
5649
5811
|
condition: hasAtomicWidgets
|
|
5650
5812
|
});
|
|
5651
|
-
(0,
|
|
5652
|
-
(0,
|
|
5813
|
+
(0, import_editor_v1_adapters24.__privateListenTo)(
|
|
5814
|
+
(0, import_editor_v1_adapters24.commandStartEvent)("document/elements/paste-style"),
|
|
5653
5815
|
(e) => pasteStyles(e.args, pasteElementStyleCommand)
|
|
5654
5816
|
);
|
|
5655
5817
|
}
|
|
@@ -5700,14 +5862,14 @@ function pasteClasses(containers, classes) {
|
|
|
5700
5862
|
}
|
|
5701
5863
|
|
|
5702
5864
|
// src/style-commands/reset-style.ts
|
|
5703
|
-
var
|
|
5865
|
+
var import_editor_v1_adapters26 = require("@elementor/editor-v1-adapters");
|
|
5704
5866
|
|
|
5705
5867
|
// src/style-commands/undoable-actions/reset-element-style.ts
|
|
5706
5868
|
var import_editor_elements18 = require("@elementor/editor-elements");
|
|
5707
5869
|
var import_editor_styles_repository5 = require("@elementor/editor-styles-repository");
|
|
5708
|
-
var
|
|
5870
|
+
var import_editor_v1_adapters25 = require("@elementor/editor-v1-adapters");
|
|
5709
5871
|
var import_i18n7 = require("@wordpress/i18n");
|
|
5710
|
-
var undoableResetElementStyle = () => (0,
|
|
5872
|
+
var undoableResetElementStyle = () => (0, import_editor_v1_adapters25.undoable)(
|
|
5711
5873
|
{
|
|
5712
5874
|
do: ({ containers }) => {
|
|
5713
5875
|
return containers.map((container) => {
|
|
@@ -5751,12 +5913,12 @@ var undoableResetElementStyle = () => (0, import_editor_v1_adapters24.undoable)(
|
|
|
5751
5913
|
// src/style-commands/reset-style.ts
|
|
5752
5914
|
function initResetStyleCommand() {
|
|
5753
5915
|
const resetElementStyles = undoableResetElementStyle();
|
|
5754
|
-
(0,
|
|
5916
|
+
(0, import_editor_v1_adapters26.blockCommand)({
|
|
5755
5917
|
command: "document/elements/reset-style",
|
|
5756
5918
|
condition: hasAtomicWidgets
|
|
5757
5919
|
});
|
|
5758
|
-
(0,
|
|
5759
|
-
(0,
|
|
5920
|
+
(0, import_editor_v1_adapters26.__privateListenTo)(
|
|
5921
|
+
(0, import_editor_v1_adapters26.commandStartEvent)("document/elements/reset-style"),
|
|
5760
5922
|
(e) => resetStyles(e.args, resetElementStyles)
|
|
5761
5923
|
);
|
|
5762
5924
|
}
|
|
@@ -5802,7 +5964,7 @@ function init() {
|
|
|
5802
5964
|
component: ClassesRename
|
|
5803
5965
|
});
|
|
5804
5966
|
initCanvasMcp(
|
|
5805
|
-
(0,
|
|
5967
|
+
(0, import_editor_mcp5.getMCPByDomain)("canvas", {
|
|
5806
5968
|
instructions: `Everything related to V4 ( Atomic ) canvas.
|
|
5807
5969
|
# Canvas workflow
|
|
5808
5970
|
- Configure element settings and styles with configure-element
|
|
@@ -5905,9 +6067,9 @@ function getRectClipPath(rect, viewport) {
|
|
|
5905
6067
|
}
|
|
5906
6068
|
|
|
5907
6069
|
// src/hooks/use-canvas-document.ts
|
|
5908
|
-
var
|
|
6070
|
+
var import_editor_v1_adapters27 = require("@elementor/editor-v1-adapters");
|
|
5909
6071
|
function useCanvasDocument() {
|
|
5910
|
-
return (0,
|
|
6072
|
+
return (0, import_editor_v1_adapters27.__privateUseListenTo)((0, import_editor_v1_adapters27.commandEndEvent)("editor/documents/attach-preview"), () => (0, import_editor_v1_adapters27.getCanvasIframeDocument)());
|
|
5911
6073
|
}
|
|
5912
6074
|
|
|
5913
6075
|
// src/hooks/use-escape-on-canvas.ts
|
package/dist/index.mjs
CHANGED
|
@@ -2529,11 +2529,11 @@ function getVal2(val) {
|
|
|
2529
2529
|
var transformOriginTransformer = createTransformer((value) => {
|
|
2530
2530
|
const x = getVal2(value.x);
|
|
2531
2531
|
const y = getVal2(value.y);
|
|
2532
|
-
const
|
|
2533
|
-
if (x === DEFAULT_XY && y === DEFAULT_XY &&
|
|
2532
|
+
const z4 = getVal2(value.z);
|
|
2533
|
+
if (x === DEFAULT_XY && y === DEFAULT_XY && z4 === DEFAULT_Z) {
|
|
2534
2534
|
return null;
|
|
2535
2535
|
}
|
|
2536
|
-
return `${x} ${y} ${
|
|
2536
|
+
return `${x} ${y} ${z4}`;
|
|
2537
2537
|
});
|
|
2538
2538
|
|
|
2539
2539
|
// src/transformers/styles/transform-rotate-transformer.ts
|
|
@@ -4156,6 +4156,11 @@ function initTabsModelExtensions() {
|
|
|
4156
4156
|
// src/mcp/canvas-mcp.ts
|
|
4157
4157
|
import { Schema as Schema3 } from "@elementor/editor-props";
|
|
4158
4158
|
|
|
4159
|
+
// src/mcp/icon-library-experiment.ts
|
|
4160
|
+
import { isExperimentActive } from "@elementor/editor-v1-adapters";
|
|
4161
|
+
var ICON_LIBRARY_EXPERIMENT = "e_svg_library";
|
|
4162
|
+
var isIconLibraryActive = () => isExperimentActive(ICON_LIBRARY_EXPERIMENT);
|
|
4163
|
+
|
|
4159
4164
|
// src/mcp/resources/available-widgets-resource.ts
|
|
4160
4165
|
import { httpService as httpService3 } from "@elementor/http-client";
|
|
4161
4166
|
var MCP_PROXY_URL2 = "elementor/v1/mcp-proxy";
|
|
@@ -4969,8 +4974,46 @@ Expected Schema: ${jsonSchema}`
|
|
|
4969
4974
|
|
|
4970
4975
|
// src/mcp/tools/configure-element/prompt.ts
|
|
4971
4976
|
import { toolPrompts } from "@elementor/editor-mcp";
|
|
4977
|
+
var ICONS_SECTION = `# Icons (the \`svg\` property on e-svg)
|
|
4978
|
+
The \`svg\` property accepts two variants. Prefer a library icon for iconography (carts, arrows, social marks, check marks) \u2014 it needs no upload and inherits \`color\` via \`currentColor\`. Use an uploaded file only for custom artwork such as a brand logo.
|
|
4979
|
+
|
|
4980
|
+
Resolve the icon with the \`find-icons\` tool FIRST, then copy its \`value\` and \`library\` verbatim. Never assemble those strings yourself: Font Awesome renamed many icons between major versions (\`shopping-cart\` became \`cart-shopping\`), so a plausible guess renders nothing. \`find-icons\` matches on names, aliases, human search terms, and categories, so describe the intent \u2014 "add to cart" finds \`cart-shopping\`. It takes a batch of \`queries\`, so resolve every icon you need in one call.
|
|
4981
|
+
|
|
4982
|
+
Library icon (note the nested PropValues \u2014 \`value\` and \`library\` are each a \`string\` PropValue):
|
|
4983
|
+
{ "$$type": "icon", "value": { "value": { "$$type": "string", "value": "fa-solid fa-cart-shopping" }, "library": { "$$type": "string", "value": "fa-solid" } } }
|
|
4984
|
+
|
|
4985
|
+
Uploaded SVG file:
|
|
4986
|
+
{ "$$type": "svg-src", "value": { "id": { "$$type": "image-attachment-id", "value": 123 } } }
|
|
4987
|
+
|
|
4988
|
+
Size and colour an e-svg through \`style\` (\`width\`, \`height\`, \`color\`), never through \`propertiesToChange\`.
|
|
4989
|
+
`;
|
|
4990
|
+
var ICON_EXAMPLE = `
|
|
4991
|
+
Setting a Font Awesome icon on an e-svg, after resolving it with \`find-icons\`:
|
|
4992
|
+
|
|
4993
|
+
\`\`\`json
|
|
4994
|
+
{
|
|
4995
|
+
propertiesToChange: {
|
|
4996
|
+
svg: {
|
|
4997
|
+
$$type: 'icon',
|
|
4998
|
+
value: {
|
|
4999
|
+
value: { $$type: 'string', value: 'fa-solid fa-cart-shopping' },
|
|
5000
|
+
library: { $$type: 'string', value: 'fa-solid' }
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
},
|
|
5004
|
+
style: {
|
|
5005
|
+
'width': '1.25rem',
|
|
5006
|
+
'height': '1.25rem',
|
|
5007
|
+
'color': '#1a1a1a'
|
|
5008
|
+
},
|
|
5009
|
+
elementId: 'element-id',
|
|
5010
|
+
elementType: 'e-svg'
|
|
5011
|
+
};
|
|
5012
|
+
\`\`\`
|
|
5013
|
+
`;
|
|
4972
5014
|
var CONFIGURE_ELEMENT_GUIDE_URI = "elementor://canvas/tools/configure-element-guide";
|
|
4973
5015
|
var generatePrompt = () => {
|
|
5016
|
+
const iconLibraryActive = isIconLibraryActive();
|
|
4974
5017
|
const configureElementToolPrompt = toolPrompts("configure-element");
|
|
4975
5018
|
configureElementToolPrompt.description(`
|
|
4976
5019
|
Configure an existing element on the page.
|
|
@@ -5028,7 +5071,8 @@ Put the dynamic object EXACTLY at the node whose schema offers the "dynamic" var
|
|
|
5028
5071
|
}
|
|
5029
5072
|
Image example: { "$$type": "image", "value": { "src": { "$$type": "dynamic", "value": { "name": "<image tag>", "settings": { ... } } } } }
|
|
5030
5073
|
Do NOT send "group" (it is resolved automatically). Use { "settings": {} } only when the tag has no settings.
|
|
5031
|
-
|
|
5074
|
+
|
|
5075
|
+
${iconLibraryActive ? ICONS_SECTION : ""}`);
|
|
5032
5076
|
configureElementToolPrompt.parameter("elementId", "The ID of the element to configure. MANDATORY.");
|
|
5033
5077
|
configureElementToolPrompt.parameter(
|
|
5034
5078
|
"elementType",
|
|
@@ -5065,6 +5109,9 @@ Do NOT send "group" (it is resolved automatically). Use { "settings": {} } only
|
|
|
5065
5109
|
};
|
|
5066
5110
|
\`\`\`
|
|
5067
5111
|
`);
|
|
5112
|
+
if (iconLibraryActive) {
|
|
5113
|
+
configureElementToolPrompt.example(ICON_EXAMPLE);
|
|
5114
|
+
}
|
|
5068
5115
|
configureElementToolPrompt.instruction(
|
|
5069
5116
|
"The $$type property is MANDATORY for every value in propertiesToChange; it is not used in the style parameter (raw CSS only)."
|
|
5070
5117
|
);
|
|
@@ -5238,8 +5285,7 @@ Provide styling as raw CSS via the "style" parameter (a flat map of CSS property
|
|
|
5238
5285
|
}`;
|
|
5239
5286
|
}
|
|
5240
5287
|
|
|
5241
|
-
// src/mcp/tools/
|
|
5242
|
-
import { getCurrentDocument } from "@elementor/editor-documents";
|
|
5288
|
+
// src/mcp/tools/find-icons/tool.ts
|
|
5243
5289
|
import { httpService as httpService6 } from "@elementor/http-client";
|
|
5244
5290
|
import { z as z2 } from "@elementor/schema";
|
|
5245
5291
|
|
|
@@ -5258,24 +5304,137 @@ function getMcpErrorMessage(error, toolName) {
|
|
|
5258
5304
|
return `${toolName} failed with an unknown error.`;
|
|
5259
5305
|
}
|
|
5260
5306
|
|
|
5261
|
-
// src/mcp/tools/
|
|
5307
|
+
// src/mcp/tools/find-icons/prompt.ts
|
|
5308
|
+
import { toolPrompts as toolPrompts2 } from "@elementor/editor-mcp";
|
|
5309
|
+
var findIconsToolPrompt = toolPrompts2("find-icons");
|
|
5310
|
+
findIconsToolPrompt.description(
|
|
5311
|
+
"Finds icons for the `svg` property of an `e-svg` element, across Font Awesome and any icon packs uploaded to this site. Returns the exact `value` and `library` pair to write - never assemble those strings yourself, and never guess an icon name. Call this before placing or configuring an `e-svg`. Nothing has to be uploaded to the Media Library for an icon to work."
|
|
5312
|
+
);
|
|
5313
|
+
findIconsToolPrompt.instruction(
|
|
5314
|
+
'Call it when the user names an icon ("use the cart-shopping icon") and take the first exact `name` match, and also when the user describes a page and you decide icons belong in it ("create a product page") - in that case pass one query per icon you intend to place, in a single call.'
|
|
5315
|
+
);
|
|
5316
|
+
findIconsToolPrompt.instruction(
|
|
5317
|
+
'Write queries the way a person would describe the icon ("shopping cart", "credit card", "phone"). Icons are matched on their name, aliases, Font Awesome search terms and category, so plain language works better than guessed icon names.'
|
|
5318
|
+
);
|
|
5319
|
+
findIconsToolPrompt.instruction(
|
|
5320
|
+
'Results are ordered best first; `matched_on: "name"` is a stronger signal than `matched_on: "term"`. Check each result\'s `license` and only use `free` icons; skip any `pro` entry. Prefer `fa-solid` for UI iconography and keep one style across a page; brand marks only exist in `fa-brands`. An empty `matches` array means no match was found: broaden the phrase instead of inventing a value.'
|
|
5321
|
+
);
|
|
5322
|
+
findIconsToolPrompt.instruction(
|
|
5323
|
+
'Pass the chosen pair to `configure-element` as the `svg` property: { "$$type": "icon", "value": { "value": { "$$type": "string", "value": "fa-solid fa-cart-shopping" }, "library": { "$$type": "string", "value": "fa-solid" } } }. An `e-svg` has no colour or size of its own, so set `width`, `height` and `color` in the same `configure-element` call.'
|
|
5324
|
+
);
|
|
5325
|
+
findIconsToolPrompt.example(`Icon named by the user:
|
|
5326
|
+
{ "queries": ["cart-shopping"] }`);
|
|
5327
|
+
findIconsToolPrompt.example(`Every icon a product page needs, in one call:
|
|
5328
|
+
{ "queries": ["add to cart", "free shipping", "secure payment", "instagram"] }`);
|
|
5329
|
+
findIconsToolPrompt.example(`Browse a category, one style only:
|
|
5330
|
+
{ "category": "shopping", "library": "fa-solid", "perPage": 25 }`);
|
|
5331
|
+
|
|
5332
|
+
// src/mcp/tools/find-icons/tool.ts
|
|
5262
5333
|
var MCP_PROXY_URL5 = "elementor/v1/mcp-proxy";
|
|
5334
|
+
var MAX_QUERIES = 10;
|
|
5335
|
+
var MAX_PER_PAGE = 25;
|
|
5336
|
+
var initFindIconsTool = (reg) => {
|
|
5337
|
+
const { addTool } = reg;
|
|
5338
|
+
addTool({
|
|
5339
|
+
name: "find-icons",
|
|
5340
|
+
description: findIconsToolPrompt.prompt(),
|
|
5341
|
+
schema: {
|
|
5342
|
+
queries: z2.array(z2.string()).max(MAX_QUERIES).optional().describe(
|
|
5343
|
+
'One plain-language phrase per icon you need, for example ["add to cart", "free shipping"]. Batch every icon a page needs into a single call.'
|
|
5344
|
+
),
|
|
5345
|
+
library: z2.string().optional().describe(
|
|
5346
|
+
'Restrict results to one library key reported in "libraries", for example "fa-solid". Omit to search every library installed on the site.'
|
|
5347
|
+
),
|
|
5348
|
+
category: z2.string().optional().describe(
|
|
5349
|
+
'Restrict results to one Font Awesome category reported in "categories", for example "shopping". May be used without a query to browse that category.'
|
|
5350
|
+
),
|
|
5351
|
+
page: z2.number().int().min(1).optional(),
|
|
5352
|
+
perPage: z2.number().int().min(1).max(MAX_PER_PAGE).optional()
|
|
5353
|
+
},
|
|
5354
|
+
outputSchema: {
|
|
5355
|
+
results: z2.array(
|
|
5356
|
+
z2.object({
|
|
5357
|
+
query: z2.string(),
|
|
5358
|
+
matches: z2.array(
|
|
5359
|
+
z2.object({
|
|
5360
|
+
value: z2.string(),
|
|
5361
|
+
library: z2.string(),
|
|
5362
|
+
name: z2.string(),
|
|
5363
|
+
label: z2.string(),
|
|
5364
|
+
matched_on: z2.enum(["name", "alias", "term", "category", "library"]),
|
|
5365
|
+
license: z2.enum(["free", "pro"])
|
|
5366
|
+
})
|
|
5367
|
+
),
|
|
5368
|
+
total: z2.number(),
|
|
5369
|
+
page: z2.number(),
|
|
5370
|
+
per_page: z2.number(),
|
|
5371
|
+
truncated: z2.boolean()
|
|
5372
|
+
})
|
|
5373
|
+
).describe(
|
|
5374
|
+
'One entry per query, each with ranked "matches". Every match carries the "value" and "library" pair to write on the e-svg icon prop, plus "matched_on" and "license".'
|
|
5375
|
+
),
|
|
5376
|
+
libraries: z2.array(z2.string()).optional().describe(
|
|
5377
|
+
"Icon library keys installed on this site. Only sent when nothing matched or when browsing."
|
|
5378
|
+
),
|
|
5379
|
+
customLibraries: z2.record(z2.string(), z2.string()).optional().describe(
|
|
5380
|
+
"Custom (uploaded) icon libraries keyed by library. Only sent when nothing matched or when browsing."
|
|
5381
|
+
),
|
|
5382
|
+
categories: z2.array(z2.string()).optional().describe(
|
|
5383
|
+
'Icon categories for the "category" filter. Only sent when nothing matched or when browsing.'
|
|
5384
|
+
),
|
|
5385
|
+
truncatedCustomLibraries: z2.array(z2.string()).optional().describe("Custom libraries with more than 250 icons; only their first 250 icons are searchable."),
|
|
5386
|
+
fontAwesomeVersion: z2.string().nullable().describe("Font Awesome version the site renders icons from."),
|
|
5387
|
+
llmInstructions: z2.string().optional().describe("Present only when nothing matched, explaining what to do next.")
|
|
5388
|
+
},
|
|
5389
|
+
handler: async ({ queries, library, category, page, perPage }) => {
|
|
5390
|
+
try {
|
|
5391
|
+
const { data } = await httpService6().post(MCP_PROXY_URL5, {
|
|
5392
|
+
tool: "find-icons",
|
|
5393
|
+
input: {
|
|
5394
|
+
...queries ? { queries } : {},
|
|
5395
|
+
...library ? { library } : {},
|
|
5396
|
+
...category ? { category } : {},
|
|
5397
|
+
...page ? { page } : {},
|
|
5398
|
+
...perPage ? { per_page: perPage } : {}
|
|
5399
|
+
}
|
|
5400
|
+
});
|
|
5401
|
+
return {
|
|
5402
|
+
results: data.data.results,
|
|
5403
|
+
...data.data.libraries ? { libraries: data.data.libraries } : {},
|
|
5404
|
+
...data.data.custom_libraries ? { customLibraries: data.data.custom_libraries } : {},
|
|
5405
|
+
...data.data.categories ? { categories: data.data.categories } : {},
|
|
5406
|
+
...data.data.truncated_custom_libraries ? { truncatedCustomLibraries: data.data.truncated_custom_libraries } : {},
|
|
5407
|
+
fontAwesomeVersion: data.data.font_awesome_version,
|
|
5408
|
+
...data.data.llm_instructions ? { llmInstructions: data.data.llm_instructions } : {}
|
|
5409
|
+
};
|
|
5410
|
+
} catch (error) {
|
|
5411
|
+
throw new Error(getMcpErrorMessage(error, "find-icons"));
|
|
5412
|
+
}
|
|
5413
|
+
}
|
|
5414
|
+
});
|
|
5415
|
+
};
|
|
5416
|
+
|
|
5417
|
+
// src/mcp/tools/get-page-structure/tool.ts
|
|
5418
|
+
import { getCurrentDocument } from "@elementor/editor-documents";
|
|
5419
|
+
import { httpService as httpService7 } from "@elementor/http-client";
|
|
5420
|
+
import { z as z3 } from "@elementor/schema";
|
|
5421
|
+
var MCP_PROXY_URL6 = "elementor/v1/mcp-proxy";
|
|
5263
5422
|
var initGetPageStructureTool = (reg) => {
|
|
5264
5423
|
const { addTool } = reg;
|
|
5265
5424
|
addTool({
|
|
5266
5425
|
name: "get-page-structure",
|
|
5267
5426
|
description: "Returns a lean Elementor element tree skeleton (id, elType, widgetType, title, nested elements) for a post or page. If no postId is provided, uses the currently open document. Optionally scope to a subtree with elementId. Set includeContent=true (requires elementId) to also return each node's settings and styles.",
|
|
5268
5427
|
schema: {
|
|
5269
|
-
postId:
|
|
5428
|
+
postId: z3.number().optional().describe(
|
|
5270
5429
|
"WordPress post ID of the Elementor document. If omitted, uses the currently open document."
|
|
5271
5430
|
),
|
|
5272
|
-
elementId:
|
|
5273
|
-
includeContent:
|
|
5431
|
+
elementId: z3.string().optional().describe("If provided, returns only the subtree rooted at that element id."),
|
|
5432
|
+
includeContent: z3.boolean().optional().describe(
|
|
5274
5433
|
"If true, includes each node's settings and styles (same shape build-composition accepts as input). Requires elementId."
|
|
5275
5434
|
)
|
|
5276
5435
|
},
|
|
5277
5436
|
outputSchema: {
|
|
5278
|
-
elements:
|
|
5437
|
+
elements: z3.array(z3.any()).describe(
|
|
5279
5438
|
"Skeleton of Elementor elements (id, elType, widgetType, title, nested elements). When includeContent is true, each node also includes settings and styles."
|
|
5280
5439
|
)
|
|
5281
5440
|
},
|
|
@@ -5285,7 +5444,7 @@ var initGetPageStructureTool = (reg) => {
|
|
|
5285
5444
|
throw new Error("No post ID provided and no active document found.");
|
|
5286
5445
|
}
|
|
5287
5446
|
try {
|
|
5288
|
-
const { data } = await
|
|
5447
|
+
const { data } = await httpService7().post(MCP_PROXY_URL6, {
|
|
5289
5448
|
tool: "get-page-structure",
|
|
5290
5449
|
input: {
|
|
5291
5450
|
post_id: resolvedPostId,
|
|
@@ -5315,6 +5474,9 @@ var initCanvasMcp = (reg) => {
|
|
|
5315
5474
|
initGeneralContextResource(reg);
|
|
5316
5475
|
initBestPracticesResource(reg);
|
|
5317
5476
|
initConfigureElementTool(reg);
|
|
5477
|
+
if (isIconLibraryActive()) {
|
|
5478
|
+
initFindIconsTool(reg);
|
|
5479
|
+
}
|
|
5318
5480
|
initGetPageStructureTool(reg);
|
|
5319
5481
|
initBreakpointsResource(reg);
|
|
5320
5482
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elementor/editor-canvas",
|
|
3
3
|
"description": "Elementor Editor Canvas",
|
|
4
|
-
"version": "4.4.0-
|
|
4
|
+
"version": "4.4.0-1125",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Elementor Team",
|
|
7
7
|
"homepage": "https://elementor.com/",
|
|
@@ -37,26 +37,26 @@
|
|
|
37
37
|
"react-dom": "^18.3.1"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@elementor/editor": "4.4.0-
|
|
41
|
-
"@elementor/editor-controls": "4.4.0-
|
|
42
|
-
"@elementor/editor-documents": "4.4.0-
|
|
43
|
-
"@elementor/editor-elements": "4.4.0-
|
|
44
|
-
"@elementor/editor-interactions": "4.4.0-
|
|
45
|
-
"@elementor/editor-mcp": "4.4.0-
|
|
46
|
-
"@elementor/editor-notifications": "4.4.0-
|
|
47
|
-
"@elementor/editor-props": "4.4.0-
|
|
48
|
-
"@elementor/editor-responsive": "4.4.0-
|
|
49
|
-
"@elementor/editor-styles": "4.4.0-
|
|
50
|
-
"@elementor/editor-styles-repository": "4.4.0-
|
|
51
|
-
"@elementor/editor-ui": "4.4.0-
|
|
52
|
-
"@elementor/editor-v1-adapters": "4.4.0-
|
|
53
|
-
"@elementor/events": "4.4.0-
|
|
54
|
-
"@elementor/http-client": "4.4.0-
|
|
55
|
-
"@elementor/schema": "4.4.0-
|
|
56
|
-
"@elementor/twing": "4.4.0-
|
|
40
|
+
"@elementor/editor": "4.4.0-1125",
|
|
41
|
+
"@elementor/editor-controls": "4.4.0-1125",
|
|
42
|
+
"@elementor/editor-documents": "4.4.0-1125",
|
|
43
|
+
"@elementor/editor-elements": "4.4.0-1125",
|
|
44
|
+
"@elementor/editor-interactions": "4.4.0-1125",
|
|
45
|
+
"@elementor/editor-mcp": "4.4.0-1125",
|
|
46
|
+
"@elementor/editor-notifications": "4.4.0-1125",
|
|
47
|
+
"@elementor/editor-props": "4.4.0-1125",
|
|
48
|
+
"@elementor/editor-responsive": "4.4.0-1125",
|
|
49
|
+
"@elementor/editor-styles": "4.4.0-1125",
|
|
50
|
+
"@elementor/editor-styles-repository": "4.4.0-1125",
|
|
51
|
+
"@elementor/editor-ui": "4.4.0-1125",
|
|
52
|
+
"@elementor/editor-v1-adapters": "4.4.0-1125",
|
|
53
|
+
"@elementor/events": "4.4.0-1125",
|
|
54
|
+
"@elementor/http-client": "4.4.0-1125",
|
|
55
|
+
"@elementor/schema": "4.4.0-1125",
|
|
56
|
+
"@elementor/twing": "4.4.0-1125",
|
|
57
57
|
"@elementor/ui": "1.37.5",
|
|
58
|
-
"@elementor/utils": "4.4.0-
|
|
59
|
-
"@elementor/wp-media": "4.4.0-
|
|
58
|
+
"@elementor/utils": "4.4.0-1125",
|
|
59
|
+
"@elementor/wp-media": "4.4.0-1125",
|
|
60
60
|
"@floating-ui/react": "^0.27.5",
|
|
61
61
|
"@wordpress/i18n": "^5.13.0",
|
|
62
62
|
"dompurify": "^3.2.6"
|
package/src/mcp/canvas-mcp.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type MCPRegistryEntry } from '@elementor/editor-mcp';
|
|
2
2
|
import { Schema } from '@elementor/editor-props';
|
|
3
3
|
|
|
4
|
+
import { isIconLibraryActive } from './icon-library-experiment';
|
|
4
5
|
import { initAvailableWidgetsResource } from './resources/available-widgets-resource';
|
|
5
6
|
import { initBestPracticesResource } from './resources/best-practices-resource';
|
|
6
7
|
import { initBreakpointsResource } from './resources/breakpoints-resource';
|
|
@@ -11,6 +12,7 @@ import { initGeneralContextResource } from './resources/general-context-resource
|
|
|
11
12
|
import { initSelectedElementResource } from './resources/selected-element-resource';
|
|
12
13
|
import { initWidgetsSchemaResource } from './resources/widgets-schema-resource';
|
|
13
14
|
import { initConfigureElementTool } from './tools/configure-element/tool';
|
|
15
|
+
import { initFindIconsTool } from './tools/find-icons/tool';
|
|
14
16
|
import { initGetPageStructureTool } from './tools/get-page-structure/tool';
|
|
15
17
|
import { getDynamicTagNamesByCategories } from './utils/resolve-dynamic-tag';
|
|
16
18
|
|
|
@@ -28,6 +30,9 @@ export const initCanvasMcp = ( reg: MCPRegistryEntry ) => {
|
|
|
28
30
|
initGeneralContextResource( reg );
|
|
29
31
|
initBestPracticesResource( reg );
|
|
30
32
|
initConfigureElementTool( reg );
|
|
33
|
+
if ( isIconLibraryActive() ) {
|
|
34
|
+
initFindIconsTool( reg );
|
|
35
|
+
}
|
|
31
36
|
initGetPageStructureTool( reg );
|
|
32
37
|
initBreakpointsResource( reg );
|
|
33
38
|
};
|
|
@@ -1,11 +1,52 @@
|
|
|
1
1
|
import { toolPrompts } from '@elementor/editor-mcp';
|
|
2
2
|
|
|
3
|
+
import { isIconLibraryActive } from '../../icon-library-experiment';
|
|
3
4
|
import { DYNAMIC_TAGS_URI } from '../../resources/dynamic-tags-resource';
|
|
4
5
|
import { WIDGET_SCHEMA_URI } from '../../resources/widgets-schema-resource';
|
|
5
6
|
|
|
7
|
+
const ICONS_SECTION = `# Icons (the \`svg\` property on e-svg)
|
|
8
|
+
The \`svg\` property accepts two variants. Prefer a library icon for iconography (carts, arrows, social marks, check marks) — it needs no upload and inherits \`color\` via \`currentColor\`. Use an uploaded file only for custom artwork such as a brand logo.
|
|
9
|
+
|
|
10
|
+
Resolve the icon with the \`find-icons\` tool FIRST, then copy its \`value\` and \`library\` verbatim. Never assemble those strings yourself: Font Awesome renamed many icons between major versions (\`shopping-cart\` became \`cart-shopping\`), so a plausible guess renders nothing. \`find-icons\` matches on names, aliases, human search terms, and categories, so describe the intent — "add to cart" finds \`cart-shopping\`. It takes a batch of \`queries\`, so resolve every icon you need in one call.
|
|
11
|
+
|
|
12
|
+
Library icon (note the nested PropValues — \`value\` and \`library\` are each a \`string\` PropValue):
|
|
13
|
+
{ "$$type": "icon", "value": { "value": { "$$type": "string", "value": "fa-solid fa-cart-shopping" }, "library": { "$$type": "string", "value": "fa-solid" } } }
|
|
14
|
+
|
|
15
|
+
Uploaded SVG file:
|
|
16
|
+
{ "$$type": "svg-src", "value": { "id": { "$$type": "image-attachment-id", "value": 123 } } }
|
|
17
|
+
|
|
18
|
+
Size and colour an e-svg through \`style\` (\`width\`, \`height\`, \`color\`), never through \`propertiesToChange\`.
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const ICON_EXAMPLE = `
|
|
22
|
+
Setting a Font Awesome icon on an e-svg, after resolving it with \`find-icons\`:
|
|
23
|
+
|
|
24
|
+
\`\`\`json
|
|
25
|
+
{
|
|
26
|
+
propertiesToChange: {
|
|
27
|
+
svg: {
|
|
28
|
+
$$type: 'icon',
|
|
29
|
+
value: {
|
|
30
|
+
value: { $$type: 'string', value: 'fa-solid fa-cart-shopping' },
|
|
31
|
+
library: { $$type: 'string', value: 'fa-solid' }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
style: {
|
|
36
|
+
'width': '1.25rem',
|
|
37
|
+
'height': '1.25rem',
|
|
38
|
+
'color': '#1a1a1a'
|
|
39
|
+
},
|
|
40
|
+
elementId: 'element-id',
|
|
41
|
+
elementType: 'e-svg'
|
|
42
|
+
};
|
|
43
|
+
\`\`\`
|
|
44
|
+
`;
|
|
45
|
+
|
|
6
46
|
export const CONFIGURE_ELEMENT_GUIDE_URI = 'elementor://canvas/tools/configure-element-guide';
|
|
7
47
|
|
|
8
48
|
export const generatePrompt = () => {
|
|
49
|
+
const iconLibraryActive = isIconLibraryActive();
|
|
9
50
|
const configureElementToolPrompt = toolPrompts( 'configure-element' );
|
|
10
51
|
|
|
11
52
|
configureElementToolPrompt.description( `
|
|
@@ -64,7 +105,8 @@ Put the dynamic object EXACTLY at the node whose schema offers the "dynamic" var
|
|
|
64
105
|
}
|
|
65
106
|
Image example: { "$$type": "image", "value": { "src": { "$$type": "dynamic", "value": { "name": "<image tag>", "settings": { ... } } } } }
|
|
66
107
|
Do NOT send "group" (it is resolved automatically). Use { "settings": {} } only when the tag has no settings.
|
|
67
|
-
|
|
108
|
+
|
|
109
|
+
${ iconLibraryActive ? ICONS_SECTION : '' }` );
|
|
68
110
|
|
|
69
111
|
configureElementToolPrompt.parameter( 'elementId', 'The ID of the element to configure. MANDATORY.' );
|
|
70
112
|
|
|
@@ -107,6 +149,10 @@ Do NOT send "group" (it is resolved automatically). Use { "settings": {} } only
|
|
|
107
149
|
\`\`\`
|
|
108
150
|
` );
|
|
109
151
|
|
|
152
|
+
if ( iconLibraryActive ) {
|
|
153
|
+
configureElementToolPrompt.example( ICON_EXAMPLE );
|
|
154
|
+
}
|
|
155
|
+
|
|
110
156
|
configureElementToolPrompt.instruction(
|
|
111
157
|
'The $$type property is MANDATORY for every value in propertiesToChange; it is not used in the style parameter (raw CSS only).'
|
|
112
158
|
);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { httpService } from '@elementor/http-client';
|
|
2
|
+
|
|
3
|
+
import { initFindIconsTool } from '../tool';
|
|
4
|
+
|
|
5
|
+
jest.mock( '@elementor/http-client', () => ( {
|
|
6
|
+
httpService: jest.fn(),
|
|
7
|
+
AxiosError: class AxiosError extends Error {},
|
|
8
|
+
} ) );
|
|
9
|
+
|
|
10
|
+
const mockedHttpService = httpService as jest.MockedFunction< typeof httpService >;
|
|
11
|
+
|
|
12
|
+
type ToolConfig = {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
schema: Record< string, unknown >;
|
|
16
|
+
handler: ( input: Record< string, unknown > ) => Promise< Record< string, unknown > >;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const captureTool = (): ToolConfig => {
|
|
20
|
+
const addTool = jest.fn();
|
|
21
|
+
initFindIconsTool( { addTool } as never );
|
|
22
|
+
return addTool.mock.calls[ 0 ][ 0 ] as ToolConfig;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const serverResponse = {
|
|
26
|
+
results: [
|
|
27
|
+
{
|
|
28
|
+
query: 'add to cart',
|
|
29
|
+
matches: [
|
|
30
|
+
{
|
|
31
|
+
value: 'fa-solid fa-cart-plus',
|
|
32
|
+
library: 'fa-solid',
|
|
33
|
+
name: 'cart-plus',
|
|
34
|
+
label: 'Cart Plus',
|
|
35
|
+
matched_on: 'term',
|
|
36
|
+
license: 'free',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
total: 1,
|
|
40
|
+
page: 1,
|
|
41
|
+
per_page: 10,
|
|
42
|
+
truncated: false,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
libraries: [ 'fa-brands', 'fa-regular', 'fa-solid' ],
|
|
46
|
+
categories: [ 'shopping' ],
|
|
47
|
+
font_awesome_version: '7.3.1',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const givenServerResponse = ( data: Record< string, unknown > ) => {
|
|
51
|
+
const post = jest.fn().mockResolvedValue( { data: { data } } );
|
|
52
|
+
mockedHttpService.mockReturnValue( { post } as never );
|
|
53
|
+
return post;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
describe( 'find-icons tool', () => {
|
|
57
|
+
beforeEach( () => {
|
|
58
|
+
jest.clearAllMocks();
|
|
59
|
+
} );
|
|
60
|
+
|
|
61
|
+
it( 'proxies a batch of queries to the php ability', async () => {
|
|
62
|
+
// Arrange
|
|
63
|
+
const post = givenServerResponse( serverResponse );
|
|
64
|
+
const { handler } = captureTool();
|
|
65
|
+
|
|
66
|
+
// Act
|
|
67
|
+
await handler( { queries: [ 'add to cart', 'free shipping' ] } );
|
|
68
|
+
|
|
69
|
+
// Assert
|
|
70
|
+
expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
|
|
71
|
+
tool: 'find-icons',
|
|
72
|
+
input: { queries: [ 'add to cart', 'free shipping' ] },
|
|
73
|
+
} );
|
|
74
|
+
} );
|
|
75
|
+
|
|
76
|
+
it( 'maps per-page to the snake-cased ability input', async () => {
|
|
77
|
+
// Arrange
|
|
78
|
+
const post = givenServerResponse( serverResponse );
|
|
79
|
+
const { handler } = captureTool();
|
|
80
|
+
|
|
81
|
+
// Act
|
|
82
|
+
await handler( {
|
|
83
|
+
queries: [ 'cart' ],
|
|
84
|
+
library: 'fa-solid',
|
|
85
|
+
category: 'shopping',
|
|
86
|
+
page: 2,
|
|
87
|
+
perPage: 25,
|
|
88
|
+
} );
|
|
89
|
+
|
|
90
|
+
// Assert
|
|
91
|
+
expect( post ).toHaveBeenCalledWith( 'elementor/v1/mcp-proxy', {
|
|
92
|
+
tool: 'find-icons',
|
|
93
|
+
input: {
|
|
94
|
+
queries: [ 'cart' ],
|
|
95
|
+
library: 'fa-solid',
|
|
96
|
+
category: 'shopping',
|
|
97
|
+
page: 2,
|
|
98
|
+
per_page: 25,
|
|
99
|
+
},
|
|
100
|
+
} );
|
|
101
|
+
} );
|
|
102
|
+
|
|
103
|
+
it( 'returns the writable icon pair and the site catalog facets', async () => {
|
|
104
|
+
// Arrange
|
|
105
|
+
givenServerResponse( serverResponse );
|
|
106
|
+
const { handler } = captureTool();
|
|
107
|
+
|
|
108
|
+
// Act
|
|
109
|
+
const result = await handler( { queries: [ 'add to cart' ] } );
|
|
110
|
+
|
|
111
|
+
// Assert
|
|
112
|
+
expect( result.results ).toEqual( serverResponse.results );
|
|
113
|
+
expect( result.libraries ).toEqual( serverResponse.libraries );
|
|
114
|
+
expect( result.categories ).toEqual( serverResponse.categories );
|
|
115
|
+
expect( result.fontAwesomeVersion ).toBe( '7.3.1' );
|
|
116
|
+
} );
|
|
117
|
+
|
|
118
|
+
it( 'omits facets the server did not send', async () => {
|
|
119
|
+
// Arrange
|
|
120
|
+
givenServerResponse( { results: serverResponse.results, font_awesome_version: '7.3.1' } );
|
|
121
|
+
const { handler } = captureTool();
|
|
122
|
+
|
|
123
|
+
// Act
|
|
124
|
+
const result = await handler( { queries: [ 'add to cart' ] } );
|
|
125
|
+
|
|
126
|
+
// Assert
|
|
127
|
+
expect( result ).not.toHaveProperty( 'libraries' );
|
|
128
|
+
expect( result ).not.toHaveProperty( 'customLibraries' );
|
|
129
|
+
expect( result ).not.toHaveProperty( 'categories' );
|
|
130
|
+
} );
|
|
131
|
+
|
|
132
|
+
it( 'surfaces a server failure as a tool error', async () => {
|
|
133
|
+
// Arrange
|
|
134
|
+
const post = jest.fn().mockRejectedValue( new Error( 'The icon catalog is missing on this site.' ) );
|
|
135
|
+
mockedHttpService.mockReturnValue( { post } as never );
|
|
136
|
+
const { handler } = captureTool();
|
|
137
|
+
|
|
138
|
+
// Act & Assert
|
|
139
|
+
await expect( handler( { queries: [ 'cart' ] } ) ).rejects.toThrow(
|
|
140
|
+
'The icon catalog is missing on this site.'
|
|
141
|
+
);
|
|
142
|
+
} );
|
|
143
|
+
} );
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { toolPrompts } from '@elementor/editor-mcp';
|
|
2
|
+
|
|
3
|
+
export const findIconsToolPrompt = toolPrompts( 'find-icons' );
|
|
4
|
+
|
|
5
|
+
findIconsToolPrompt.description(
|
|
6
|
+
'Finds icons for the `svg` property of an `e-svg` element, across Font Awesome and any icon packs uploaded to this site. ' +
|
|
7
|
+
'Returns the exact `value` and `library` pair to write - never assemble those strings yourself, and never guess an icon name. ' +
|
|
8
|
+
'Call this before placing or configuring an `e-svg`. Nothing has to be uploaded to the Media Library for an icon to work.'
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
findIconsToolPrompt.instruction(
|
|
12
|
+
'Call it when the user names an icon ("use the cart-shopping icon") and take the first exact `name` match, ' +
|
|
13
|
+
'and also when the user describes a page and you decide icons belong in it ("create a product page") - ' +
|
|
14
|
+
'in that case pass one query per icon you intend to place, in a single call.'
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
findIconsToolPrompt.instruction(
|
|
18
|
+
'Write queries the way a person would describe the icon ("shopping cart", "credit card", "phone"). Icons are matched on ' +
|
|
19
|
+
'their name, aliases, Font Awesome search terms and category, so plain language works better than guessed icon names.'
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
findIconsToolPrompt.instruction(
|
|
23
|
+
'Results are ordered best first; `matched_on: "name"` is a stronger signal than `matched_on: "term"`. ' +
|
|
24
|
+
"Check each result's `license` and only use `free` icons; skip any `pro` entry. " +
|
|
25
|
+
'Prefer `fa-solid` for UI iconography and keep one style across a page; brand marks only exist in `fa-brands`. ' +
|
|
26
|
+
'An empty `matches` array means no match was found: broaden the phrase instead of inventing a value.'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
findIconsToolPrompt.instruction(
|
|
30
|
+
'Pass the chosen pair to `configure-element` as the `svg` property: ' +
|
|
31
|
+
'{ "$$type": "icon", "value": { "value": { "$$type": "string", "value": "fa-solid fa-cart-shopping" }, ' +
|
|
32
|
+
'"library": { "$$type": "string", "value": "fa-solid" } } }. ' +
|
|
33
|
+
'An `e-svg` has no colour or size of its own, so set `width`, `height` and `color` in the same `configure-element` call.'
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
findIconsToolPrompt.example( `Icon named by the user:
|
|
37
|
+
{ "queries": ["cart-shopping"] }` );
|
|
38
|
+
|
|
39
|
+
findIconsToolPrompt.example( `Every icon a product page needs, in one call:
|
|
40
|
+
{ "queries": ["add to cart", "free shipping", "secure payment", "instagram"] }` );
|
|
41
|
+
|
|
42
|
+
findIconsToolPrompt.example( `Browse a category, one style only:
|
|
43
|
+
{ "category": "shopping", "library": "fa-solid", "perPage": 25 }` );
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { type MCPRegistryEntry } from '@elementor/editor-mcp';
|
|
2
|
+
import { type HttpResponse, httpService } from '@elementor/http-client';
|
|
3
|
+
import { z } from '@elementor/schema';
|
|
4
|
+
|
|
5
|
+
import { getMcpErrorMessage } from '../../utils/get-mcp-error-message';
|
|
6
|
+
import { findIconsToolPrompt } from './prompt';
|
|
7
|
+
|
|
8
|
+
const MCP_PROXY_URL = 'elementor/v1/mcp-proxy';
|
|
9
|
+
|
|
10
|
+
const MAX_QUERIES = 10;
|
|
11
|
+
|
|
12
|
+
const MAX_PER_PAGE = 25;
|
|
13
|
+
|
|
14
|
+
type IconMatch = {
|
|
15
|
+
value: string;
|
|
16
|
+
library: string;
|
|
17
|
+
name: string;
|
|
18
|
+
label: string;
|
|
19
|
+
matched_on: 'name' | 'alias' | 'term' | 'category' | 'library';
|
|
20
|
+
license: 'free' | 'pro';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type IconQueryResult = {
|
|
24
|
+
query: string;
|
|
25
|
+
matches: IconMatch[];
|
|
26
|
+
total: number;
|
|
27
|
+
page: number;
|
|
28
|
+
per_page: number;
|
|
29
|
+
truncated: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// NOTE: shape defined by Find_Icons_Ability::execute() in modules/mcp/abilities/find-icons-ability.php
|
|
33
|
+
type FindIconsResponse = {
|
|
34
|
+
results: IconQueryResult[];
|
|
35
|
+
libraries?: string[];
|
|
36
|
+
custom_libraries?: Record< string, string >;
|
|
37
|
+
categories?: string[];
|
|
38
|
+
truncated_custom_libraries?: string[];
|
|
39
|
+
font_awesome_version: string | null;
|
|
40
|
+
llm_instructions?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const initFindIconsTool = ( reg: MCPRegistryEntry ) => {
|
|
44
|
+
const { addTool } = reg;
|
|
45
|
+
|
|
46
|
+
addTool( {
|
|
47
|
+
name: 'find-icons',
|
|
48
|
+
description: findIconsToolPrompt.prompt(),
|
|
49
|
+
schema: {
|
|
50
|
+
queries: z
|
|
51
|
+
.array( z.string() )
|
|
52
|
+
.max( MAX_QUERIES )
|
|
53
|
+
.optional()
|
|
54
|
+
.describe(
|
|
55
|
+
'One plain-language phrase per icon you need, for example ["add to cart", "free shipping"]. Batch every icon a page needs into a single call.'
|
|
56
|
+
),
|
|
57
|
+
library: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe(
|
|
61
|
+
'Restrict results to one library key reported in "libraries", for example "fa-solid". Omit to search every library installed on the site.'
|
|
62
|
+
),
|
|
63
|
+
category: z
|
|
64
|
+
.string()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe(
|
|
67
|
+
'Restrict results to one Font Awesome category reported in "categories", for example "shopping". May be used without a query to browse that category.'
|
|
68
|
+
),
|
|
69
|
+
page: z.number().int().min( 1 ).optional(),
|
|
70
|
+
perPage: z.number().int().min( 1 ).max( MAX_PER_PAGE ).optional(),
|
|
71
|
+
},
|
|
72
|
+
outputSchema: {
|
|
73
|
+
results: z
|
|
74
|
+
.array(
|
|
75
|
+
z.object( {
|
|
76
|
+
query: z.string(),
|
|
77
|
+
matches: z.array(
|
|
78
|
+
z.object( {
|
|
79
|
+
value: z.string(),
|
|
80
|
+
library: z.string(),
|
|
81
|
+
name: z.string(),
|
|
82
|
+
label: z.string(),
|
|
83
|
+
matched_on: z.enum( [ 'name', 'alias', 'term', 'category', 'library' ] ),
|
|
84
|
+
license: z.enum( [ 'free', 'pro' ] ),
|
|
85
|
+
} )
|
|
86
|
+
),
|
|
87
|
+
total: z.number(),
|
|
88
|
+
page: z.number(),
|
|
89
|
+
per_page: z.number(),
|
|
90
|
+
truncated: z.boolean(),
|
|
91
|
+
} )
|
|
92
|
+
)
|
|
93
|
+
.describe(
|
|
94
|
+
'One entry per query, each with ranked "matches". Every match carries the "value" and "library" pair to write on the e-svg icon prop, plus "matched_on" and "license".'
|
|
95
|
+
),
|
|
96
|
+
libraries: z
|
|
97
|
+
.array( z.string() )
|
|
98
|
+
.optional()
|
|
99
|
+
.describe(
|
|
100
|
+
'Icon library keys installed on this site. Only sent when nothing matched or when browsing.'
|
|
101
|
+
),
|
|
102
|
+
customLibraries: z
|
|
103
|
+
.record( z.string(), z.string() )
|
|
104
|
+
.optional()
|
|
105
|
+
.describe(
|
|
106
|
+
'Custom (uploaded) icon libraries keyed by library. Only sent when nothing matched or when browsing.'
|
|
107
|
+
),
|
|
108
|
+
categories: z
|
|
109
|
+
.array( z.string() )
|
|
110
|
+
.optional()
|
|
111
|
+
.describe(
|
|
112
|
+
'Icon categories for the "category" filter. Only sent when nothing matched or when browsing.'
|
|
113
|
+
),
|
|
114
|
+
truncatedCustomLibraries: z
|
|
115
|
+
.array( z.string() )
|
|
116
|
+
.optional()
|
|
117
|
+
.describe( 'Custom libraries with more than 250 icons; only their first 250 icons are searchable.' ),
|
|
118
|
+
fontAwesomeVersion: z.string().nullable().describe( 'Font Awesome version the site renders icons from.' ),
|
|
119
|
+
llmInstructions: z
|
|
120
|
+
.string()
|
|
121
|
+
.optional()
|
|
122
|
+
.describe( 'Present only when nothing matched, explaining what to do next.' ),
|
|
123
|
+
},
|
|
124
|
+
handler: async ( { queries, library, category, page, perPage } ) => {
|
|
125
|
+
try {
|
|
126
|
+
const { data } = await httpService().post< HttpResponse< FindIconsResponse > >( MCP_PROXY_URL, {
|
|
127
|
+
tool: 'find-icons',
|
|
128
|
+
input: {
|
|
129
|
+
...( queries ? { queries } : {} ),
|
|
130
|
+
...( library ? { library } : {} ),
|
|
131
|
+
...( category ? { category } : {} ),
|
|
132
|
+
...( page ? { page } : {} ),
|
|
133
|
+
...( perPage ? { per_page: perPage } : {} ),
|
|
134
|
+
},
|
|
135
|
+
} );
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
results: data.data.results,
|
|
139
|
+
...( data.data.libraries ? { libraries: data.data.libraries } : {} ),
|
|
140
|
+
...( data.data.custom_libraries ? { customLibraries: data.data.custom_libraries } : {} ),
|
|
141
|
+
...( data.data.categories ? { categories: data.data.categories } : {} ),
|
|
142
|
+
...( data.data.truncated_custom_libraries
|
|
143
|
+
? { truncatedCustomLibraries: data.data.truncated_custom_libraries }
|
|
144
|
+
: {} ),
|
|
145
|
+
fontAwesomeVersion: data.data.font_awesome_version,
|
|
146
|
+
...( data.data.llm_instructions ? { llmInstructions: data.data.llm_instructions } : {} ),
|
|
147
|
+
};
|
|
148
|
+
} catch ( error ) {
|
|
149
|
+
throw new Error( getMcpErrorMessage( error, 'find-icons' ) );
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
} );
|
|
153
|
+
};
|