@nbakka/mcp-appium 2.0.40 → 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 +178 -0
- package/package.json +1 -1
package/lib/server.js
CHANGED
|
@@ -474,6 +474,184 @@ const createMcpServer = () => {
|
|
|
474
474
|
return [...new Set(figmaLinks)];
|
|
475
475
|
}
|
|
476
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
|
+
}
|
|
651
|
+
|
|
652
|
+
return frameIds;
|
|
653
|
+
}
|
|
654
|
+
|
|
477
655
|
return server;
|
|
478
656
|
};
|
|
479
657
|
|