@nbakka/mcp-appium 2.0.68 → 2.0.70

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 CHANGED
@@ -509,13 +509,13 @@ function extractFileAndNodeId(url) {
509
509
  }
510
510
 
511
511
  // ----------------------
512
- // TOOL 1: Export Figma to PDF
512
+ // TOOL 1: Export Figma to PNG
513
513
  // ----------------------
514
514
  tool(
515
- "mobile_export_figma_pdf",
516
- "Export Figma file as PDF",
515
+ "mobile_export_figma_png",
516
+ "Export Figma file as PNG",
517
517
  {
518
- figmaUrl: zod_1.z.string().describe("The Figma file URL to export as PDF")
518
+ figmaUrl: zod_1.z.string().describe("The Figma file URL to export as PNG")
519
519
  },
520
520
  async ({ figmaUrl }) => {
521
521
  try {
@@ -552,24 +552,22 @@ tool(
552
552
  throw new Error("No frames found in Figma file");
553
553
  }
554
554
 
555
- // Request PDF export
555
+ // Request PNG export with higher scale for better quality
556
556
  const exportResponse = await axios.get(
557
557
  `https://api.figma.com/v1/images/${fileId}`,
558
558
  {
559
559
  headers: { "X-Figma-Token": figmaToken },
560
- params: { ids: idsToExport.join(","), format: "pdf" }
560
+ params: {
561
+ ids: idsToExport.join(","),
562
+ format: "png",
563
+ scale: "2" // 2x scale for better quality
564
+ }
561
565
  }
562
566
  );
563
567
 
564
- const pdfUrl = Object.values(exportResponse.data.images)[0];
565
- if (!pdfUrl) throw new Error("No PDF export URL returned from Figma");
566
-
567
- // Download PDF
568
- const pdfResponse = await axios.get(pdfUrl, { responseType: "arraybuffer" });
569
-
570
568
  const exportPath = path.join(os.homedir(), "Desktop", "figma");
571
569
 
572
- // Clear the folder before creating new PDF
570
+ // Clear the folder before creating new PNGs
573
571
  try {
574
572
  // Check if folder exists
575
573
  await fs.access(exportPath);
@@ -588,19 +586,35 @@ tool(
588
586
  await fs.mkdir(exportPath, { recursive: true });
589
587
 
590
588
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5);
591
- const pdfPath = path.join(exportPath, `figma-export-${timestamp}.pdf`);
592
- await fs.writeFile(pdfPath, pdfResponse.data);
593
589
 
594
- return `✅ PDF Export Complete: ${pdfPath}`;
590
+ // Download all PNG images
591
+ const downloadPromises = Object.entries(exportResponse.data.images).map(
592
+ async ([nodeId, pngUrl], index) => {
593
+ if (!pngUrl) throw new Error(`No PNG export URL returned for node ${nodeId}`);
594
+
595
+ const pngResponse = await axios.get(pngUrl, { responseType: "arraybuffer" });
596
+ const filename = idsToExport.length > 1
597
+ ? `figma-export-${timestamp}-${index + 1}.png`
598
+ : `figma-export-${timestamp}.png`;
599
+ const pngPath = path.join(exportPath, filename);
600
+
601
+ await fs.writeFile(pngPath, pngResponse.data);
602
+ return pngPath;
603
+ }
604
+ );
605
+
606
+ const savedPaths = await Promise.all(downloadPromises);
607
+
608
+ return `✅ PNG Export Complete: ${savedPaths.length} file(s) saved to ${exportPath}`;
595
609
  } catch (err) {
596
- return `❌ Error exporting Figma PDF: ${err.message}`;
610
+ return `❌ Error exporting Figma PNG: ${err.message}`;
597
611
  }
598
612
  }
599
613
  );
600
614
 
601
615
  tool(
602
- "upload_pdf_to_openai",
603
- "Generate manual test cases by analyzing PDF design with JIRA requirements",
616
+ "generate_testcases_from_ticket_data",
617
+ "Generate manual test cases by analyzing PNG design with JIRA requirements",
604
618
  {
605
619
  jiraSummary: zod_1.z.string().describe("Jira issue summary"),
606
620
  jiraDescription: zod_1.z.string().describe("Jira issue description")
@@ -612,45 +626,73 @@ tool(
612
626
  const configContent = await fs.readFile(openaiConfigPath, "utf-8");
613
627
  const { apiKey } = JSON.parse(configContent);
614
628
 
629
+ // Load test case generation guidelines
630
+ const guidelinesPath = path.join(__dirname, 'testcases-generation-context.txt');
631
+ const guidelines = await fs.readFile(guidelinesPath, "utf-8");
632
+
615
633
  const figmaDir = path.join(os.homedir(), "Desktop", "figma");
616
634
  const files = await fs.readdir(figmaDir);
617
- const pdfFiles = files.filter(file => file.toLowerCase().endsWith('.pdf'));
635
+ const pngFiles = files.filter(file => file.toLowerCase().endsWith('.png'));
618
636
 
619
- if (pdfFiles.length === 0) throw new Error("No PDF files found in figma folder");
637
+ if (pngFiles.length === 0) throw new Error("No PNG files found in figma folder");
620
638
 
621
- const latestPdf = pdfFiles.sort((a, b) => b.localeCompare(a))[0];
622
- const pdfPath = path.join(figmaDir, latestPdf);
639
+ // Get the latest PNG file
640
+ const latestPng = pngFiles.sort((a, b) => b.localeCompare(a))[0];
641
+ const pngPath = path.join(figmaDir, latestPng);
623
642
 
624
643
  const client = new OpenAI({ apiKey });
625
644
 
626
- const file = await client.files.create({
627
- file: fsSync.createReadStream(pdfPath),
628
- purpose: "user_data",
629
- });
645
+ // Convert PNG to base64 for vision API
646
+ const pngBuffer = await fs.readFile(pngPath);
647
+ const base64Image = pngBuffer.toString('base64');
630
648
 
631
649
  const completion = await client.chat.completions.create({
632
- model: "gpt-5",
650
+ model: "gpt-5", // Use vision model for image analysis
633
651
  messages: [{
634
652
  role: "user",
635
- content: [{
636
- type: "file",
637
- file: { file_id: file.id }
638
- }, {
639
- type: "text",
640
- text: `For the attached pdf, OCR the page return a JSON with text, bounding boxes. Line-level boxes, Normalized [0–1] coordinates, origin top-left, Include readingOrder and no confidence, Keep all occurrences as they appear NOTE: Dont ask anymore questions, just give me output. I wont be able to answer`
641
- }]
642
- }]
653
+ content: [
654
+ {
655
+ type: "text",
656
+ text: `Generate comprehensive manual test cases based on the following:
657
+
658
+ JIRA Summary: ${jiraSummary}
659
+
660
+ JIRA Description: ${jiraDescription}
661
+
662
+ Test Case Generation Guidelines:
663
+ ${guidelines}
664
+
665
+ Please analyze the provided Figma design (PNG image) and create detailed manual test cases that cover all the functionality described in the JIRA requirements. Focus on UI/UX changes, user interactions, navigation flows, and edge cases.`
666
+ },
667
+ {
668
+ type: "image_url",
669
+ image_url: {
670
+ url: `data:image/png;base64,${base64Image}`,
671
+ detail: "high"
672
+ }
673
+ }
674
+ ]
675
+ }],
676
+ max_tokens: 10000
643
677
  });
644
678
 
645
679
  const testCases = completion.choices[0].message.content;
646
680
 
647
- return testCases;
681
+ // Save the generated test cases to a file for reference
682
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5);
683
+ const testCasesPath = path.join(figmaDir, `test-cases-${timestamp}.txt`);
684
+ await fs.writeFile(testCasesPath, testCases);
685
+
686
+ return `✅ Test cases generated successfully!\n\nSaved to: ${testCasesPath}\n\n${testCases}`;
648
687
  } catch (err) {
649
- return `Error: ${err.message}`;
688
+ return `❌ Error generating test cases: ${err.message}`;
650
689
  }
651
690
  }
652
691
  );
653
692
 
693
+ // Global state for tracking approval sessions
694
+ let approvalSessions = new Map();
695
+
654
696
  tool(
655
697
  "review_testcases",
656
698
  "Open JSON test cases in browser for manual approval",
@@ -664,193 +706,264 @@ tool(
664
706
  async ({ testCases }) => {
665
707
  try {
666
708
  const express = require('express');
709
+ const sessionId = Date.now().toString();
710
+
711
+ // Initialize session state
712
+ approvalSessions.set(sessionId, {
713
+ status: 'pending',
714
+ testCases: testCases,
715
+ server: null,
716
+ startTime: Date.now()
717
+ });
667
718
 
668
- return new Promise(async (resolve, reject) => {
669
- const app = express();
670
- const port = 3001;
671
- let server;
672
- let userApproved = false;
673
-
674
- // Serve a simple HTML page with test cases
675
- app.get("/", (req, res) => {
676
- let html = `
677
- <html>
678
- <head>
679
- <title>Test Case Approval</title>
680
- <style>
681
- body {
682
- font-family: Arial, sans-serif;
683
- margin: 20px;
684
- background-color: #f5f5f5;
685
- }
686
- .container {
687
- max-width: 1200px;
688
- margin: 0 auto;
689
- background: white;
690
- padding: 20px;
691
- border-radius: 8px;
692
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
693
- }
694
- h1 {
695
- color: #333;
696
- text-align: center;
697
- border-bottom: 2px solid #007cba;
698
- padding-bottom: 10px;
699
- }
700
- .test-case {
701
- margin: 15px 0;
702
- padding: 15px;
703
- border: 1px solid #ddd;
704
- border-radius: 8px;
705
- background: #fafafa;
706
- }
707
- .test-id {
708
- font-weight: bold;
709
- color: #007cba;
710
- font-size: 16px;
711
- }
712
- .test-title {
713
- font-weight: bold;
714
- margin: 5px 0;
715
- color: #333;
716
- }
717
- .test-description {
718
- color: #666;
719
- line-height: 1.4;
720
- }
721
- .button-container {
722
- text-align: center;
723
- margin-top: 30px;
724
- padding-top: 20px;
725
- border-top: 1px solid #ddd;
726
- }
727
- button {
728
- padding: 15px 30px;
729
- background: #28a745;
730
- color: white;
731
- border: none;
732
- border-radius: 5px;
733
- cursor: pointer;
734
- font-size: 16px;
735
- font-weight: bold;
736
- }
737
- button:hover {
738
- background: #218838;
739
- }
740
- .status {
741
- text-align: center;
742
- margin-top: 20px;
743
- font-weight: bold;
744
- display: none;
745
- }
746
- </style>
747
- </head>
748
- <body>
749
- <div class="container">
750
- <h1>Review Test Cases</h1>
751
- <div id="test-cases">
752
- `;
753
-
754
- testCases.forEach(tc => {
755
- html += `
756
- <div class="test-case">
757
- <div class="test-id">${tc.id}</div>
758
- <div class="test-title">${tc.title}</div>
759
- <div class="test-description">${tc.description}</div>
760
- </div>
761
- `;
762
- });
763
-
719
+ const app = express();
720
+ const port = 3001;
721
+
722
+ // Serve a simple HTML page with test cases
723
+ app.get("/", (req, res) => {
724
+ let html = `
725
+ <html>
726
+ <head>
727
+ <title>Test Case Approval</title>
728
+ <style>
729
+ body {
730
+ font-family: Arial, sans-serif;
731
+ margin: 20px;
732
+ background-color: #f5f5f5;
733
+ }
734
+ .container {
735
+ max-width: 1200px;
736
+ margin: 0 auto;
737
+ background: white;
738
+ padding: 20px;
739
+ border-radius: 8px;
740
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
741
+ }
742
+ h1 {
743
+ color: #333;
744
+ text-align: center;
745
+ border-bottom: 2px solid #007cba;
746
+ padding-bottom: 10px;
747
+ }
748
+ .test-case {
749
+ margin: 15px 0;
750
+ padding: 15px;
751
+ border: 1px solid #ddd;
752
+ border-radius: 8px;
753
+ background: #fafafa;
754
+ }
755
+ .test-id {
756
+ font-weight: bold;
757
+ color: #007cba;
758
+ font-size: 16px;
759
+ }
760
+ .test-title {
761
+ font-weight: bold;
762
+ margin: 5px 0;
763
+ color: #333;
764
+ }
765
+ .test-description {
766
+ color: #666;
767
+ line-height: 1.4;
768
+ }
769
+ .button-container {
770
+ text-align: center;
771
+ margin-top: 30px;
772
+ padding-top: 20px;
773
+ border-top: 1px solid #ddd;
774
+ }
775
+ button {
776
+ padding: 15px 30px;
777
+ background: #28a745;
778
+ color: white;
779
+ border: none;
780
+ border-radius: 5px;
781
+ cursor: pointer;
782
+ font-size: 16px;
783
+ font-weight: bold;
784
+ }
785
+ button:hover {
786
+ background: #218838;
787
+ }
788
+ .status {
789
+ text-align: center;
790
+ margin-top: 20px;
791
+ font-weight: bold;
792
+ display: none;
793
+ }
794
+ </style>
795
+ </head>
796
+ <body>
797
+ <div class="container">
798
+ <h1>Review Test Cases</h1>
799
+ <div id="test-cases">
800
+ `;
801
+
802
+ testCases.forEach(tc => {
764
803
  html += `
765
- </div>
766
- <div class="button-container">
767
- <button onclick="approveTestCases()">✓ Approve Test Cases</button>
768
- <div id="status" class="status"></div>
769
- </div>
804
+ <div class="test-case">
805
+ <div class="test-id">${tc.id}</div>
806
+ <div class="test-title">${tc.title}</div>
807
+ <div class="test-description">${tc.description}</div>
770
808
  </div>
771
-
772
- <script>
773
- function approveTestCases() {
774
- document.querySelector('button').disabled = true;
775
- document.querySelector('button').style.background = '#6c757d';
776
- document.querySelector('button').innerHTML = 'Processing...';
777
-
778
- const status = document.getElementById('status');
779
- status.style.display = 'block';
780
- status.style.color = '#28a745';
781
- status.innerHTML = 'Test cases approved! You can close this window.';
782
-
783
- fetch('/approve')
784
- .then(response => response.text())
785
- .then(data => {
786
- status.innerHTML = data + ' You can close this window.';
787
- setTimeout(() => {
788
- window.close();
789
- }, 2000);
790
- })
791
- .catch(error => {
792
- status.style.color = '#dc3545';
793
- status.innerHTML = 'Error: ' + error.message;
794
- });
795
- }
796
- </script>
797
- </body>
798
- </html>
799
809
  `;
800
- res.send(html);
801
810
  });
802
811
 
803
- // Approval endpoint
804
- app.get("/approve", (req, res) => {
805
- userApproved = true;
806
- res.send(" Test cases approved successfully!");
807
-
808
- // Close server after a short delay
809
- setTimeout(() => {
810
- if (server) {
811
- server.close();
812
+ html += `
813
+ </div>
814
+ <div class="button-container">
815
+ <button onclick="approveTestCases()">✓ Approve Test Cases</button>
816
+ <div id="status" class="status"></div>
817
+ </div>
818
+ </div>
819
+
820
+ <script>
821
+ function approveTestCases() {
822
+ document.querySelector('button').disabled = true;
823
+ document.querySelector('button').style.background = '#6c757d';
824
+ document.querySelector('button').innerHTML = 'Processing...';
825
+
826
+ const status = document.getElementById('status');
827
+ status.style.display = 'block';
828
+ status.style.color = '#28a745';
829
+ status.innerHTML = 'Test cases approved! You can close this window.';
830
+
831
+ fetch('/approve/${sessionId}')
832
+ .then(response => response.text())
833
+ .then(data => {
834
+ status.innerHTML = data + ' You can close this window.';
835
+ setTimeout(() => {
836
+ window.close();
837
+ }, 2000);
838
+ })
839
+ .catch(error => {
840
+ status.style.color = '#dc3545';
841
+ status.innerHTML = 'Error: ' + error.message;
842
+ });
812
843
  }
813
- resolve("✓ Test cases have been approved by the user. Continuing with next operations...");
814
- }, 1000);
815
- });
844
+ </script>
845
+ </body>
846
+ </html>
847
+ `;
848
+ res.send(html);
849
+ });
816
850
 
817
- // Start server
818
- server = app.listen(port, async () => {
819
- console.log(`Test case review server started on http://localhost:${port}`);
820
-
821
- try {
822
- // Use dynamic import for the open package (ES module)
823
- const { default: open } = await import('open');
824
- await open(`http://localhost:${port}`);
825
- console.log('Browser opened successfully');
826
- } catch (openError) {
827
- console.log('Failed to open browser automatically:', openError.message);
828
- console.log(`Please manually open: http://localhost:${port}`);
829
- }
830
- });
851
+ // Approval endpoint
852
+ app.get(`/approve/${sessionId}`, (req, res) => {
853
+ const session = approvalSessions.get(sessionId);
854
+ if (session) {
855
+ session.status = 'approved';
856
+ approvalSessions.set(sessionId, session);
857
+ }
858
+ res.send("✓ Test cases approved successfully!");
859
+ });
831
860
 
832
- // Handle server errors
833
- server.on('error', (err) => {
834
- reject(`Server error: ${err.message}`);
835
- });
861
+ // Start server
862
+ const server = app.listen(port, async () => {
863
+ console.log(`Test case review server started on http://localhost:${port}`);
864
+
865
+ try {
866
+ // Use dynamic import for the open package (ES module)
867
+ const { default: open } = await import('open');
868
+ await open(`http://localhost:${port}`);
869
+ console.log('Browser opened successfully');
870
+ } catch (openError) {
871
+ console.log('Failed to open browser automatically:', openError.message);
872
+ console.log(`Please manually open: http://localhost:${port}`);
873
+ }
874
+ });
836
875
 
837
- // Timeout after 10 minutes if user doesn't approve
838
- setTimeout(() => {
839
- if (!userApproved && server) {
840
- server.close();
841
- reject("⚠️ Review session timed out after 10 minutes. Test cases were not approved.");
876
+ // Store server reference in session
877
+ const session = approvalSessions.get(sessionId);
878
+ session.server = server;
879
+ approvalSessions.set(sessionId, session);
880
+
881
+ // Auto cleanup after 5 minutes
882
+ setTimeout(() => {
883
+ const session = approvalSessions.get(sessionId);
884
+ if (session && session.status === 'pending') {
885
+ session.status = 'timeout';
886
+ if (session.server) {
887
+ session.server.close();
842
888
  }
843
- }, 600000); // 10 minutes
889
+ approvalSessions.set(sessionId, session);
890
+ }
891
+ }, 300000); // 5 minutes
892
+
893
+ return JSON.stringify({
894
+ status: "review_started",
895
+ sessionId: sessionId,
896
+ message: "Test case review interface opened in browser. Use check_approval_status tool to poll for approval.",
897
+ testCasesCount: testCases.length,
898
+ browserUrl: `http://localhost:${port}`,
899
+ instructions: "Poll every 10 seconds using check_approval_status tool until approved or timeout (5 minutes)"
844
900
  });
845
901
 
846
902
  } catch (err) {
847
- return `❌ Error setting up review interface: ${err.message}`;
903
+ return JSON.stringify({
904
+ status: "error",
905
+ message: `Error setting up review interface: ${err.message}`
906
+ });
848
907
  }
849
908
  }
850
909
  );
851
910
 
911
+ tool(
912
+ "check_approval_status",
913
+ "Check the approval status of test cases review session",
914
+ {
915
+ sessionId: zod_1.z.string().describe("The session ID returned from review_testcases tool")
916
+ },
917
+ async ({ sessionId }) => {
918
+ const session = approvalSessions.get(sessionId);
852
919
 
920
+ if (!session) {
921
+ return JSON.stringify({
922
+ status: "error",
923
+ message: "Session not found. Invalid session ID."
924
+ });
925
+ }
926
+
927
+ const currentTime = Date.now();
928
+ const elapsedTime = Math.floor((currentTime - session.startTime) / 1000);
929
+
930
+ if (session.status === 'approved') {
931
+ // Clean up session and close server
932
+ if (session.server) {
933
+ session.server.close();
934
+ }
935
+ approvalSessions.delete(sessionId);
853
936
 
937
+ return JSON.stringify({
938
+ status: "approved",
939
+ message: "Test cases have been approved by the user. Continuing with next operations...",
940
+ testCasesCount: session.testCases.length,
941
+ elapsedTime: elapsedTime
942
+ });
943
+ } else if (session.status === 'timeout' || elapsedTime > 300) { // 5 minutes
944
+ // Clean up session and close server
945
+ if (session.server) {
946
+ session.server.close();
947
+ }
948
+ approvalSessions.delete(sessionId);
949
+
950
+ return JSON.stringify({
951
+ status: "timeout",
952
+ message: "Review session timed out after 5 minutes. Test cases were not approved.",
953
+ testCasesCount: session.testCases.length,
954
+ elapsedTime: elapsedTime
955
+ });
956
+ } else {
957
+ return JSON.stringify({
958
+ status: "pending",
959
+ message: "Test cases are still pending approval. Continue polling.",
960
+ testCasesCount: session.testCases.length,
961
+ elapsedTime: elapsedTime,
962
+ remainingTime: Math.max(0, 300 - elapsedTime)
963
+ });
964
+ }
965
+ }
966
+ );
854
967
  return server;
855
968
  };
856
969
 
@@ -0,0 +1,11 @@
1
+ generate test cases using jira summary, description and figma image,
2
+ don't take any extra user input,
3
+ final output should be test in following format
4
+ "Verify pay on credit banner exits on PDP", "Existing"
5
+ "Verify overview tab on PDP", "New"
6
+ "Verify financial services are displayed under financial tab", "Remove"
7
+ "Verify xyz is moved to sja ", "Modify"
8
+ here existing means the test case is already present in the repo
9
+ new means the test case is not present in the repo and needs to be added
10
+ remove means the test case is present in the repo but is not required and needs to be removed
11
+ modify means the test case is present in the repo but needs some modification
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbakka/mcp-appium",
3
- "version": "2.0.68",
3
+ "version": "2.0.70",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"