@nbakka/mcp-appium 2.0.41 → 2.0.43

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 +154 -57
  2. package/package.json +1 -1
package/lib/server.js CHANGED
@@ -476,11 +476,12 @@ const createMcpServer = () => {
476
476
 
477
477
  tool(
478
478
  "mobile_export_figma_images",
479
- "Export all frames from Figma file as PNG images with sequential naming",
479
+ "Export only main parent frames from Figma file as PNG images",
480
480
  {
481
- figmaUrl: zod_1.z.string().describe("The Figma file URL to export frames from")
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)")
482
483
  },
483
- async ({ figmaUrl }) => {
484
+ async ({ figmaUrl, nodeId }) => {
484
485
  try {
485
486
  // Read Figma credentials from desktop/figma.json file
486
487
  const figmaConfigPath = path.join(os.homedir(), 'Desktop', 'figma.json');
@@ -500,12 +501,18 @@ const createMcpServer = () => {
500
501
  throw new Error('Figma API token not found in figma.json file. Please ensure the file contains "token" field.');
501
502
  }
502
503
 
503
- // Extract file ID from Figma URL
504
+ // Extract file ID and node ID from Figma URL
504
505
  const fileId = extractFileIdFromUrl(figmaUrl);
505
506
  if (!fileId) {
506
507
  throw new Error('Invalid Figma URL. Unable to extract file ID.');
507
508
  }
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
+
509
516
  // Set up export directory
510
517
  const exportPath = path.join(os.homedir(), 'Desktop', 'figma');
511
518
 
@@ -521,11 +528,9 @@ const createMcpServer = () => {
521
528
  }
522
529
  } catch (cleanupError) {
523
530
  // Continue if cleanup fails
524
- console.log('Could not clean existing files:', cleanupError.message);
525
531
  }
526
532
 
527
533
  // Get file information
528
- console.log('Fetching Figma file information...');
529
534
  const fileResponse = await axios.get(
530
535
  `https://api.figma.com/v1/files/${fileId}`,
531
536
  {
@@ -537,62 +542,87 @@ const createMcpServer = () => {
537
542
 
538
543
  const fileData = fileResponse.data;
539
544
 
540
- // Extract only frame node IDs
541
- const frameIds = extractFrameNodes(fileData.document);
545
+ let framesToExport = [];
542
546
 
543
- if (frameIds.length === 0) {
544
- return `No frames found in Figma file.`;
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);
545
558
  }
546
559
 
547
- console.log(`Found ${frameIds.length} frames. Starting export...`);
560
+ if (framesToExport.length === 0) {
561
+ return `No main frames found in Figma file.`;
562
+ }
548
563
 
549
- // Request image exports for frames only
550
- const exportResponse = await axios.get(
551
- `https://api.figma.com/v1/images/${fileId}`,
552
- {
553
- headers: {
554
- 'X-Figma-Token': figmaToken
555
- },
556
- params: {
557
- ids: frameIds.join(','),
558
- format: 'png',
559
- scale: 2
560
- }
561
- }
562
- );
564
+ // Limit to reasonable number to avoid API issues
565
+ const limitedFrameIds = framesToExport.slice(0, 10);
563
566
 
564
- const imageUrls = exportResponse.data.images;
565
567
  let successCount = 0;
568
+ const batchSize = 5; // Smaller batches for main frames
566
569
 
567
- // Download and save each image with sequential naming
568
- for (let i = 0; i < frameIds.length; i++) {
569
- const nodeId = frameIds[i];
570
- const imageUrl = imageUrls[nodeId];
571
-
572
- if (imageUrl) {
573
- try {
574
- const filename = `${i + 1}.png`;
575
- const filepath = path.join(exportPath, filename);
576
-
577
- // Download image
578
- const imageResponse = await axios.get(imageUrl, {
579
- responseType: 'arraybuffer'
580
- });
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);
581
574
 
582
- // Save image to file
583
- await fs.writeFile(filepath, imageResponse.data);
584
- successCount++;
585
-
586
- console.log(`Exported: ${filename}`);
587
- } catch (downloadError) {
588
- console.error(`Failed to download frame ${i + 1}:`, downloadError.message);
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
+ }
589
616
  }
617
+ } catch (batchError) {
618
+ // Continue with next batch if one fails
590
619
  }
591
620
  }
592
621
 
593
622
  return `Figma Export Complete!
594
623
  Export Path: ${exportPath}
595
- Successfully exported: ${successCount}/${frameIds.length} frames as PNG files
624
+ Total main frames found: ${framesToExport.length}
625
+ Exported: ${successCount} main frames as PNG files
596
626
  Files: ${Array.from({length: successCount}, (_, i) => `${i + 1}.png`).join(', ')}`;
597
627
 
598
628
  } catch (error) {
@@ -624,23 +654,30 @@ const createMcpServer = () => {
624
654
  return null;
625
655
  }
626
656
 
627
- // Helper function to extract only frame node IDs
628
- function extractFrameNodes(document) {
629
- const frameIds = [];
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;
630
669
 
631
670
  function traverse(node) {
632
- // Only collect frames, not components
633
- if (node.type === 'FRAME') {
634
- frameIds.push(node.id);
671
+ if (node.id === nodeId) {
672
+ foundNode = node;
673
+ return;
635
674
  }
636
675
 
637
- // Continue traversing children
638
676
  if (node.children && Array.isArray(node.children)) {
639
677
  node.children.forEach(traverse);
640
678
  }
641
679
  }
642
680
 
643
- // Start traversal from document children (pages)
644
681
  if (document.children && Array.isArray(document.children)) {
645
682
  document.children.forEach(page => {
646
683
  if (page.children && Array.isArray(page.children)) {
@@ -649,7 +686,67 @@ const createMcpServer = () => {
649
686
  });
650
687
  }
651
688
 
652
- return frameIds;
689
+ return foundNode;
690
+ }
691
+
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
+ }
713
+
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
+ }
718
+ }
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));
725
+ }
726
+ });
727
+ }
728
+
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
+ }
748
+
749
+ return count;
653
750
  }
654
751
 
655
752
  return server;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbakka/mcp-appium",
3
- "version": "2.0.41",
3
+ "version": "2.0.43",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"