@nbakka/mcp-appium 2.0.44 → 2.0.45
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 +200 -238
- package/package.json +1 -1
package/lib/server.js
CHANGED
|
@@ -474,280 +474,242 @@ const createMcpServer = () => {
|
|
|
474
474
|
return [...new Set(figmaLinks)];
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
477
|
+
tool(
|
|
478
|
+
"mobile_export_figma_images",
|
|
479
|
+
"Export only main parent frames from Figma file as PNG images",
|
|
480
|
+
{
|
|
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)")
|
|
483
|
+
},
|
|
484
|
+
async ({ figmaUrl, nodeId }) => {
|
|
485
|
+
try {
|
|
486
|
+
// Read Figma credentials from desktop/figma.json file
|
|
487
|
+
const figmaConfigPath = path.join(os.homedir(), 'Desktop', 'figma.json');
|
|
488
|
+
|
|
489
|
+
let figmaConfig;
|
|
490
|
+
try {
|
|
491
|
+
const configContent = await fs.readFile(figmaConfigPath, 'utf-8');
|
|
492
|
+
figmaConfig = JSON.parse(configContent);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
throw new Error(`Failed to read Figma config from ${figmaConfigPath}: ${error.message}`);
|
|
495
|
+
}
|
|
488
496
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const configContent = await fs.readFile(figmaConfigPath, 'utf-8');
|
|
492
|
-
figmaConfig = JSON.parse(configContent);
|
|
493
|
-
} catch (error) {
|
|
494
|
-
throw new Error(`Failed to read Figma config from ${figmaConfigPath}: ${error.message}`);
|
|
495
|
-
}
|
|
497
|
+
// Extract API token from config
|
|
498
|
+
const { token: figmaToken } = figmaConfig;
|
|
496
499
|
|
|
497
|
-
|
|
498
|
-
|
|
500
|
+
if (!figmaToken) {
|
|
501
|
+
throw new Error('Figma API token not found in figma.json file. Please ensure the file contains "token" field.');
|
|
502
|
+
}
|
|
499
503
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
504
|
+
// Extract file ID and node ID from Figma URL
|
|
505
|
+
const fileId = extractFileIdFromUrl(figmaUrl);
|
|
506
|
+
if (!fileId) {
|
|
507
|
+
throw new Error('Invalid Figma URL. Unable to extract file ID.');
|
|
508
|
+
}
|
|
503
509
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
510
|
+
// Extract node ID from URL if not provided separately
|
|
511
|
+
let targetNodeId = nodeId;
|
|
512
|
+
if (!targetNodeId) {
|
|
513
|
+
targetNodeId = extractNodeIdFromUrl(figmaUrl);
|
|
514
|
+
}
|
|
509
515
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if (!targetNodeId) {
|
|
513
|
-
targetNodeId = extractNodeIdFromUrl(figmaUrl);
|
|
514
|
-
}
|
|
516
|
+
// Set up export directory
|
|
517
|
+
const exportPath = path.join(os.homedir(), 'Desktop', 'figma');
|
|
515
518
|
|
|
516
|
-
|
|
517
|
-
|
|
519
|
+
// Create export directory if it doesn't exist
|
|
520
|
+
await fs.mkdir(exportPath, { recursive: true });
|
|
518
521
|
|
|
519
|
-
|
|
520
|
-
|
|
522
|
+
// Clear existing PNG files in the directory
|
|
523
|
+
try {
|
|
524
|
+
const existingFiles = await fs.readdir(exportPath);
|
|
525
|
+
const pngFiles = existingFiles.filter(file => file.endsWith('.png'));
|
|
526
|
+
for (const file of pngFiles) {
|
|
527
|
+
await fs.unlink(path.join(exportPath, file));
|
|
528
|
+
}
|
|
529
|
+
} catch (cleanupError) {
|
|
530
|
+
// Continue if cleanup fails
|
|
531
|
+
}
|
|
521
532
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
} catch (cleanupError) {
|
|
530
|
-
// Continue if cleanup fails
|
|
533
|
+
// Get file information
|
|
534
|
+
const fileResponse = await axios.get(
|
|
535
|
+
`https://api.figma.com/v1/files/${fileId}`,
|
|
536
|
+
{
|
|
537
|
+
headers: {
|
|
538
|
+
'X-Figma-Token': figmaToken
|
|
531
539
|
}
|
|
540
|
+
}
|
|
541
|
+
);
|
|
532
542
|
|
|
533
|
-
|
|
534
|
-
const fileResponse = await axios.get(
|
|
535
|
-
`https://api.figma.com/v1/files/${fileId}`,
|
|
536
|
-
{
|
|
537
|
-
headers: {
|
|
538
|
-
'X-Figma-Token': figmaToken
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
);
|
|
542
|
-
|
|
543
|
-
const fileData = fileResponse.data;
|
|
543
|
+
const fileData = fileResponse.data;
|
|
544
544
|
|
|
545
|
-
|
|
545
|
+
let framesToExport = [];
|
|
546
546
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
547
|
+
if (targetNodeId) {
|
|
548
|
+
// If specific node ID provided, find that node and export it
|
|
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
|
+
// Get only top-level frames (direct children of pages)
|
|
557
|
+
framesToExport = extractMainFrames(fileData.document);
|
|
558
|
+
}
|
|
559
559
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
560
|
+
if (framesToExport.length === 0) {
|
|
561
|
+
return `No main frames found in Figma file.`;
|
|
562
|
+
}
|
|
563
563
|
|
|
564
|
-
|
|
565
|
-
|
|
564
|
+
// Limit to reasonable number to avoid API issues
|
|
565
|
+
const limitedFrameIds = framesToExport.slice(0, 10);
|
|
566
566
|
|
|
567
|
-
|
|
568
|
-
|
|
567
|
+
let successCount = 0;
|
|
568
|
+
const batchSize = 5; // Smaller batches for main frames
|
|
569
569
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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);
|
|
574
574
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
}
|
|
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
|
|
616
587
|
}
|
|
617
|
-
} catch (batchError) {
|
|
618
|
-
// Continue with next batch if one fails
|
|
619
588
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
return `Figma Export Complete!
|
|
623
|
-
Export Path: ${exportPath}
|
|
624
|
-
Total main frames found: ${framesToExport.length}
|
|
625
|
-
Exported: ${successCount} main frames as PNG files
|
|
626
|
-
Files: ${Array.from({length: successCount}, (_, i) => `${i + 1}.png`).join(', ')}`;
|
|
589
|
+
);
|
|
627
590
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
+
}
|
|
635
616
|
}
|
|
617
|
+
} catch (batchError) {
|
|
618
|
+
// Continue with next batch if one fails
|
|
636
619
|
}
|
|
637
620
|
}
|
|
638
|
-
);
|
|
639
621
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
622
|
+
return `Figma Export Complete!
|
|
623
|
+
Export Path: ${exportPath}
|
|
624
|
+
Total main frames found: ${framesToExport.length}
|
|
625
|
+
Exported: ${successCount} main frames as PNG files
|
|
626
|
+
Files: ${Array.from({length: successCount}, (_, i) => `${i + 1}.png`).join(', ')}`;
|
|
627
|
+
|
|
628
|
+
} catch (error) {
|
|
629
|
+
if (error.response && error.response.status === 403) {
|
|
630
|
+
return `Error: Access denied. Please check your Figma API token and file permissions.`;
|
|
631
|
+
} else if (error.response && error.response.status === 404) {
|
|
632
|
+
return `Error: Figma file not found. Please check the URL and ensure the file is accessible.`;
|
|
633
|
+
} else {
|
|
634
|
+
return `Error exporting from Figma: ${error.message}`;
|
|
653
635
|
}
|
|
654
|
-
return null;
|
|
655
636
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
637
|
+
}
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
// Helper function to extract file ID from Figma URL
|
|
641
|
+
function extractFileIdFromUrl(url) {
|
|
642
|
+
const patterns = [
|
|
643
|
+
/figma\.com\/file\/([a-zA-Z0-9]+)/,
|
|
644
|
+
/figma\.com\/design\/([a-zA-Z0-9]+)/,
|
|
645
|
+
/figma\.com\/proto\/([a-zA-Z0-9]+)/
|
|
646
|
+
];
|
|
647
|
+
|
|
648
|
+
for (const pattern of patterns) {
|
|
649
|
+
const match = url.match(pattern);
|
|
650
|
+
if (match) {
|
|
651
|
+
return match[1];
|
|
664
652
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
return foundNode;
|
|
653
|
+
}
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
|
|
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;
|
|
669
|
+
|
|
670
|
+
function traverse(node) {
|
|
671
|
+
if (node.id === nodeId) {
|
|
672
|
+
foundNode = node;
|
|
673
|
+
return;
|
|
690
674
|
}
|
|
691
675
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
}
|
|
676
|
+
if (node.children && Array.isArray(node.children)) {
|
|
677
|
+
node.children.forEach(traverse);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
713
680
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
681
|
+
if (document.children && Array.isArray(document.children)) {
|
|
682
|
+
document.children.forEach(page => {
|
|
683
|
+
if (page.children && Array.isArray(page.children)) {
|
|
684
|
+
page.children.forEach(traverse);
|
|
718
685
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return foundNode;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Simple function to extract only top-level frames (direct children of pages)
|
|
693
|
+
function extractMainFrames(document) {
|
|
694
|
+
const topLevelFrames = [];
|
|
695
|
+
|
|
696
|
+
// Only get direct children of pages - no traversing deeper
|
|
697
|
+
if (document.children && Array.isArray(document.children)) {
|
|
698
|
+
document.children.forEach(page => {
|
|
699
|
+
if (page.children && Array.isArray(page.children)) {
|
|
700
|
+
// Only look at direct children of the page
|
|
701
|
+
page.children.forEach(child => {
|
|
702
|
+
if (child.type === 'FRAME') {
|
|
703
|
+
topLevelFrames.push(child.id);
|
|
725
704
|
}
|
|
726
705
|
});
|
|
727
706
|
}
|
|
707
|
+
});
|
|
708
|
+
}
|
|
728
709
|
|
|
729
|
-
|
|
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
|
-
}
|
|
710
|
+
return topLevelFrames;
|
|
711
|
+
}
|
|
748
712
|
|
|
749
|
-
return count;
|
|
750
|
-
}
|
|
751
713
|
|
|
752
714
|
return server;
|
|
753
715
|
};
|