@nbakka/mcp-appium 2.0.43 → 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.
Files changed (2) hide show
  1. package/lib/server.js +200 -238
  2. 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
- 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');
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
- 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
- }
497
+ // Extract API token from config
498
+ const { token: figmaToken } = figmaConfig;
496
499
 
497
- // Extract API token from config
498
- const { token: figmaToken } = figmaConfig;
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
- if (!figmaToken) {
501
- throw new Error('Figma API token not found in figma.json file. Please ensure the file contains "token" field.');
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
- // 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
- }
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
- // Extract node ID from URL if not provided separately
511
- let targetNodeId = nodeId;
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
- // Set up export directory
517
- const exportPath = path.join(os.homedir(), 'Desktop', 'figma');
519
+ // Create export directory if it doesn't exist
520
+ await fs.mkdir(exportPath, { recursive: true });
518
521
 
519
- // Create export directory if it doesn't exist
520
- await fs.mkdir(exportPath, { recursive: true });
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
- // 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
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
- // 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;
543
+ const fileData = fileResponse.data;
544
544
 
545
- let framesToExport = [];
545
+ let framesToExport = [];
546
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
- }
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
- if (framesToExport.length === 0) {
561
- return `No main frames found in Figma file.`;
562
- }
560
+ if (framesToExport.length === 0) {
561
+ return `No main frames found in Figma file.`;
562
+ }
563
563
 
564
- // Limit to reasonable number to avoid API issues
565
- const limitedFrameIds = framesToExport.slice(0, 10);
564
+ // Limit to reasonable number to avoid API issues
565
+ const limitedFrameIds = framesToExport.slice(0, 10);
566
566
 
567
- let successCount = 0;
568
- const batchSize = 5; // Smaller batches for main frames
567
+ let successCount = 0;
568
+ const batchSize = 5; // Smaller batches for main frames
569
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);
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
- 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
- }
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
- } 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}`;
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
- // 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];
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
- // 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;
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
- // 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;
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
- });
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
- // 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
- }
676
+ if (node.children && Array.isArray(node.children)) {
677
+ node.children.forEach(traverse);
678
+ }
679
+ }
713
680
 
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
- }
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
- // 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));
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
- 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
- }
710
+ return topLevelFrames;
711
+ }
748
712
 
749
- return count;
750
- }
751
713
 
752
714
  return server;
753
715
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbakka/mcp-appium",
3
- "version": "2.0.43",
3
+ "version": "2.0.45",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"