@nbakka/mcp-appium 2.0.40 → 2.0.42
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 +190 -0
- package/package.json +1 -1
package/lib/server.js
CHANGED
|
@@ -474,6 +474,196 @@ const createMcpServer = () => {
|
|
|
474
474
|
return [...new Set(figmaLinks)];
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
+
tool(
|
|
478
|
+
"mobile_export_figma_images",
|
|
479
|
+
"Export all frames from Figma file as PNG images with sequential naming",
|
|
480
|
+
{
|
|
481
|
+
figmaUrl: zod_1.z.string().describe("The Figma file URL to export frames from")
|
|
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}`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Extract API token from config
|
|
497
|
+
const { token: figmaToken } = figmaConfig;
|
|
498
|
+
|
|
499
|
+
if (!figmaToken) {
|
|
500
|
+
throw new Error('Figma API token not found in figma.json file. Please ensure the file contains "token" field.');
|
|
501
|
+
}
|
|
502
|
+
|
|
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.');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Set up export directory
|
|
510
|
+
const exportPath = path.join(os.homedir(), 'Desktop', 'figma');
|
|
511
|
+
|
|
512
|
+
// Create export directory if it doesn't exist
|
|
513
|
+
await fs.mkdir(exportPath, { recursive: true });
|
|
514
|
+
|
|
515
|
+
// Clear existing PNG files in the directory
|
|
516
|
+
try {
|
|
517
|
+
const existingFiles = await fs.readdir(exportPath);
|
|
518
|
+
const pngFiles = existingFiles.filter(file => file.endsWith('.png'));
|
|
519
|
+
for (const file of pngFiles) {
|
|
520
|
+
await fs.unlink(path.join(exportPath, file));
|
|
521
|
+
}
|
|
522
|
+
} catch (cleanupError) {
|
|
523
|
+
// Continue if cleanup fails
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Get file information
|
|
527
|
+
// Get file information
|
|
528
|
+
const fileResponse = await axios.get(
|
|
529
|
+
`https://api.figma.com/v1/files/${fileId}`,
|
|
530
|
+
{
|
|
531
|
+
headers: {
|
|
532
|
+
'X-Figma-Token': figmaToken
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const fileData = fileResponse.data;
|
|
538
|
+
|
|
539
|
+
// Extract only frame node IDs
|
|
540
|
+
const frameIds = extractFrameNodes(fileData.document);
|
|
541
|
+
|
|
542
|
+
if (frameIds.length === 0) {
|
|
543
|
+
return `No frames found in Figma file.`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Limit to first 50 frames to avoid URI too long error
|
|
547
|
+
const limitedFrameIds = frameIds.slice(0, 50);
|
|
548
|
+
|
|
549
|
+
let successCount = 0;
|
|
550
|
+
const batchSize = 20; // Process in smaller batches to avoid 414 error
|
|
551
|
+
|
|
552
|
+
// Process frames in batches
|
|
553
|
+
for (let batchStart = 0; batchStart < limitedFrameIds.length; batchStart += batchSize) {
|
|
554
|
+
const batchEnd = Math.min(batchStart + batchSize, limitedFrameIds.length);
|
|
555
|
+
const batchIds = limitedFrameIds.slice(batchStart, batchEnd);
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
// Request image exports for current batch
|
|
559
|
+
const exportResponse = await axios.get(
|
|
560
|
+
`https://api.figma.com/v1/images/${fileId}`,
|
|
561
|
+
{
|
|
562
|
+
headers: {
|
|
563
|
+
'X-Figma-Token': figmaToken
|
|
564
|
+
},
|
|
565
|
+
params: {
|
|
566
|
+
ids: batchIds.join(','),
|
|
567
|
+
format: 'png',
|
|
568
|
+
scale: 2
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
const imageUrls = exportResponse.data.images;
|
|
574
|
+
|
|
575
|
+
// Download and save each image in the batch
|
|
576
|
+
for (let i = 0; i < batchIds.length; i++) {
|
|
577
|
+
const nodeId = batchIds[i];
|
|
578
|
+
const imageUrl = imageUrls[nodeId];
|
|
579
|
+
|
|
580
|
+
if (imageUrl) {
|
|
581
|
+
try {
|
|
582
|
+
const frameNumber = batchStart + i + 1;
|
|
583
|
+
const filename = `${frameNumber}.png`;
|
|
584
|
+
const filepath = path.join(exportPath, filename);
|
|
585
|
+
|
|
586
|
+
// Download image
|
|
587
|
+
const imageResponse = await axios.get(imageUrl, {
|
|
588
|
+
responseType: 'arraybuffer'
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// Save image to file
|
|
592
|
+
await fs.writeFile(filepath, imageResponse.data);
|
|
593
|
+
successCount++;
|
|
594
|
+
} catch (downloadError) {
|
|
595
|
+
// Continue with next image if one fails
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
} catch (batchError) {
|
|
600
|
+
// Continue with next batch if one fails
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return `Figma Export Complete!
|
|
605
|
+
Export Path: ${exportPath}
|
|
606
|
+
Total frames in file: ${frameIds.length}
|
|
607
|
+
Exported first: ${successCount} frames as PNG files (limited to 50 max)
|
|
608
|
+
Files: ${Array.from({length: successCount}, (_, i) => `${i + 1}.png`).join(', ')}`;
|
|
609
|
+
|
|
610
|
+
} catch (error) {
|
|
611
|
+
if (error.response && error.response.status === 403) {
|
|
612
|
+
return `Error: Access denied. Please check your Figma API token and file permissions.`;
|
|
613
|
+
} else if (error.response && error.response.status === 404) {
|
|
614
|
+
return `Error: Figma file not found. Please check the URL and ensure the file is accessible.`;
|
|
615
|
+
} else {
|
|
616
|
+
return `Error exporting from Figma: ${error.message}`;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
// Helper function to extract file ID from Figma URL
|
|
623
|
+
function extractFileIdFromUrl(url) {
|
|
624
|
+
const patterns = [
|
|
625
|
+
/figma\.com\/file\/([a-zA-Z0-9]+)/,
|
|
626
|
+
/figma\.com\/design\/([a-zA-Z0-9]+)/,
|
|
627
|
+
/figma\.com\/proto\/([a-zA-Z0-9]+)/
|
|
628
|
+
];
|
|
629
|
+
|
|
630
|
+
for (const pattern of patterns) {
|
|
631
|
+
const match = url.match(pattern);
|
|
632
|
+
if (match) {
|
|
633
|
+
return match[1];
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Helper function to extract only frame node IDs
|
|
640
|
+
function extractFrameNodes(document) {
|
|
641
|
+
const frameIds = [];
|
|
642
|
+
|
|
643
|
+
function traverse(node) {
|
|
644
|
+
// Only collect frames, not components
|
|
645
|
+
if (node.type === 'FRAME') {
|
|
646
|
+
frameIds.push(node.id);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Continue traversing children
|
|
650
|
+
if (node.children && Array.isArray(node.children)) {
|
|
651
|
+
node.children.forEach(traverse);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Start traversal from document children (pages)
|
|
656
|
+
if (document.children && Array.isArray(document.children)) {
|
|
657
|
+
document.children.forEach(page => {
|
|
658
|
+
if (page.children && Array.isArray(page.children)) {
|
|
659
|
+
page.children.forEach(traverse);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return frameIds;
|
|
665
|
+
}
|
|
666
|
+
|
|
477
667
|
return server;
|
|
478
668
|
};
|
|
479
669
|
|