@nbakka/mcp-appium 2.0.45 → 2.0.47
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 +87 -151
- package/package.json +1 -1
package/lib/server.js
CHANGED
|
@@ -476,12 +476,11 @@ const createMcpServer = () => {
|
|
|
476
476
|
|
|
477
477
|
tool(
|
|
478
478
|
"mobile_export_figma_images",
|
|
479
|
-
"Export
|
|
479
|
+
"Export Figma file as PDF",
|
|
480
480
|
{
|
|
481
|
-
figmaUrl: zod_1.z.string().describe("The Figma file URL to export
|
|
482
|
-
nodeId: zod_1.z.string().optional().describe("Specific node ID from URL (e.g., 13223-1724)")
|
|
481
|
+
figmaUrl: zod_1.z.string().describe("The Figma file URL to export as PDF")
|
|
483
482
|
},
|
|
484
|
-
async ({ figmaUrl
|
|
483
|
+
async ({ figmaUrl }) => {
|
|
485
484
|
try {
|
|
486
485
|
// Read Figma credentials from desktop/figma.json file
|
|
487
486
|
const figmaConfigPath = path.join(os.homedir(), 'Desktop', 'figma.json');
|
|
@@ -501,36 +500,30 @@ tool(
|
|
|
501
500
|
throw new Error('Figma API token not found in figma.json file. Please ensure the file contains "token" field.');
|
|
502
501
|
}
|
|
503
502
|
|
|
504
|
-
// Extract file ID
|
|
503
|
+
// Extract file ID from Figma URL
|
|
505
504
|
const fileId = extractFileIdFromUrl(figmaUrl);
|
|
506
505
|
if (!fileId) {
|
|
507
506
|
throw new Error('Invalid Figma URL. Unable to extract file ID.');
|
|
508
507
|
}
|
|
509
508
|
|
|
510
|
-
// Extract node ID from URL if not provided separately
|
|
511
|
-
let targetNodeId = nodeId;
|
|
512
|
-
if (!targetNodeId) {
|
|
513
|
-
targetNodeId = extractNodeIdFromUrl(figmaUrl);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
509
|
// Set up export directory
|
|
517
510
|
const exportPath = path.join(os.homedir(), 'Desktop', 'figma');
|
|
518
511
|
|
|
519
512
|
// Create export directory if it doesn't exist
|
|
520
513
|
await fs.mkdir(exportPath, { recursive: true });
|
|
521
514
|
|
|
522
|
-
// Clear existing
|
|
515
|
+
// Clear existing PDF files in the directory
|
|
523
516
|
try {
|
|
524
517
|
const existingFiles = await fs.readdir(exportPath);
|
|
525
|
-
const
|
|
526
|
-
for (const file of
|
|
518
|
+
const pdfFiles = existingFiles.filter(file => file.endsWith('.pdf'));
|
|
519
|
+
for (const file of pdfFiles) {
|
|
527
520
|
await fs.unlink(path.join(exportPath, file));
|
|
528
521
|
}
|
|
529
522
|
} catch (cleanupError) {
|
|
530
523
|
// Continue if cleanup fails
|
|
531
524
|
}
|
|
532
525
|
|
|
533
|
-
//
|
|
526
|
+
// First, get the file structure to find all top-level frames
|
|
534
527
|
const fileResponse = await axios.get(
|
|
535
528
|
`https://api.figma.com/v1/files/${fileId}`,
|
|
536
529
|
{
|
|
@@ -542,97 +535,98 @@ tool(
|
|
|
542
535
|
|
|
543
536
|
const fileData = fileResponse.data;
|
|
544
537
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
framesToExport = extractMainFrames(fileData.document);
|
|
538
|
+
// Extract all top-level frame IDs
|
|
539
|
+
const frameIds = [];
|
|
540
|
+
if (fileData.document.children && Array.isArray(fileData.document.children)) {
|
|
541
|
+
fileData.document.children.forEach(page => {
|
|
542
|
+
if (page.children && Array.isArray(page.children)) {
|
|
543
|
+
page.children.forEach(child => {
|
|
544
|
+
if (child.type === 'FRAME') {
|
|
545
|
+
frameIds.push(child.id);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
});
|
|
558
550
|
}
|
|
559
551
|
|
|
560
|
-
if (
|
|
561
|
-
|
|
552
|
+
if (frameIds.length === 0) {
|
|
553
|
+
throw new Error('No frames found in the Figma file');
|
|
562
554
|
}
|
|
563
555
|
|
|
564
|
-
|
|
565
|
-
const limitedFrameIds = framesToExport.slice(0, 10);
|
|
556
|
+
console.log(`Found ${frameIds.length} top-level frames:`, frameIds.slice(0, 5));
|
|
566
557
|
|
|
567
|
-
|
|
568
|
-
const
|
|
558
|
+
// Request PDF export with specific frame IDs
|
|
559
|
+
const exportResponse = await axios.get(
|
|
560
|
+
`https://api.figma.com/v1/images/${fileId}`,
|
|
561
|
+
{
|
|
562
|
+
headers: {
|
|
563
|
+
'X-Figma-Token': figmaToken
|
|
564
|
+
},
|
|
565
|
+
params: {
|
|
566
|
+
ids: frameIds.join(','),
|
|
567
|
+
format: 'pdf',
|
|
568
|
+
use_absolute_bounds: false
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
);
|
|
569
572
|
|
|
570
|
-
|
|
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);
|
|
573
|
+
console.log('Export response:', exportResponse.data);
|
|
574
574
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
params: {
|
|
584
|
-
ids: batchIds.join(','),
|
|
585
|
-
format: 'png',
|
|
586
|
-
scale: 2
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
);
|
|
575
|
+
// For PDF export, the response structure might be different
|
|
576
|
+
let pdfUrl = null;
|
|
577
|
+
if (exportResponse.data.images) {
|
|
578
|
+
// Try to get PDF URL - might be under file ID or first frame ID
|
|
579
|
+
pdfUrl = exportResponse.data.images[fileId] ||
|
|
580
|
+
exportResponse.data.images[frameIds[0]] ||
|
|
581
|
+
Object.values(exportResponse.data.images)[0];
|
|
582
|
+
}
|
|
590
583
|
|
|
591
|
-
|
|
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
|
-
}
|
|
584
|
+
if (!pdfUrl) {
|
|
585
|
+
throw new Error('Failed to get PDF export URL from Figma');
|
|
620
586
|
}
|
|
621
587
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
588
|
+
// Download PDF
|
|
589
|
+
const pdfResponse = await axios.get(pdfUrl, {
|
|
590
|
+
responseType: 'arraybuffer'
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// Save PDF to file with timestamp
|
|
594
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
|
595
|
+
const pdfPath = path.join(exportPath, `figma-export-${timestamp}.pdf`);
|
|
596
|
+
await fs.writeFile(pdfPath, pdfResponse.data);
|
|
597
|
+
|
|
598
|
+
return `Figma PDF Export Complete!
|
|
599
|
+
Export Path: ${pdfPath}
|
|
600
|
+
File: figma-export-${timestamp}.pdf`;
|
|
627
601
|
|
|
628
602
|
} catch (error) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
603
|
+
// Enhanced error logging
|
|
604
|
+
let errorMessage = error.message;
|
|
605
|
+
|
|
606
|
+
if (error.response) {
|
|
607
|
+
const status = error.response.status;
|
|
608
|
+
const responseData = error.response.data;
|
|
609
|
+
|
|
610
|
+
console.error('Figma API Error:', {
|
|
611
|
+
status,
|
|
612
|
+
statusText: error.response.statusText,
|
|
613
|
+
data: responseData,
|
|
614
|
+
url: error.config?.url,
|
|
615
|
+
params: error.config?.params
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
if (status === 400) {
|
|
619
|
+
errorMessage = `Bad Request (400): ${responseData?.message || responseData?.err || 'Invalid request parameters'}. This might be due to PDF export limitations or invalid frame IDs.`;
|
|
620
|
+
} else if (status === 403) {
|
|
621
|
+
return `Error: Access denied. Please check your Figma API token and file permissions.`;
|
|
622
|
+
} else if (status === 404) {
|
|
623
|
+
return `Error: Figma file not found. Please check the URL and ensure the file is accessible.`;
|
|
624
|
+
} else {
|
|
625
|
+
errorMessage = `HTTP ${status}: ${responseData?.message || responseData?.err || error.response.statusText}`;
|
|
626
|
+
}
|
|
635
627
|
}
|
|
628
|
+
|
|
629
|
+
return `Error exporting from Figma: ${errorMessage}`;
|
|
636
630
|
}
|
|
637
631
|
}
|
|
638
632
|
);
|
|
@@ -653,64 +647,6 @@ function extractFileIdFromUrl(url) {
|
|
|
653
647
|
}
|
|
654
648
|
return null;
|
|
655
649
|
}
|
|
656
|
-
|
|
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;
|
|
669
|
-
|
|
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
|
-
});
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
return foundNode;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Simple function to extract only top-level frames (direct children of pages)
|
|
693
|
-
function extractMainFrames(document) {
|
|
694
|
-
const topLevelFrames = [];
|
|
695
|
-
|
|
696
|
-
// Only get direct children of pages - no traversing deeper
|
|
697
|
-
if (document.children && Array.isArray(document.children)) {
|
|
698
|
-
document.children.forEach(page => {
|
|
699
|
-
if (page.children && Array.isArray(page.children)) {
|
|
700
|
-
// Only look at direct children of the page
|
|
701
|
-
page.children.forEach(child => {
|
|
702
|
-
if (child.type === 'FRAME') {
|
|
703
|
-
topLevelFrames.push(child.id);
|
|
704
|
-
}
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
return topLevelFrames;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
|
|
714
650
|
return server;
|
|
715
651
|
};
|
|
716
652
|
|