@nbakka/mcp-appium 2.0.44 → 2.0.46

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 +92 -255
  2. package/package.json +1 -1
package/lib/server.js CHANGED
@@ -474,280 +474,117 @@ 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');
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
- }
496
-
497
- // Extract API token from config
498
- const { token: figmaToken } = figmaConfig;
499
-
500
- if (!figmaToken) {
501
- throw new Error('Figma API token not found in figma.json file. Please ensure the file contains "token" field.');
502
- }
503
-
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
- }
509
-
510
- // Extract node ID from URL if not provided separately
511
- let targetNodeId = nodeId;
512
- if (!targetNodeId) {
513
- targetNodeId = extractNodeIdFromUrl(figmaUrl);
514
- }
515
-
516
- // Set up export directory
517
- const exportPath = path.join(os.homedir(), 'Desktop', 'figma');
518
-
519
- // Create export directory if it doesn't exist
520
- await fs.mkdir(exportPath, { recursive: true });
521
-
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
- }
532
-
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;
544
-
545
- let framesToExport = [];
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
- }
559
-
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);
566
-
567
- let successCount = 0;
568
- const batchSize = 5; // Smaller batches for main frames
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);
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
- }
616
- }
617
- } catch (batchError) {
618
- // Continue with next batch if one fails
619
- }
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(', ')}`;
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}`;
635
- }
636
- }
477
+ tool(
478
+ "mobile_export_figma_images",
479
+ "Export Figma file as PDF",
480
+ {
481
+ figmaUrl: zod_1.z.string().describe("The Figma file URL to export as PDF")
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}`);
637
494
  }
638
- );
639
495
 
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
- }
653
- }
654
- return null;
655
- }
496
+ // Extract API token from config
497
+ const { token: figmaToken } = figmaConfig;
656
498
 
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];
499
+ if (!figmaToken) {
500
+ throw new Error('Figma API token not found in figma.json file. Please ensure the file contains "token" field.');
662
501
  }
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
502
 
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
- });
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.');
687
507
  }
688
508
 
689
- return foundNode;
690
- }
509
+ // Set up export directory
510
+ const exportPath = path.join(os.homedir(), 'Desktop', 'figma');
691
511
 
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
- }
512
+ // Create export directory if it doesn't exist
513
+ await fs.mkdir(exportPath, { recursive: true });
713
514
 
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));
515
+ // Clear existing PDF files in the directory
516
+ try {
517
+ const existingFiles = await fs.readdir(exportPath);
518
+ const pdfFiles = existingFiles.filter(file => file.endsWith('.pdf'));
519
+ for (const file of pdfFiles) {
520
+ await fs.unlink(path.join(exportPath, file));
717
521
  }
522
+ } catch (cleanupError) {
523
+ // Continue if cleanup fails
718
524
  }
719
525
 
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));
526
+ // Request PDF export of entire file
527
+ const exportResponse = await axios.get(
528
+ `https://api.figma.com/v1/images/${fileId}`,
529
+ {
530
+ headers: {
531
+ 'X-Figma-Token': figmaToken
532
+ },
533
+ params: {
534
+ format: 'pdf'
725
535
  }
726
- });
727
- }
728
-
729
- return mainFrames;
730
- }
536
+ }
537
+ );
731
538
 
732
- // Helper function to count child frames (to detect component libraries)
733
- function countChildFrames(node) {
734
- let count = 0;
539
+ const pdfUrl = exportResponse.data.images[fileId];
735
540
 
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
- }
541
+ if (!pdfUrl) {
542
+ throw new Error('Failed to get PDF export URL from Figma');
743
543
  }
744
544
 
745
- if (node.children && Array.isArray(node.children)) {
746
- node.children.forEach(traverse);
545
+ // Download PDF
546
+ const pdfResponse = await axios.get(pdfUrl, {
547
+ responseType: 'arraybuffer'
548
+ });
549
+
550
+ // Save PDF to file with timestamp
551
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
552
+ const pdfPath = path.join(exportPath, `figma-export-${timestamp}.pdf`);
553
+ await fs.writeFile(pdfPath, pdfResponse.data);
554
+
555
+ return `Figma PDF Export Complete!
556
+ Export Path: ${pdfPath}
557
+ File: figma-export-${timestamp}.pdf`;
558
+
559
+ } catch (error) {
560
+ if (error.response && error.response.status === 403) {
561
+ return `Error: Access denied. Please check your Figma API token and file permissions.`;
562
+ } else if (error.response && error.response.status === 404) {
563
+ return `Error: Figma file not found. Please check the URL and ensure the file is accessible.`;
564
+ } else {
565
+ return `Error exporting from Figma: ${error.message}`;
747
566
  }
748
-
749
- return count;
750
567
  }
568
+ }
569
+ );
570
+
571
+ // Helper function to extract file ID from Figma URL
572
+ function extractFileIdFromUrl(url) {
573
+ const patterns = [
574
+ /figma\.com\/file\/([a-zA-Z0-9]+)/,
575
+ /figma\.com\/design\/([a-zA-Z0-9]+)/,
576
+ /figma\.com\/proto\/([a-zA-Z0-9]+)/
577
+ ];
578
+
579
+ for (const pattern of patterns) {
580
+ const match = url.match(pattern);
581
+ if (match) {
582
+ return match[1];
583
+ }
584
+ }
585
+ return null;
586
+ }
587
+
751
588
 
752
589
  return server;
753
590
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbakka/mcp-appium",
3
- "version": "2.0.44",
3
+ "version": "2.0.46",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"