unframer 4.2.0 → 4.2.1
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/css.d.ts +1 -1
- package/dist/css.d.ts.map +1 -1
- package/dist/framer-chunks/fontshare-PZLWRK4B-MHMZIGTX.js +7 -0
- package/dist/framer-chunks/fontshare-SXU5BGFE-OWTMMPGS.js +7 -0
- package/dist/framer-chunks/fontshare-TIA7QUPT-PUDLUTQ7.js +7 -0
- package/dist/framer-chunks/framer-font-RD2SUPQH-Q4MS7WS6.js +7 -0
- package/dist/framer-chunks/google-EGNT223R-P4DUHBW2.js +7 -0
- package/dist/framer-chunks/google-H6SFY4F5-5JSJCGDR.js +7 -0
- package/dist/framer-chunks/google-YSYBFRE6-L7YAHH7V.js +7 -0
- package/dist/framer.js +15625 -10915
- package/dist/plugin-mcp-dist/lib/framer-client.server.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/framer-client.server.js +9 -0
- package/dist/plugin-mcp-dist/lib/framer-client.server.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/framer.d.ts +8 -1
- package/dist/plugin-mcp-dist/lib/framer.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/framer.js +87 -7
- package/dist/plugin-mcp-dist/lib/framer.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/framer.test.js +45 -1
- package/dist/plugin-mcp-dist/lib/framer.test.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts +12 -12
- package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/mcp-handlers.js +190 -56
- package/dist/plugin-mcp-dist/lib/mcp-handlers.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/plugin-websocket.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/plugin-websocket.js +36 -25
- package/dist/plugin-mcp-dist/lib/plugin-websocket.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/utils.d.ts +1 -1
- package/dist/plugin-mcp-dist/lib/utils.d.ts.map +1 -1
- package/dist/react.d.ts +3 -3
- package/dist/react.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/src/framer.js +15625 -10915
- package/src/plugin-mcp-dist/lib/framer-client.server.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/framer-client.server.js +9 -0
- package/src/plugin-mcp-dist/lib/framer-client.server.js.map +1 -1
- package/src/plugin-mcp-dist/lib/framer.d.ts +10 -1
- package/src/plugin-mcp-dist/lib/framer.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/framer.js +87 -7
- package/src/plugin-mcp-dist/lib/framer.js.map +1 -1
- package/src/plugin-mcp-dist/lib/framer.test.js +45 -1
- package/src/plugin-mcp-dist/lib/framer.test.js.map +1 -1
- package/src/plugin-mcp-dist/lib/mcp-handlers.d.ts +3 -3
- package/src/plugin-mcp-dist/lib/mcp-handlers.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/mcp-handlers.js +190 -56
- package/src/plugin-mcp-dist/lib/mcp-handlers.js.map +1 -1
- package/src/plugin-mcp-dist/lib/plugin-websocket.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/plugin-websocket.js +36 -25
- package/src/plugin-mcp-dist/lib/plugin-websocket.js.map +1 -1
- package/src/plugin-mcp-dist/lib/react-export.d.ts +1 -1
- package/src/plugin-mcp-dist/lib/schema.d.ts +3 -3
- package/src/styles/framer.css +2 -56
- package/src/version.ts +1 -1
- package/dist/framer-chunks/SqliteDatabase-VAKIICSG-OPV4MG3Y.js +0 -98
- package/dist/framer-chunks/chunk-7ZLQTYXQ.js +0 -102
- package/dist/framer-chunks/default-blog-sqlite-7ZHEY3GT-TVGUYU6H.js +0 -7
- package/dist/framer-chunks/fontshare-4THNDPMZ-FLLTWCDO.js +0 -7
- package/dist/framer-chunks/fontshare-B2QLD7YB-5V3XZJAH.js +0 -7
- package/dist/framer-chunks/fontshare-O22OBJ3D-FIG3CRN3.js +0 -7
- package/dist/framer-chunks/framer-font-45AI7UCZ-CKGC4MJC.js +0 -7
- package/dist/framer-chunks/google-3FCAKCAC-AM34UPJK.js +0 -7
- package/dist/framer-chunks/google-3SZHWBC6-2VTQEQ7J.js +0 -7
- package/dist/framer-chunks/google-GXDJLGJB-JNEJGCGD.js +0 -7
- package/dist/framer-chunks/sqlite-wasm-FGP37EAY-NO5QVZDI.js +0 -7
- package/dist/framer-chunks/sqlite3-SISQ6ENZ-5WRKGWTY.js +0 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-handlers.d.ts","sourceRoot":"","sources":["../../../src/plugin-mcp-dist/lib/mcp-handlers.js"],"names":[],"mappings":"AAYA,OAAO,EAA6B,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,CAAC;AAgDpB,wBAAgB,sBAAsB,CAAC,SAAS,KAAA;;EAa/C;
|
|
1
|
+
{"version":3,"file":"mcp-handlers.d.ts","sourceRoot":"","sources":["../../../src/plugin-mcp-dist/lib/mcp-handlers.js"],"names":[],"mappings":"AAYA,OAAO,EAA6B,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,CAAC;AAgDpB,wBAAgB,sBAAsB,CAAC,SAAS,KAAA;;EAa/C;AAqUD,wBAAsB,cAAc,CAAC,EAAE,KAAK,EAAE,IAAI,EAAG;;;CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsmDpD"}
|
|
@@ -7,7 +7,7 @@ import dedent from 'string-dedent';
|
|
|
7
7
|
import { createPatch } from 'diff';
|
|
8
8
|
import { createSpiceflowFetch } from 'spiceflow/client';
|
|
9
9
|
import { framerLayersTreeToXml, extractObjectsFromXmlContent, TEMP_NODE_ID_PREFIX, } from './xml.js';
|
|
10
|
-
import { getFramerTree, applyAttributes, getComponentPropertyControls } from './framer.js';
|
|
10
|
+
import { getFramerTree, applyAttributes, getComponentPropertyControls, createNodeResolver, createChildIndex, } from './framer.js';
|
|
11
11
|
import { processReactExportData } from './react-export.js';
|
|
12
12
|
import { propControlsToTypedocComments, componentCamelCase } from 'unframer';
|
|
13
13
|
import { codeComponentsResourceUri, mcpTools } from './schema.js';
|
|
@@ -186,8 +186,8 @@ function stripVersionFromUrl(url) {
|
|
|
186
186
|
return atIndex !== -1 ? url.substring(0, atIndex) : url;
|
|
187
187
|
}
|
|
188
188
|
// Helper function to get XML for a node
|
|
189
|
-
async function getNodeXml(nodeId, maxCharacters = 15000) {
|
|
190
|
-
const node = await
|
|
189
|
+
async function getNodeXml(nodeId, maxCharacters = 15000, resolver = createNodeResolver()) {
|
|
190
|
+
const node = await resolver.get(nodeId);
|
|
191
191
|
if (!node) {
|
|
192
192
|
return null;
|
|
193
193
|
}
|
|
@@ -217,13 +217,52 @@ async function getAddedNodesDuring(callback) {
|
|
|
217
217
|
const newNodes = childrenAfter.filter((child) => !idsBefore.has(child.id));
|
|
218
218
|
return newNodes;
|
|
219
219
|
}
|
|
220
|
-
//
|
|
221
|
-
|
|
220
|
+
// Framer silently ignores structural writes that target a partially loaded scope:
|
|
221
|
+
// `setParent` resolves without moving anything and `createFrameNode(attrs, parentId)`
|
|
222
|
+
// drops the new node under the canvas root instead of the requested parent. Both
|
|
223
|
+
// only work for the page or component currently open on the canvas. Always verify
|
|
224
|
+
// the parent afterwards so the tool reports a real failure instead of pretending.
|
|
225
|
+
async function offScopeHint() {
|
|
226
|
+
const canvasRootId = await framer
|
|
227
|
+
.getCanvasRoot()
|
|
228
|
+
.then((root) => root.id)
|
|
229
|
+
.catch(() => undefined);
|
|
230
|
+
const open = canvasRootId
|
|
231
|
+
? `the page or component currently open on the canvas (${canvasRootId})`
|
|
232
|
+
: 'the page or component currently open on the canvas';
|
|
233
|
+
return `Framer can only move and create layers inside ${open}, and this node is not in it. Open that page in Framer, then retry.`;
|
|
234
|
+
}
|
|
235
|
+
async function assertParent({ nodeId, parentId, children, }) {
|
|
236
|
+
// Reads the parent fresh: a failed read throws instead of being read as "the move
|
|
237
|
+
// did not land", so callers never destroy or re-move a correctly placed node.
|
|
238
|
+
children.invalidate();
|
|
239
|
+
const childIds = await children.get(parentId);
|
|
240
|
+
if (childIds.includes(nodeId)) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
throw new Error(`Framer did not move node ${nodeId} into parent ${parentId}. ${await offScopeHint()}`);
|
|
244
|
+
}
|
|
245
|
+
async function setParentOrThrow({ nodeId, parentId, index, children, }) {
|
|
246
|
+
await framer.setParent(nodeId, parentId, index);
|
|
247
|
+
await assertParent({ nodeId, parentId, children });
|
|
248
|
+
}
|
|
249
|
+
// Helper function to create a new Framer node based on its type.
|
|
250
|
+
// `onCreated` fires as soon as Framer hands back a node id, before parenting and
|
|
251
|
+
// verification, so the caller can roll the node back if a later step throws. Without
|
|
252
|
+
// it a failed setParent leaves an orphan layer on the canvas root.
|
|
253
|
+
async function createFramerNode({ extractedNode, parentId, resolver, children, onCreated, }) {
|
|
222
254
|
const { nodeType, attributes, newContent } = extractedNode;
|
|
223
255
|
switch (nodeType) {
|
|
224
256
|
case 'Frame': {
|
|
225
257
|
const newFrame = await framer.createFrameNode(attributes, parentId);
|
|
226
|
-
|
|
258
|
+
if (!newFrame) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
onCreated(newFrame.id);
|
|
262
|
+
// createFrameNode ignores parentId outside the loaded scope and drops the
|
|
263
|
+
// node on the canvas root instead, so the placement has to be checked.
|
|
264
|
+
await assertParent({ nodeId: newFrame.id, parentId, children });
|
|
265
|
+
return { id: newFrame.id, type: 'Frame' };
|
|
227
266
|
}
|
|
228
267
|
case 'Text': {
|
|
229
268
|
// Text nodes need special handling
|
|
@@ -233,11 +272,12 @@ async function createFramerNode({ extractedNode, parentId, }) {
|
|
|
233
272
|
});
|
|
234
273
|
const newNodeId = newNodes[0]?.id;
|
|
235
274
|
if (newNodeId) {
|
|
275
|
+
onCreated(newNodeId);
|
|
236
276
|
// Move to correct parent
|
|
237
|
-
await
|
|
277
|
+
await setParentOrThrow({ nodeId: newNodeId, parentId, children });
|
|
238
278
|
// Apply attributes if any
|
|
239
279
|
if (Object.keys(attributes).length > 0) {
|
|
240
|
-
const node = await
|
|
280
|
+
const node = await resolver.get(newNodeId);
|
|
241
281
|
await applyAttributes(node, attributes);
|
|
242
282
|
}
|
|
243
283
|
return { id: newNodeId, type: 'Text' };
|
|
@@ -252,14 +292,15 @@ async function createFramerNode({ extractedNode, parentId, }) {
|
|
|
252
292
|
});
|
|
253
293
|
const newNodeId = newNodes[0]?.id;
|
|
254
294
|
if (newNodeId) {
|
|
295
|
+
onCreated(newNodeId);
|
|
255
296
|
// Move to correct parent
|
|
256
|
-
await
|
|
297
|
+
await setParentOrThrow({ nodeId: newNodeId, parentId, children });
|
|
257
298
|
// Apply remaining attributes
|
|
258
299
|
const remainingAttrs = { ...attributes };
|
|
259
300
|
delete remainingAttrs.svg;
|
|
260
301
|
delete remainingAttrs.name;
|
|
261
302
|
if (Object.keys(remainingAttrs).length > 0) {
|
|
262
|
-
const node = await
|
|
303
|
+
const node = await resolver.get(newNodeId);
|
|
263
304
|
await applyAttributes(node, remainingAttrs);
|
|
264
305
|
}
|
|
265
306
|
return { id: newNodeId, type: 'SVG' };
|
|
@@ -271,7 +312,7 @@ async function createFramerNode({ extractedNode, parentId, }) {
|
|
|
271
312
|
// If no insertUrl, try to find it from componentId
|
|
272
313
|
if (!insertUrl && attributes.componentId) {
|
|
273
314
|
// First try to get it as a component node
|
|
274
|
-
const node = await
|
|
315
|
+
const node = await resolver.get(attributes.componentId);
|
|
275
316
|
if (node && isComponentNode(node) && node.insertURL) {
|
|
276
317
|
insertUrl = node.insertURL;
|
|
277
318
|
}
|
|
@@ -328,8 +369,9 @@ async function createFramerNode({ extractedNode, parentId, }) {
|
|
|
328
369
|
nodeId = instance.id;
|
|
329
370
|
nodeType = 'ComponentInstance';
|
|
330
371
|
}
|
|
372
|
+
onCreated(nodeId);
|
|
331
373
|
// Move to correct parent
|
|
332
|
-
await
|
|
374
|
+
await setParentOrThrow({ nodeId, parentId, children });
|
|
333
375
|
return { id: nodeId, type: nodeType };
|
|
334
376
|
}
|
|
335
377
|
default:
|
|
@@ -356,6 +398,24 @@ function checkPermissions(...methods) {
|
|
|
356
398
|
}
|
|
357
399
|
// Websocket handler function
|
|
358
400
|
export async function mcpToolHandler({ input, type, }) {
|
|
401
|
+
// Validate input against zod schema before processing.
|
|
402
|
+
// Without this, malformed input (e.g., wrong parameter names, missing fields)
|
|
403
|
+
// causes cryptic runtime errors deep in handler code — like htmlparser2 crashing
|
|
404
|
+
// with "Cannot read properties of undefined (reading 'length')" when the `xml`
|
|
405
|
+
// parameter is missing from updateXmlForNode.
|
|
406
|
+
const tool = mcpTools[type];
|
|
407
|
+
if (tool?.input) {
|
|
408
|
+
const result = tool.input.safeParse(input);
|
|
409
|
+
if (!result.success) {
|
|
410
|
+
const issues = result.error.issues
|
|
411
|
+
.map((i) => {
|
|
412
|
+
return `- ${i.path.join('.')}: ${i.message}`;
|
|
413
|
+
})
|
|
414
|
+
.join('\n');
|
|
415
|
+
return `Invalid input for tool "${type}":\n${issues}\n\nExpected parameters: ${Object.keys(tool.input.shape || {}).join(', ')}`;
|
|
416
|
+
}
|
|
417
|
+
input = result.data;
|
|
418
|
+
}
|
|
359
419
|
switch (type) {
|
|
360
420
|
case 'getNodeXml': {
|
|
361
421
|
const { nodeId } = input;
|
|
@@ -568,27 +628,58 @@ export async function mcpToolHandler({ input, type, }) {
|
|
|
568
628
|
if (rootNodeId.startsWith('/')) {
|
|
569
629
|
return `Node ID cannot start with a slash. It should be a valid node ID, not a color style or text path. To update styles use 'manageColorStyle' or 'manageTextStyle' tools.`;
|
|
570
630
|
}
|
|
571
|
-
|
|
572
|
-
|
|
631
|
+
const resolver = createNodeResolver();
|
|
632
|
+
const children = createChildIndex(resolver);
|
|
633
|
+
// Structural writes (setParent, createFrameNode with a parentId) silently
|
|
634
|
+
// do nothing outside the loaded canvas root, and navigating is the only way
|
|
635
|
+
// to load another scope. Only navigate when the target really is out of
|
|
636
|
+
// scope: navigation takes over the page the user is looking at, and Framer
|
|
637
|
+
// may restart the plugin in another mode. A direct getNode hit proves the
|
|
638
|
+
// node is in the loaded scope, so the common case never navigates.
|
|
639
|
+
const isRootInLoadedScope = Boolean(await framer.getNode(rootNodeId));
|
|
640
|
+
if (isRootInLoadedScope) {
|
|
641
|
+
if (zoomIntoView) {
|
|
642
|
+
try {
|
|
643
|
+
await framer.zoomIntoView(rootNodeId, { maxZoom: 0.9 });
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
// Don't fail the entire operation if zooming fails
|
|
647
|
+
console.warn('Failed to zoom into view:', error);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
// navigateTo does not exist in server-api mode (no canvas to move), so
|
|
653
|
+
// this is a no-op there and structural writes fail loudly instead.
|
|
573
654
|
try {
|
|
574
|
-
await framer.
|
|
655
|
+
await framer.navigateTo(rootNodeId, {
|
|
656
|
+
select: false,
|
|
657
|
+
zoomIntoView: zoomIntoView ? { maxZoom: 0.9 } : false,
|
|
658
|
+
});
|
|
575
659
|
}
|
|
576
660
|
catch (error) {
|
|
577
|
-
|
|
578
|
-
console.warn('Failed to zoom into view:', error);
|
|
661
|
+
console.warn('Failed to navigate to node:', error);
|
|
579
662
|
}
|
|
580
663
|
}
|
|
581
664
|
// Get the original XML before making changes
|
|
582
|
-
const originalResult = await getNodeXml(rootNodeId, Infinity);
|
|
665
|
+
const originalResult = await getNodeXml(rootNodeId, Infinity, resolver);
|
|
583
666
|
const originalXml = originalResult?.xml || '';
|
|
584
667
|
// Extract nodes from the provided XML with node creation enabled
|
|
585
668
|
const extractedNodes = extractObjectsFromXmlContent(xml, {
|
|
586
669
|
enableNodeCreation: true,
|
|
587
670
|
});
|
|
588
671
|
const results = [];
|
|
672
|
+
// Nodes whose attributes already held the requested values. Kept apart
|
|
673
|
+
// from `results` so the final message can say "nothing to change" instead
|
|
674
|
+
// of blaming made up attributes.
|
|
675
|
+
const alreadyUpToDateNodeIds = [];
|
|
589
676
|
const nodesToReorder = [];
|
|
590
677
|
// Phase 0: Create new nodes (nodes with temp IDs)
|
|
591
678
|
const tempIdToRealId = new Map();
|
|
679
|
+
// Every node Framer actually created, recorded before parenting so a
|
|
680
|
+
// half-created node is still rolled back. tempIdToRealId only gets an entry
|
|
681
|
+
// once creation fully succeeded, so it cannot drive the rollback.
|
|
682
|
+
const createdNodeIds = [];
|
|
592
683
|
for (const extractedNode of extractedNodes) {
|
|
593
684
|
// Check if this is a new node (has temp ID)
|
|
594
685
|
if (extractedNode.nodeId.startsWith(TEMP_NODE_ID_PREFIX)) {
|
|
@@ -603,6 +694,11 @@ export async function mcpToolHandler({ input, type, }) {
|
|
|
603
694
|
const newNode = await createFramerNode({
|
|
604
695
|
extractedNode,
|
|
605
696
|
parentId,
|
|
697
|
+
resolver,
|
|
698
|
+
children,
|
|
699
|
+
onCreated: (nodeId) => {
|
|
700
|
+
createdNodeIds.push(nodeId);
|
|
701
|
+
},
|
|
606
702
|
});
|
|
607
703
|
if (newNode) {
|
|
608
704
|
// Map temp ID to real ID
|
|
@@ -642,10 +738,10 @@ export async function mcpToolHandler({ input, type, }) {
|
|
|
642
738
|
await framer.notify(`Failed to create ${extractedNode.nodeType || 'node'}: ${errorMessage}`, {
|
|
643
739
|
variant: 'error',
|
|
644
740
|
});
|
|
645
|
-
// Rollback
|
|
646
|
-
for (const nodeId of
|
|
741
|
+
// Rollback every node Framer created during this call
|
|
742
|
+
for (const nodeId of createdNodeIds) {
|
|
647
743
|
try {
|
|
648
|
-
const node = await
|
|
744
|
+
const node = await resolver.get(nodeId);
|
|
649
745
|
if (node) {
|
|
650
746
|
await node.remove();
|
|
651
747
|
}
|
|
@@ -667,9 +763,9 @@ export async function mcpToolHandler({ input, type, }) {
|
|
|
667
763
|
}
|
|
668
764
|
const targetNodeId = extractedNode.nodeId || rootNodeId;
|
|
669
765
|
try {
|
|
670
|
-
const node = await
|
|
766
|
+
const node = await resolver.get(targetNodeId);
|
|
671
767
|
if (!node) {
|
|
672
|
-
results.push(`
|
|
768
|
+
results.push(`Failed to process node ${targetNodeId}: no node with this ID exists in the project.`);
|
|
673
769
|
continue;
|
|
674
770
|
}
|
|
675
771
|
// Update text content for text nodes
|
|
@@ -680,17 +776,27 @@ export async function mcpToolHandler({ input, type, }) {
|
|
|
680
776
|
// Apply attributes
|
|
681
777
|
if (extractedNode.attributes &&
|
|
682
778
|
Object.keys(extractedNode.attributes).length > 0) {
|
|
683
|
-
await applyAttributes(node, extractedNode.attributes);
|
|
684
|
-
|
|
779
|
+
const changedKeys = await applyAttributes(node, extractedNode.attributes);
|
|
780
|
+
if (changedKeys.length > 0) {
|
|
781
|
+
results.push(`Updated attributes for node ${targetNodeId}: ${changedKeys.join(', ')}`);
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
alreadyUpToDateNodeIds.push(targetNodeId);
|
|
785
|
+
}
|
|
685
786
|
}
|
|
686
|
-
// Check if parent needs to change
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
787
|
+
// Check if parent needs to change. A failed children read throws
|
|
788
|
+
// out of this block instead of being read as "not a child", which
|
|
789
|
+
// would move nodes that are already in the right place.
|
|
790
|
+
if (extractedNode.parentId) {
|
|
791
|
+
const siblingIds = await children.get(extractedNode.parentId);
|
|
792
|
+
if (!siblingIds.includes(targetNodeId)) {
|
|
691
793
|
// Move to new parent without specifying position yet
|
|
692
|
-
await
|
|
693
|
-
|
|
794
|
+
await setParentOrThrow({
|
|
795
|
+
nodeId: targetNodeId,
|
|
796
|
+
parentId: extractedNode.parentId,
|
|
797
|
+
children,
|
|
798
|
+
});
|
|
799
|
+
results.push(`Moved node ${targetNodeId} into ${extractedNode.parentId}`);
|
|
694
800
|
}
|
|
695
801
|
// Queue for reordering if sibling info is provided
|
|
696
802
|
if (extractedNode.beforeNodeId ||
|
|
@@ -714,28 +820,25 @@ export async function mcpToolHandler({ input, type, }) {
|
|
|
714
820
|
// been moved yet, and index calculations would be incorrect during the moving process.
|
|
715
821
|
for (const reorderInfo of nodesToReorder) {
|
|
716
822
|
try {
|
|
717
|
-
const
|
|
718
|
-
if (!parent)
|
|
719
|
-
continue;
|
|
720
|
-
const siblings = await parent.getChildren();
|
|
823
|
+
const siblings = await children.get(reorderInfo.parentId);
|
|
721
824
|
let targetIndex;
|
|
722
825
|
if (reorderInfo.beforeNodeId) {
|
|
723
826
|
// Place after the beforeNode
|
|
724
|
-
const beforeIndex = siblings.
|
|
827
|
+
const beforeIndex = siblings.indexOf(reorderInfo.beforeNodeId);
|
|
725
828
|
if (beforeIndex !== -1) {
|
|
726
829
|
targetIndex = beforeIndex + 1;
|
|
727
830
|
}
|
|
728
831
|
}
|
|
729
832
|
else if (reorderInfo.afterNodeId) {
|
|
730
833
|
// Place before the afterNode
|
|
731
|
-
const afterIndex = siblings.
|
|
834
|
+
const afterIndex = siblings.indexOf(reorderInfo.afterNodeId);
|
|
732
835
|
if (afterIndex !== -1) {
|
|
733
836
|
targetIndex = afterIndex;
|
|
734
837
|
}
|
|
735
838
|
}
|
|
736
839
|
if (targetIndex !== undefined) {
|
|
737
840
|
// Get current index
|
|
738
|
-
const currentIndex = siblings.
|
|
841
|
+
const currentIndex = siblings.indexOf(reorderInfo.nodeId);
|
|
739
842
|
// Only reorder if position needs to change
|
|
740
843
|
if (currentIndex !== -1 &&
|
|
741
844
|
currentIndex !== targetIndex) {
|
|
@@ -749,7 +852,12 @@ export async function mcpToolHandler({ input, type, }) {
|
|
|
749
852
|
if (currentIndex < targetIndex) {
|
|
750
853
|
targetIndex -= 1;
|
|
751
854
|
}
|
|
752
|
-
await
|
|
855
|
+
await setParentOrThrow({
|
|
856
|
+
nodeId: reorderInfo.nodeId,
|
|
857
|
+
parentId: reorderInfo.parentId,
|
|
858
|
+
index: targetIndex,
|
|
859
|
+
children,
|
|
860
|
+
});
|
|
753
861
|
results.push(`Reordered node ${reorderInfo.nodeId} within parent ${reorderInfo.parentId} to index ${targetIndex}`);
|
|
754
862
|
}
|
|
755
863
|
}
|
|
@@ -759,13 +867,19 @@ export async function mcpToolHandler({ input, type, }) {
|
|
|
759
867
|
}
|
|
760
868
|
}
|
|
761
869
|
// Get the updated XML for the primary node
|
|
762
|
-
const updatedResult = await getNodeXml(rootNodeId, Infinity);
|
|
870
|
+
const updatedResult = await getNodeXml(rootNodeId, Infinity, resolver);
|
|
763
871
|
const updatedXml = updatedResult?.xml || '';
|
|
764
872
|
// Check if there were actual changes by comparing XML
|
|
765
873
|
const hasChanges = originalXml.trim() !== updatedXml.trim();
|
|
874
|
+
// Some nodes can land while others fail, so a changed XML alone must not be
|
|
875
|
+
// reported as a full success or the agent stops chasing the failures.
|
|
876
|
+
const hasErrors = results.some((r) => r.startsWith('Failed '));
|
|
766
877
|
if (hasChanges && updatedResult) {
|
|
878
|
+
const header = hasErrors
|
|
879
|
+
? 'Partially updated, some operations failed:'
|
|
880
|
+
: 'Successfully updated:';
|
|
767
881
|
const resultMessage = results.length > 0
|
|
768
|
-
?
|
|
882
|
+
? `${header}\n${results.join('\n')}`
|
|
769
883
|
: 'Successfully updated';
|
|
770
884
|
// Create a diff patch showing the changes with more context
|
|
771
885
|
const patch = createPatch('node.xml', originalXml, updatedXml, 'Before', 'After', { context: 20 });
|
|
@@ -775,10 +889,15 @@ export async function mcpToolHandler({ input, type, }) {
|
|
|
775
889
|
: '';
|
|
776
890
|
return `${resultMessage}\n\nXML Changes:\n${patch}${zoomNote}`;
|
|
777
891
|
}
|
|
778
|
-
const hasErrors = results.some((r) => r.startsWith('Failed '));
|
|
779
892
|
if (hasErrors) {
|
|
780
893
|
return `Encountered errors while updating:\n${results.join('\n')}`;
|
|
781
894
|
}
|
|
895
|
+
if (alreadyUpToDateNodeIds.length > 0) {
|
|
896
|
+
return `No changes were made: every attribute already had the requested value on ${alreadyUpToDateNodeIds.join(', ')}. Call getNodeXml to read the current values before deciding what to change.`;
|
|
897
|
+
}
|
|
898
|
+
if (results.length > 0) {
|
|
899
|
+
return `Framer reported these operations but the XML of ${rootNodeId} is unchanged:\n${results.join('\n')}`;
|
|
900
|
+
}
|
|
782
901
|
return 'No changes were made! Make sure you are not using made up attributes, follow the outlined attributes only.';
|
|
783
902
|
}
|
|
784
903
|
case 'zoomIntoView': {
|
|
@@ -1045,7 +1164,7 @@ export async function mcpToolHandler({ input, type, }) {
|
|
|
1045
1164
|
const permissionError = checkPermissions('Node.remove');
|
|
1046
1165
|
if (permissionError)
|
|
1047
1166
|
return permissionError;
|
|
1048
|
-
const node = await
|
|
1167
|
+
const node = await createNodeResolver().get(nodeId);
|
|
1049
1168
|
if (!node) {
|
|
1050
1169
|
return `Node with ID ${nodeId} not found.`;
|
|
1051
1170
|
}
|
|
@@ -1058,24 +1177,30 @@ export async function mcpToolHandler({ input, type, }) {
|
|
|
1058
1177
|
const permissionError = checkPermissions('Node.clone', 'setParent');
|
|
1059
1178
|
if (permissionError)
|
|
1060
1179
|
return permissionError;
|
|
1061
|
-
const
|
|
1180
|
+
const resolver = createNodeResolver();
|
|
1181
|
+
const node = await resolver.get(nodeId);
|
|
1062
1182
|
if (!node) {
|
|
1063
1183
|
return `Node with ID ${nodeId} not found.`;
|
|
1064
1184
|
}
|
|
1065
1185
|
try {
|
|
1186
|
+
// getParent() returns null for nodes outside the loaded canvas scope,
|
|
1187
|
+
// and cloning needs that scope anyway, so say which case it is.
|
|
1066
1188
|
const parent = await node.getParent();
|
|
1067
1189
|
if (!parent) {
|
|
1068
|
-
throw new Error(
|
|
1190
|
+
throw new Error(`Cannot read the parent of ${nodeId}. ${await offScopeHint()}`);
|
|
1069
1191
|
}
|
|
1070
|
-
|
|
1192
|
+
const cloned = await node.clone();
|
|
1071
1193
|
if (!cloned) {
|
|
1072
1194
|
throw new Error('No new node cloned found');
|
|
1073
1195
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1196
|
+
const children = createChildIndex(resolver);
|
|
1197
|
+
await setParentOrThrow({
|
|
1198
|
+
nodeId: cloned.id,
|
|
1199
|
+
parentId: parent.id,
|
|
1200
|
+
children,
|
|
1201
|
+
});
|
|
1202
|
+
const clonedXml = await getNodeXml(cloned.id, undefined, resolver);
|
|
1203
|
+
return `Here is the new node XML:\n\n${clonedXml?.xml || ''}`;
|
|
1079
1204
|
}
|
|
1080
1205
|
catch (error) {
|
|
1081
1206
|
return `Failed to duplicate node ${nodeId}: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
@@ -1242,8 +1367,10 @@ export async function mcpToolHandler({ input, type, }) {
|
|
|
1242
1367
|
try {
|
|
1243
1368
|
// Build array of component info objects
|
|
1244
1369
|
const components = [];
|
|
1245
|
-
// First try as component node
|
|
1246
|
-
|
|
1370
|
+
// First try as component node. Component IDs come from getProjectXml,
|
|
1371
|
+
// which lists them via getNodesWithType, so they routinely point
|
|
1372
|
+
// outside the loaded canvas scope where framer.getNode returns null.
|
|
1373
|
+
const node = await createNodeResolver().get(id);
|
|
1247
1374
|
if (node) {
|
|
1248
1375
|
// Check if it's a component node
|
|
1249
1376
|
if (!isComponentNode(node)) {
|
|
@@ -1810,15 +1937,22 @@ export async function mcpToolHandler({ input, type, }) {
|
|
|
1810
1937
|
// Move existing nodes into the component if provided
|
|
1811
1938
|
if (nodeIds && nodeIds.length > 0) {
|
|
1812
1939
|
const moveResults = [];
|
|
1940
|
+
const resolver = createNodeResolver();
|
|
1941
|
+
const children = createChildIndex(resolver);
|
|
1813
1942
|
let targetIndex = 0;
|
|
1814
1943
|
for (const nodeId of nodeIds) {
|
|
1815
1944
|
try {
|
|
1816
|
-
const node = await
|
|
1945
|
+
const node = await resolver.get(nodeId);
|
|
1817
1946
|
if (!node) {
|
|
1818
1947
|
moveResults.push(`Node ${nodeId} not found, skipped`);
|
|
1819
1948
|
continue;
|
|
1820
1949
|
}
|
|
1821
|
-
await
|
|
1950
|
+
await setParentOrThrow({
|
|
1951
|
+
nodeId,
|
|
1952
|
+
parentId: componentId,
|
|
1953
|
+
index: targetIndex,
|
|
1954
|
+
children,
|
|
1955
|
+
});
|
|
1822
1956
|
targetIndex += 1;
|
|
1823
1957
|
moveResults.push(`Moved node ${nodeId} into component`);
|
|
1824
1958
|
}
|