@nbakka/mcp-appium 2.0.42 → 2.0.44

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 +108 -23
  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
 
@@ -523,7 +530,6 @@ const createMcpServer = () => {
523
530
  // Continue if cleanup fails
524
531
  }
525
532
 
526
- // Get file information
527
533
  // Get file information
528
534
  const fileResponse = await axios.get(
529
535
  `https://api.figma.com/v1/files/${fileId}`,
@@ -536,18 +542,30 @@ const createMcpServer = () => {
536
542
 
537
543
  const fileData = fileResponse.data;
538
544
 
539
- // Extract only frame node IDs
540
- const frameIds = extractFrameNodes(fileData.document);
545
+ let framesToExport = [];
541
546
 
542
- if (frameIds.length === 0) {
543
- 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);
544
558
  }
545
559
 
546
- // Limit to first 50 frames to avoid URI too long error
547
- const limitedFrameIds = frameIds.slice(0, 50);
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);
548
566
 
549
567
  let successCount = 0;
550
- const batchSize = 20; // Process in smaller batches to avoid 414 error
568
+ const batchSize = 5; // Smaller batches for main frames
551
569
 
552
570
  // Process frames in batches
553
571
  for (let batchStart = 0; batchStart < limitedFrameIds.length; batchStart += batchSize) {
@@ -603,8 +621,8 @@ const createMcpServer = () => {
603
621
 
604
622
  return `Figma Export Complete!
605
623
  Export Path: ${exportPath}
606
- Total frames in file: ${frameIds.length}
607
- Exported first: ${successCount} frames as PNG files (limited to 50 max)
624
+ Total main frames found: ${framesToExport.length}
625
+ Exported: ${successCount} main frames as PNG files
608
626
  Files: ${Array.from({length: successCount}, (_, i) => `${i + 1}.png`).join(', ')}`;
609
627
 
610
628
  } catch (error) {
@@ -636,23 +654,30 @@ const createMcpServer = () => {
636
654
  return null;
637
655
  }
638
656
 
639
- // Helper function to extract only frame node IDs
640
- function extractFrameNodes(document) {
641
- 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;
642
669
 
643
670
  function traverse(node) {
644
- // Only collect frames, not components
645
- if (node.type === 'FRAME') {
646
- frameIds.push(node.id);
671
+ if (node.id === nodeId) {
672
+ foundNode = node;
673
+ return;
647
674
  }
648
675
 
649
- // Continue traversing children
650
676
  if (node.children && Array.isArray(node.children)) {
651
677
  node.children.forEach(traverse);
652
678
  }
653
679
  }
654
680
 
655
- // Start traversal from document children (pages)
656
681
  if (document.children && Array.isArray(document.children)) {
657
682
  document.children.forEach(page => {
658
683
  if (page.children && Array.isArray(page.children)) {
@@ -661,7 +686,67 @@ const createMcpServer = () => {
661
686
  });
662
687
  }
663
688
 
664
- 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;
665
750
  }
666
751
 
667
752
  return server;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbakka/mcp-appium",
3
- "version": "2.0.42",
3
+ "version": "2.0.44",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"