@nbakka/mcp-appium 2.0.39 → 2.0.41

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 +217 -9
  2. package/package.json +1 -1
package/lib/server.js CHANGED
@@ -374,8 +374,8 @@ const createMcpServer = () => {
374
374
  extractTextFromADF(issue.fields.description) :
375
375
  'No description available';
376
376
 
377
- // Extract Figma links from description
378
- const figmaLinks = extractFigmaLinks(description);
377
+ // Extract Figma links directly from ADF structure
378
+ const figmaLinks = extractFigmaLinksFromADF(issue.fields.description);
379
379
 
380
380
  // Format response
381
381
  const result = {
@@ -431,17 +431,225 @@ const createMcpServer = () => {
431
431
  return text.trim();
432
432
  }
433
433
 
434
- // Helper function to extract Figma links from text
435
- function extractFigmaLinks(text) {
436
- if (!text || typeof text !== 'string') {
434
+ // Helper function to extract Figma links directly from ADF structure
435
+ function extractFigmaLinksFromADF(adfContent) {
436
+ if (!adfContent || typeof adfContent !== 'object') {
437
437
  return [];
438
438
  }
439
439
 
440
- // Regular expression to match Figma URLs
441
- const figmaRegex = /https?:\/\/(?:www\.)?figma\.com\/[^\s<>"{}|\\^`\[\]]+/gi;
442
- const matches = text.match(figmaRegex);
440
+ const figmaLinks = [];
441
+
442
+ function traverse(node) {
443
+ // Check for inlineCard nodes with Figma URLs
444
+ if (node.type === 'inlineCard' && node.attrs && node.attrs.url) {
445
+ const url = node.attrs.url;
446
+ if (url.includes('figma.com')) {
447
+ figmaLinks.push(url);
448
+ }
449
+ }
450
+
451
+ // Check for link marks with Figma URLs
452
+ if (node.marks && Array.isArray(node.marks)) {
453
+ node.marks.forEach(mark => {
454
+ if (mark.type === 'link' && mark.attrs && mark.attrs.href) {
455
+ const href = mark.attrs.href;
456
+ if (href.includes('figma.com')) {
457
+ figmaLinks.push(href);
458
+ }
459
+ }
460
+ });
461
+ }
462
+
463
+ // Traverse child content
464
+ if (node.content && Array.isArray(node.content)) {
465
+ node.content.forEach(traverse);
466
+ }
467
+ }
468
+
469
+ if (adfContent.content) {
470
+ adfContent.content.forEach(traverse);
471
+ }
472
+
473
+ // Remove duplicates and return
474
+ return [...new Set(figmaLinks)];
475
+ }
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
+ console.log('Could not clean existing files:', cleanupError.message);
525
+ }
526
+
527
+ // Get file information
528
+ console.log('Fetching Figma file information...');
529
+ const fileResponse = await axios.get(
530
+ `https://api.figma.com/v1/files/${fileId}`,
531
+ {
532
+ headers: {
533
+ 'X-Figma-Token': figmaToken
534
+ }
535
+ }
536
+ );
537
+
538
+ const fileData = fileResponse.data;
539
+
540
+ // Extract only frame node IDs
541
+ const frameIds = extractFrameNodes(fileData.document);
542
+
543
+ if (frameIds.length === 0) {
544
+ return `No frames found in Figma file.`;
545
+ }
546
+
547
+ console.log(`Found ${frameIds.length} frames. Starting export...`);
548
+
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
+ );
563
+
564
+ const imageUrls = exportResponse.data.images;
565
+ let successCount = 0;
566
+
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
+ });
581
+
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);
589
+ }
590
+ }
591
+ }
592
+
593
+ return `Figma Export Complete!
594
+ Export Path: ${exportPath}
595
+ Successfully exported: ${successCount}/${frameIds.length} frames as PNG files
596
+ Files: ${Array.from({length: successCount}, (_, i) => `${i + 1}.png`).join(', ')}`;
597
+
598
+ } catch (error) {
599
+ if (error.response && error.response.status === 403) {
600
+ return `Error: Access denied. Please check your Figma API token and file permissions.`;
601
+ } else if (error.response && error.response.status === 404) {
602
+ return `Error: Figma file not found. Please check the URL and ensure the file is accessible.`;
603
+ } else {
604
+ return `Error exporting from Figma: ${error.message}`;
605
+ }
606
+ }
607
+ }
608
+ );
609
+
610
+ // Helper function to extract file ID from Figma URL
611
+ function extractFileIdFromUrl(url) {
612
+ const patterns = [
613
+ /figma\.com\/file\/([a-zA-Z0-9]+)/,
614
+ /figma\.com\/design\/([a-zA-Z0-9]+)/,
615
+ /figma\.com\/proto\/([a-zA-Z0-9]+)/
616
+ ];
617
+
618
+ for (const pattern of patterns) {
619
+ const match = url.match(pattern);
620
+ if (match) {
621
+ return match[1];
622
+ }
623
+ }
624
+ return null;
625
+ }
626
+
627
+ // Helper function to extract only frame node IDs
628
+ function extractFrameNodes(document) {
629
+ const frameIds = [];
630
+
631
+ function traverse(node) {
632
+ // Only collect frames, not components
633
+ if (node.type === 'FRAME') {
634
+ frameIds.push(node.id);
635
+ }
636
+
637
+ // Continue traversing children
638
+ if (node.children && Array.isArray(node.children)) {
639
+ node.children.forEach(traverse);
640
+ }
641
+ }
642
+
643
+ // Start traversal from document children (pages)
644
+ if (document.children && Array.isArray(document.children)) {
645
+ document.children.forEach(page => {
646
+ if (page.children && Array.isArray(page.children)) {
647
+ page.children.forEach(traverse);
648
+ }
649
+ });
650
+ }
443
651
 
444
- return matches ? [...new Set(matches)] : []; // Remove duplicates
652
+ return frameIds;
445
653
  }
446
654
 
447
655
  return server;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbakka/mcp-appium",
3
- "version": "2.0.39",
3
+ "version": "2.0.41",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"