@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.
Files changed (2) hide show
  1. package/lib/server.js +190 -0
  2. 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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbakka/mcp-appium",
3
- "version": "2.0.40",
3
+ "version": "2.0.42",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"