@nbakka/mcp-appium 2.0.41 → 2.0.43
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/lib/server.js +154 -57
- package/package.json +1 -1
package/lib/server.js
CHANGED
|
@@ -476,11 +476,12 @@ const createMcpServer = () => {
|
|
|
476
476
|
|
|
477
477
|
tool(
|
|
478
478
|
"mobile_export_figma_images",
|
|
479
|
-
"Export
|
|
479
|
+
"Export only main parent frames from Figma file as PNG images",
|
|
480
480
|
{
|
|
481
|
-
figmaUrl: zod_1.z.string().describe("The Figma file URL to export frames from")
|
|
481
|
+
figmaUrl: zod_1.z.string().describe("The Figma file URL to export frames from"),
|
|
482
|
+
nodeId: zod_1.z.string().optional().describe("Specific node ID from URL (e.g., 13223-1724)")
|
|
482
483
|
},
|
|
483
|
-
async ({ figmaUrl }) => {
|
|
484
|
+
async ({ figmaUrl, nodeId }) => {
|
|
484
485
|
try {
|
|
485
486
|
// Read Figma credentials from desktop/figma.json file
|
|
486
487
|
const figmaConfigPath = path.join(os.homedir(), 'Desktop', 'figma.json');
|
|
@@ -500,12 +501,18 @@ const createMcpServer = () => {
|
|
|
500
501
|
throw new Error('Figma API token not found in figma.json file. Please ensure the file contains "token" field.');
|
|
501
502
|
}
|
|
502
503
|
|
|
503
|
-
// Extract file ID from Figma URL
|
|
504
|
+
// Extract file ID and node ID from Figma URL
|
|
504
505
|
const fileId = extractFileIdFromUrl(figmaUrl);
|
|
505
506
|
if (!fileId) {
|
|
506
507
|
throw new Error('Invalid Figma URL. Unable to extract file ID.');
|
|
507
508
|
}
|
|
508
509
|
|
|
510
|
+
// Extract node ID from URL if not provided separately
|
|
511
|
+
let targetNodeId = nodeId;
|
|
512
|
+
if (!targetNodeId) {
|
|
513
|
+
targetNodeId = extractNodeIdFromUrl(figmaUrl);
|
|
514
|
+
}
|
|
515
|
+
|
|
509
516
|
// Set up export directory
|
|
510
517
|
const exportPath = path.join(os.homedir(), 'Desktop', 'figma');
|
|
511
518
|
|
|
@@ -521,11 +528,9 @@ const createMcpServer = () => {
|
|
|
521
528
|
}
|
|
522
529
|
} catch (cleanupError) {
|
|
523
530
|
// Continue if cleanup fails
|
|
524
|
-
console.log('Could not clean existing files:', cleanupError.message);
|
|
525
531
|
}
|
|
526
532
|
|
|
527
533
|
// Get file information
|
|
528
|
-
console.log('Fetching Figma file information...');
|
|
529
534
|
const fileResponse = await axios.get(
|
|
530
535
|
`https://api.figma.com/v1/files/${fileId}`,
|
|
531
536
|
{
|
|
@@ -537,62 +542,87 @@ const createMcpServer = () => {
|
|
|
537
542
|
|
|
538
543
|
const fileData = fileResponse.data;
|
|
539
544
|
|
|
540
|
-
|
|
541
|
-
const frameIds = extractFrameNodes(fileData.document);
|
|
545
|
+
let framesToExport = [];
|
|
542
546
|
|
|
543
|
-
if (
|
|
544
|
-
|
|
547
|
+
if (targetNodeId) {
|
|
548
|
+
// If specific node ID provided, find that node and export it (not its children)
|
|
549
|
+
const specificNode = findNodeById(fileData.document, targetNodeId);
|
|
550
|
+
if (specificNode && specificNode.type === 'FRAME') {
|
|
551
|
+
framesToExport = [specificNode.id];
|
|
552
|
+
} else {
|
|
553
|
+
return `Node ID ${targetNodeId} not found or is not a frame.`;
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
// Extract only main/parent frames (large frames, likely screens/pages)
|
|
557
|
+
framesToExport = extractMainFrames(fileData.document);
|
|
545
558
|
}
|
|
546
559
|
|
|
547
|
-
|
|
560
|
+
if (framesToExport.length === 0) {
|
|
561
|
+
return `No main frames found in Figma file.`;
|
|
562
|
+
}
|
|
548
563
|
|
|
549
|
-
//
|
|
550
|
-
const
|
|
551
|
-
`https://api.figma.com/v1/images/${fileId}`,
|
|
552
|
-
{
|
|
553
|
-
headers: {
|
|
554
|
-
'X-Figma-Token': figmaToken
|
|
555
|
-
},
|
|
556
|
-
params: {
|
|
557
|
-
ids: frameIds.join(','),
|
|
558
|
-
format: 'png',
|
|
559
|
-
scale: 2
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
);
|
|
564
|
+
// Limit to reasonable number to avoid API issues
|
|
565
|
+
const limitedFrameIds = framesToExport.slice(0, 10);
|
|
563
566
|
|
|
564
|
-
const imageUrls = exportResponse.data.images;
|
|
565
567
|
let successCount = 0;
|
|
568
|
+
const batchSize = 5; // Smaller batches for main frames
|
|
566
569
|
|
|
567
|
-
//
|
|
568
|
-
for (let
|
|
569
|
-
const
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
if (imageUrl) {
|
|
573
|
-
try {
|
|
574
|
-
const filename = `${i + 1}.png`;
|
|
575
|
-
const filepath = path.join(exportPath, filename);
|
|
576
|
-
|
|
577
|
-
// Download image
|
|
578
|
-
const imageResponse = await axios.get(imageUrl, {
|
|
579
|
-
responseType: 'arraybuffer'
|
|
580
|
-
});
|
|
570
|
+
// Process frames in batches
|
|
571
|
+
for (let batchStart = 0; batchStart < limitedFrameIds.length; batchStart += batchSize) {
|
|
572
|
+
const batchEnd = Math.min(batchStart + batchSize, limitedFrameIds.length);
|
|
573
|
+
const batchIds = limitedFrameIds.slice(batchStart, batchEnd);
|
|
581
574
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
575
|
+
try {
|
|
576
|
+
// Request image exports for current batch
|
|
577
|
+
const exportResponse = await axios.get(
|
|
578
|
+
`https://api.figma.com/v1/images/${fileId}`,
|
|
579
|
+
{
|
|
580
|
+
headers: {
|
|
581
|
+
'X-Figma-Token': figmaToken
|
|
582
|
+
},
|
|
583
|
+
params: {
|
|
584
|
+
ids: batchIds.join(','),
|
|
585
|
+
format: 'png',
|
|
586
|
+
scale: 2
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
const imageUrls = exportResponse.data.images;
|
|
592
|
+
|
|
593
|
+
// Download and save each image in the batch
|
|
594
|
+
for (let i = 0; i < batchIds.length; i++) {
|
|
595
|
+
const nodeId = batchIds[i];
|
|
596
|
+
const imageUrl = imageUrls[nodeId];
|
|
597
|
+
|
|
598
|
+
if (imageUrl) {
|
|
599
|
+
try {
|
|
600
|
+
const frameNumber = batchStart + i + 1;
|
|
601
|
+
const filename = `${frameNumber}.png`;
|
|
602
|
+
const filepath = path.join(exportPath, filename);
|
|
603
|
+
|
|
604
|
+
// Download image
|
|
605
|
+
const imageResponse = await axios.get(imageUrl, {
|
|
606
|
+
responseType: 'arraybuffer'
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// Save image to file
|
|
610
|
+
await fs.writeFile(filepath, imageResponse.data);
|
|
611
|
+
successCount++;
|
|
612
|
+
} catch (downloadError) {
|
|
613
|
+
// Continue with next image if one fails
|
|
614
|
+
}
|
|
615
|
+
}
|
|
589
616
|
}
|
|
617
|
+
} catch (batchError) {
|
|
618
|
+
// Continue with next batch if one fails
|
|
590
619
|
}
|
|
591
620
|
}
|
|
592
621
|
|
|
593
622
|
return `Figma Export Complete!
|
|
594
623
|
Export Path: ${exportPath}
|
|
595
|
-
|
|
624
|
+
Total main frames found: ${framesToExport.length}
|
|
625
|
+
Exported: ${successCount} main frames as PNG files
|
|
596
626
|
Files: ${Array.from({length: successCount}, (_, i) => `${i + 1}.png`).join(', ')}`;
|
|
597
627
|
|
|
598
628
|
} catch (error) {
|
|
@@ -624,23 +654,30 @@ const createMcpServer = () => {
|
|
|
624
654
|
return null;
|
|
625
655
|
}
|
|
626
656
|
|
|
627
|
-
// Helper function to extract
|
|
628
|
-
function
|
|
629
|
-
const
|
|
657
|
+
// Helper function to extract node ID from Figma URL
|
|
658
|
+
function extractNodeIdFromUrl(url) {
|
|
659
|
+
const match = url.match(/node-id=([^&]+)/);
|
|
660
|
+
if (match) {
|
|
661
|
+
return match[1];
|
|
662
|
+
}
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Helper function to find a specific node by ID
|
|
667
|
+
function findNodeById(document, nodeId) {
|
|
668
|
+
let foundNode = null;
|
|
630
669
|
|
|
631
670
|
function traverse(node) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
671
|
+
if (node.id === nodeId) {
|
|
672
|
+
foundNode = node;
|
|
673
|
+
return;
|
|
635
674
|
}
|
|
636
675
|
|
|
637
|
-
// Continue traversing children
|
|
638
676
|
if (node.children && Array.isArray(node.children)) {
|
|
639
677
|
node.children.forEach(traverse);
|
|
640
678
|
}
|
|
641
679
|
}
|
|
642
680
|
|
|
643
|
-
// Start traversal from document children (pages)
|
|
644
681
|
if (document.children && Array.isArray(document.children)) {
|
|
645
682
|
document.children.forEach(page => {
|
|
646
683
|
if (page.children && Array.isArray(page.children)) {
|
|
@@ -649,7 +686,67 @@ const createMcpServer = () => {
|
|
|
649
686
|
});
|
|
650
687
|
}
|
|
651
688
|
|
|
652
|
-
return
|
|
689
|
+
return foundNode;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Helper function to extract only main/parent frames (not small UI components)
|
|
693
|
+
function extractMainFrames(document) {
|
|
694
|
+
const mainFrames = [];
|
|
695
|
+
|
|
696
|
+
function traversePage(node, depth = 0) {
|
|
697
|
+
if (node.type === 'FRAME') {
|
|
698
|
+
// Consider frames as "main" if they are:
|
|
699
|
+
// 1. Top-level frames on a page (depth 0)
|
|
700
|
+
// 2. Large frames (width > 300 or height > 300)
|
|
701
|
+
// 3. Frames that don't have many small children (likely screens, not component libraries)
|
|
702
|
+
|
|
703
|
+
const isTopLevel = depth === 0;
|
|
704
|
+
const isLargeFrame = (node.absoluteBoundingBox?.width > 300) || (node.absoluteBoundingBox?.height > 300);
|
|
705
|
+
const childFrameCount = countChildFrames(node);
|
|
706
|
+
const isNotComponentLibrary = childFrameCount < 50; // Avoid component libraries with many small frames
|
|
707
|
+
|
|
708
|
+
if (isTopLevel || (isLargeFrame && isNotComponentLibrary)) {
|
|
709
|
+
mainFrames.push(node.id);
|
|
710
|
+
return; // Don't traverse children of main frames to avoid getting sub-components
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Continue traversing children for non-main-frame nodes
|
|
715
|
+
if (node.children && Array.isArray(node.children)) {
|
|
716
|
+
node.children.forEach(child => traversePage(child, depth + 1));
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Start traversal from document children (pages)
|
|
721
|
+
if (document.children && Array.isArray(document.children)) {
|
|
722
|
+
document.children.forEach(page => {
|
|
723
|
+
if (page.children && Array.isArray(page.children)) {
|
|
724
|
+
page.children.forEach(child => traversePage(child, 0));
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return mainFrames;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Helper function to count child frames (to detect component libraries)
|
|
733
|
+
function countChildFrames(node) {
|
|
734
|
+
let count = 0;
|
|
735
|
+
|
|
736
|
+
function traverse(n) {
|
|
737
|
+
if (n.type === 'FRAME') {
|
|
738
|
+
count++;
|
|
739
|
+
}
|
|
740
|
+
if (n.children && Array.isArray(n.children)) {
|
|
741
|
+
n.children.forEach(traverse);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (node.children && Array.isArray(node.children)) {
|
|
746
|
+
node.children.forEach(traverse);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return count;
|
|
653
750
|
}
|
|
654
751
|
|
|
655
752
|
return server;
|