@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.
- package/lib/server.js +217 -9
- 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
|
|
378
|
-
const figmaLinks =
|
|
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
|
|
435
|
-
function
|
|
436
|
-
if (!
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
|
652
|
+
return frameIds;
|
|
445
653
|
}
|
|
446
654
|
|
|
447
655
|
return server;
|