@nbakka/mcp-appium 2.0.44 → 2.0.46
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 +92 -255
- package/package.json +1 -1
package/lib/server.js
CHANGED
|
@@ -474,280 +474,117 @@ const createMcpServer = () => {
|
|
|
474
474
|
return [...new Set(figmaLinks)];
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
throw new Error(`Failed to read Figma config from ${figmaConfigPath}: ${error.message}`);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Extract API token from config
|
|
498
|
-
const { token: figmaToken } = figmaConfig;
|
|
499
|
-
|
|
500
|
-
if (!figmaToken) {
|
|
501
|
-
throw new Error('Figma API token not found in figma.json file. Please ensure the file contains "token" field.');
|
|
502
|
-
}
|
|
503
|
-
|
|
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
|
-
}
|
|
509
|
-
|
|
510
|
-
// Extract node ID from URL if not provided separately
|
|
511
|
-
let targetNodeId = nodeId;
|
|
512
|
-
if (!targetNodeId) {
|
|
513
|
-
targetNodeId = extractNodeIdFromUrl(figmaUrl);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Set up export directory
|
|
517
|
-
const exportPath = path.join(os.homedir(), 'Desktop', 'figma');
|
|
518
|
-
|
|
519
|
-
// Create export directory if it doesn't exist
|
|
520
|
-
await fs.mkdir(exportPath, { recursive: true });
|
|
521
|
-
|
|
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
|
-
}
|
|
532
|
-
|
|
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
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
);
|
|
542
|
-
|
|
543
|
-
const fileData = fileResponse.data;
|
|
544
|
-
|
|
545
|
-
let framesToExport = [];
|
|
546
|
-
|
|
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);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
if (framesToExport.length === 0) {
|
|
561
|
-
return `No main frames found in Figma file.`;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Limit to reasonable number to avoid API issues
|
|
565
|
-
const limitedFrameIds = framesToExport.slice(0, 10);
|
|
566
|
-
|
|
567
|
-
let successCount = 0;
|
|
568
|
-
const batchSize = 5; // Smaller batches for main frames
|
|
569
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
616
|
-
}
|
|
617
|
-
} catch (batchError) {
|
|
618
|
-
// Continue with next batch if one fails
|
|
619
|
-
}
|
|
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(', ')}`;
|
|
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}`;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
477
|
+
tool(
|
|
478
|
+
"mobile_export_figma_images",
|
|
479
|
+
"Export Figma file as PDF",
|
|
480
|
+
{
|
|
481
|
+
figmaUrl: zod_1.z.string().describe("The Figma file URL to export as PDF")
|
|
482
|
+
},
|
|
483
|
+
async ({ figmaUrl }) => {
|
|
484
|
+
try {
|
|
485
|
+
// Read Figma credentials from desktop/figma.json file
|
|
486
|
+
const figmaConfigPath = path.join(os.homedir(), 'Desktop', 'figma.json');
|
|
487
|
+
|
|
488
|
+
let figmaConfig;
|
|
489
|
+
try {
|
|
490
|
+
const configContent = await fs.readFile(figmaConfigPath, 'utf-8');
|
|
491
|
+
figmaConfig = JSON.parse(configContent);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
throw new Error(`Failed to read Figma config from ${figmaConfigPath}: ${error.message}`);
|
|
637
494
|
}
|
|
638
|
-
);
|
|
639
495
|
|
|
640
|
-
|
|
641
|
-
|
|
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];
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
return null;
|
|
655
|
-
}
|
|
496
|
+
// Extract API token from config
|
|
497
|
+
const { token: figmaToken } = figmaConfig;
|
|
656
498
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const match = url.match(/node-id=([^&]+)/);
|
|
660
|
-
if (match) {
|
|
661
|
-
return match[1];
|
|
499
|
+
if (!figmaToken) {
|
|
500
|
+
throw new Error('Figma API token not found in figma.json file. Please ensure the file contains "token" field.');
|
|
662
501
|
}
|
|
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
502
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
if (node.children && Array.isArray(node.children)) {
|
|
677
|
-
node.children.forEach(traverse);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
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);
|
|
685
|
-
}
|
|
686
|
-
});
|
|
503
|
+
// Extract file ID from Figma URL
|
|
504
|
+
const fileId = extractFileIdFromUrl(figmaUrl);
|
|
505
|
+
if (!fileId) {
|
|
506
|
+
throw new Error('Invalid Figma URL. Unable to extract file ID.');
|
|
687
507
|
}
|
|
688
508
|
|
|
689
|
-
|
|
690
|
-
|
|
509
|
+
// Set up export directory
|
|
510
|
+
const exportPath = path.join(os.homedir(), 'Desktop', 'figma');
|
|
691
511
|
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
}
|
|
512
|
+
// Create export directory if it doesn't exist
|
|
513
|
+
await fs.mkdir(exportPath, { recursive: true });
|
|
713
514
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
515
|
+
// Clear existing PDF files in the directory
|
|
516
|
+
try {
|
|
517
|
+
const existingFiles = await fs.readdir(exportPath);
|
|
518
|
+
const pdfFiles = existingFiles.filter(file => file.endsWith('.pdf'));
|
|
519
|
+
for (const file of pdfFiles) {
|
|
520
|
+
await fs.unlink(path.join(exportPath, file));
|
|
717
521
|
}
|
|
522
|
+
} catch (cleanupError) {
|
|
523
|
+
// Continue if cleanup fails
|
|
718
524
|
}
|
|
719
525
|
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
526
|
+
// Request PDF export of entire file
|
|
527
|
+
const exportResponse = await axios.get(
|
|
528
|
+
`https://api.figma.com/v1/images/${fileId}`,
|
|
529
|
+
{
|
|
530
|
+
headers: {
|
|
531
|
+
'X-Figma-Token': figmaToken
|
|
532
|
+
},
|
|
533
|
+
params: {
|
|
534
|
+
format: 'pdf'
|
|
725
535
|
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
return mainFrames;
|
|
730
|
-
}
|
|
536
|
+
}
|
|
537
|
+
);
|
|
731
538
|
|
|
732
|
-
|
|
733
|
-
function countChildFrames(node) {
|
|
734
|
-
let count = 0;
|
|
539
|
+
const pdfUrl = exportResponse.data.images[fileId];
|
|
735
540
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
count++;
|
|
739
|
-
}
|
|
740
|
-
if (n.children && Array.isArray(n.children)) {
|
|
741
|
-
n.children.forEach(traverse);
|
|
742
|
-
}
|
|
541
|
+
if (!pdfUrl) {
|
|
542
|
+
throw new Error('Failed to get PDF export URL from Figma');
|
|
743
543
|
}
|
|
744
544
|
|
|
745
|
-
|
|
746
|
-
|
|
545
|
+
// Download PDF
|
|
546
|
+
const pdfResponse = await axios.get(pdfUrl, {
|
|
547
|
+
responseType: 'arraybuffer'
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Save PDF to file with timestamp
|
|
551
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
|
552
|
+
const pdfPath = path.join(exportPath, `figma-export-${timestamp}.pdf`);
|
|
553
|
+
await fs.writeFile(pdfPath, pdfResponse.data);
|
|
554
|
+
|
|
555
|
+
return `Figma PDF Export Complete!
|
|
556
|
+
Export Path: ${pdfPath}
|
|
557
|
+
File: figma-export-${timestamp}.pdf`;
|
|
558
|
+
|
|
559
|
+
} catch (error) {
|
|
560
|
+
if (error.response && error.response.status === 403) {
|
|
561
|
+
return `Error: Access denied. Please check your Figma API token and file permissions.`;
|
|
562
|
+
} else if (error.response && error.response.status === 404) {
|
|
563
|
+
return `Error: Figma file not found. Please check the URL and ensure the file is accessible.`;
|
|
564
|
+
} else {
|
|
565
|
+
return `Error exporting from Figma: ${error.message}`;
|
|
747
566
|
}
|
|
748
|
-
|
|
749
|
-
return count;
|
|
750
567
|
}
|
|
568
|
+
}
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
// Helper function to extract file ID from Figma URL
|
|
572
|
+
function extractFileIdFromUrl(url) {
|
|
573
|
+
const patterns = [
|
|
574
|
+
/figma\.com\/file\/([a-zA-Z0-9]+)/,
|
|
575
|
+
/figma\.com\/design\/([a-zA-Z0-9]+)/,
|
|
576
|
+
/figma\.com\/proto\/([a-zA-Z0-9]+)/
|
|
577
|
+
];
|
|
578
|
+
|
|
579
|
+
for (const pattern of patterns) {
|
|
580
|
+
const match = url.match(pattern);
|
|
581
|
+
if (match) {
|
|
582
|
+
return match[1];
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
|
|
751
588
|
|
|
752
589
|
return server;
|
|
753
590
|
};
|