@nbakka/mcp-appium 3.0.0 → 3.0.2

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 +142 -82
  2. package/package.json +1 -1
package/lib/server.js CHANGED
@@ -794,7 +794,7 @@ tool(
794
794
  }
795
795
  );
796
796
 
797
- // Tool 1: Review Test Cases - Launch HTML UI for approval
797
+ // Fix 2: Updated review_testcases tool with proper JSON handling and open module usage
798
798
  tool(
799
799
  "review_testcases",
800
800
  "Open test cases in browser for manual approval.",
@@ -803,8 +803,6 @@ tool(
803
803
  },
804
804
  async ({ testCases }) => {
805
805
  try {
806
- const express = require('express');
807
- const open = require('open');
808
806
  const app = express();
809
807
  const port = 3001;
810
808
 
@@ -815,20 +813,23 @@ tool(
815
813
  let approvalStatus = 'pending';
816
814
  let finalTestCases = JSON.parse(JSON.stringify(testCases)); // Deep copy
817
815
 
818
- // Serve static files from review-ui folder if it exists
819
- const reviewUiPath = path.join(__dirname, 'review-ui');
820
- try {
821
- await fs.access(reviewUiPath);
822
- app.use('/static', express.static(reviewUiPath));
823
- } catch (err) {
824
- // review-ui folder doesn't exist, continue without it
825
- }
816
+ app.use(express.json({ limit: '10mb' })); // Increase JSON limit
817
+ app.use(express.urlencoded({ extended: true, limit: '10mb' }));
826
818
 
827
- app.use(express.json());
828
- app.use(express.static('public'));
829
-
830
- // Main review page
819
+ // Main review page with fixed JSON handling
831
820
  app.get('/', (req, res) => {
821
+ // Safely stringify testCases to avoid JSON issues
822
+ const safeTestCases = testCases.map((testCase, index) => {
823
+ // Ensure each test case is an array with at least 3 elements
824
+ const safeCase = Array.isArray(testCase) ? testCase : [testCase];
825
+ return [
826
+ String(safeCase[0] || `Test Case ${index + 1}`), // Title
827
+ String(safeCase[1] || ''), // Description/Steps
828
+ String(safeCase[2] || 'New'), // Status
829
+ String(safeCase[3] || '') // Original case (for modifications)
830
+ ];
831
+ });
832
+
832
833
  res.send(`
833
834
  <!DOCTYPE html>
834
835
  <html lang="en">
@@ -902,6 +903,8 @@ tool(
902
903
 
903
904
  .content {
904
905
  padding: 30px;
906
+ max-height: 70vh;
907
+ overflow-y: auto;
905
908
  }
906
909
 
907
910
  .test-case {
@@ -930,7 +933,7 @@ tool(
930
933
  padding: 15px 20px;
931
934
  border-bottom: 1px solid #e9ecef;
932
935
  display: flex;
933
- justify-content: between;
936
+ justify-content: space-between;
934
937
  align-items: center;
935
938
  }
936
939
 
@@ -967,28 +970,35 @@ tool(
967
970
  }
968
971
 
969
972
  .test-case-title {
970
- font-size: 1.1rem;
971
- font-weight: 500;
972
- color: #212529;
973
973
  margin-bottom: 15px;
974
- line-height: 1.5;
975
974
  }
976
975
 
977
- .test-case-title input {
976
+ .test-case-title textarea {
978
977
  width: 100%;
979
978
  border: 1px solid #ced4da;
980
979
  border-radius: 6px;
981
980
  padding: 10px;
982
981
  font-size: 1rem;
983
982
  transition: border-color 0.3s ease;
983
+ min-height: 100px;
984
+ resize: vertical;
985
+ font-family: inherit;
984
986
  }
985
987
 
986
- .test-case-title input:focus {
988
+ .test-case-title textarea:focus {
987
989
  outline: none;
988
990
  border-color: #4facfe;
989
991
  box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.1);
990
992
  }
991
993
 
994
+ .original-case, .remove-case {
995
+ background: #f8f9fa;
996
+ padding: 10px;
997
+ border-radius: 6px;
998
+ margin-bottom: 15px;
999
+ font-size: 0.9rem;
1000
+ }
1001
+
992
1002
  .test-case-actions {
993
1003
  display: flex;
994
1004
  gap: 10px;
@@ -1086,11 +1096,11 @@ tool(
1086
1096
 
1087
1097
  <div class="stats">
1088
1098
  <div class="stat-item">
1089
- <div class="stat-number" id="totalCount">${testCases.length}</div>
1099
+ <div class="stat-number" id="totalCount">${safeTestCases.length}</div>
1090
1100
  <div class="stat-label">Total Test Cases</div>
1091
1101
  </div>
1092
1102
  <div class="stat-item">
1093
- <div class="stat-number" id="activeCount">${testCases.length}</div>
1103
+ <div class="stat-number" id="activeCount">${safeTestCases.length}</div>
1094
1104
  <div class="stat-label">Active</div>
1095
1105
  </div>
1096
1106
  <div class="stat-item">
@@ -1101,22 +1111,35 @@ tool(
1101
1111
 
1102
1112
  <div class="content">
1103
1113
  <div id="testCases">
1104
- ${testCases.map((testCase, index) => `
1114
+ ${safeTestCases.map((testCase, index) => {
1115
+ const escapedContent = testCase[0].replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/\n/g, '\\n');
1116
+ return `
1105
1117
  <div class="test-case" data-index="${index}">
1106
1118
  <div class="test-case-header">
1107
1119
  <span class="test-case-id">Test Case #${index + 1}</span>
1108
- <span class="test-case-status status-${testCase[2] ? testCase[2].toLowerCase() : 'new'}">${testCase[2] || 'New'}</span>
1120
+ <span class="test-case-status status-${testCase[2].toLowerCase()}">${testCase[2]}</span>
1109
1121
  </div>
1110
1122
  <div class="test-case-content">
1111
1123
  <div class="test-case-title">
1112
- <input type="text" value="${testCase[0]}" data-index="${index}">
1124
+ <textarea data-index="${index}" placeholder="Enter test case title and steps...">${escapedContent}</textarea>
1113
1125
  </div>
1126
+ ${testCase[2] === 'Modify' && testCase[3] ? `
1127
+ <div class="original-case">
1128
+ <small><strong>Original:</strong> ${testCase[3]}</small>
1129
+ </div>
1130
+ ` : ''}
1131
+ ${testCase[2] === 'Remove' ? `
1132
+ <div class="remove-case">
1133
+ <small><strong>Marked for Removal</strong></small>
1134
+ </div>
1135
+ ` : ''}
1114
1136
  <div class="test-case-actions">
1115
1137
  <button class="btn btn-delete" onclick="toggleDelete(${index})">Delete</button>
1116
1138
  </div>
1117
1139
  </div>
1118
1140
  </div>
1119
- `).join('')}
1141
+ `;
1142
+ }).join('')}
1120
1143
  </div>
1121
1144
  </div>
1122
1145
 
@@ -1129,7 +1152,7 @@ tool(
1129
1152
  <div class="notification" id="notification"></div>
1130
1153
 
1131
1154
  <script>
1132
- let testCases = ${JSON.stringify(testCases)};
1155
+ let testCases = ${JSON.stringify(safeTestCases)};
1133
1156
  let deletedIndices = new Set();
1134
1157
 
1135
1158
  function updateStats() {
@@ -1139,7 +1162,7 @@ tool(
1139
1162
 
1140
1163
  function toggleDelete(index) {
1141
1164
  const testCaseEl = document.querySelector(\`[data-index="\${index}"]\`);
1142
- const btn = testCaseEl.querySelector('.btn-delete');
1165
+ const btn = testCaseEl.querySelector('.btn-delete, .btn-restore');
1143
1166
 
1144
1167
  if (deletedIndices.has(index)) {
1145
1168
  // Restore
@@ -1165,9 +1188,9 @@ tool(
1165
1188
  btn.textContent = 'Delete';
1166
1189
  btn.className = 'btn btn-delete';
1167
1190
 
1168
- // Reset input value
1169
- const input = el.querySelector('input');
1170
- input.value = testCases[index][0];
1191
+ // Reset textarea value
1192
+ const textarea = el.querySelector('textarea');
1193
+ textarea.value = testCases[index][0];
1171
1194
  });
1172
1195
  updateStats();
1173
1196
  }
@@ -1184,50 +1207,57 @@ tool(
1184
1207
  }
1185
1208
 
1186
1209
  function approveTestCases() {
1187
- // Collect updated test cases
1188
- const updatedTestCases = [];
1189
- document.querySelectorAll('.test-case').forEach((el, index) => {
1190
- if (!deletedIndices.has(index)) {
1191
- const input = el.querySelector('input');
1192
- const originalTestCase = [...testCases[index]];
1193
- originalTestCase[0] = input.value;
1194
- updatedTestCases.push(originalTestCase);
1195
- }
1196
- });
1197
-
1198
- // Send approval to server
1199
- fetch('/approve', {
1200
- method: 'POST',
1201
- headers: {
1202
- 'Content-Type': 'application/json',
1203
- },
1204
- body: JSON.stringify({
1205
- sessionId: '${sessionId}',
1206
- testCases: updatedTestCases
1210
+ try {
1211
+ // Collect updated test cases
1212
+ const updatedTestCases = [];
1213
+ document.querySelectorAll('.test-case').forEach((el, index) => {
1214
+ if (!deletedIndices.has(index)) {
1215
+ const textarea = el.querySelector('textarea');
1216
+ const originalTestCase = [...testCases[index]];
1217
+ originalTestCase[0] = textarea.value;
1218
+ updatedTestCases.push(originalTestCase);
1219
+ }
1220
+ });
1221
+
1222
+ // Send approval to server
1223
+ fetch('/approve', {
1224
+ method: 'POST',
1225
+ headers: {
1226
+ 'Content-Type': 'application/json',
1227
+ },
1228
+ body: JSON.stringify({
1229
+ sessionId: '${sessionId}',
1230
+ testCases: updatedTestCases
1231
+ })
1207
1232
  })
1208
- })
1209
- .then(response => response.json())
1210
- .then(data => {
1211
- if (data.success) {
1212
- showNotification('Test cases approved successfully!');
1213
- setTimeout(() => {
1214
- window.close();
1215
- }, 2000);
1216
- } else {
1233
+ .then(response => response.json())
1234
+ .then(data => {
1235
+ if (data.success) {
1236
+ showNotification('Test cases approved successfully!');
1237
+ setTimeout(() => {
1238
+ window.close();
1239
+ }, 2000);
1240
+ } else {
1241
+ showNotification('Error approving test cases', 'error');
1242
+ }
1243
+ })
1244
+ .catch(error => {
1217
1245
  showNotification('Error approving test cases', 'error');
1218
- }
1219
- })
1220
- .catch(error => {
1221
- showNotification('Error approving test cases', 'error');
1246
+ console.error('Error:', error);
1247
+ });
1248
+ } catch (error) {
1249
+ showNotification('Error processing test cases', 'error');
1222
1250
  console.error('Error:', error);
1223
- });
1251
+ }
1224
1252
  }
1225
1253
 
1226
- // Update test cases when input changes
1254
+ // Update test cases when textarea changes
1227
1255
  document.addEventListener('input', function(e) {
1228
- if (e.target.tagName === 'INPUT') {
1256
+ if (e.target.tagName === 'TEXTAREA') {
1229
1257
  const index = parseInt(e.target.getAttribute('data-index'));
1230
- testCases[index][0] = e.target.value;
1258
+ if (!isNaN(index) && testCases[index]) {
1259
+ testCases[index][0] = e.target.value;
1260
+ }
1231
1261
  }
1232
1262
  });
1233
1263
  </script>
@@ -1236,10 +1266,15 @@ tool(
1236
1266
  `);
1237
1267
  });
1238
1268
 
1239
- // Approval endpoint
1269
+ // Approval endpoint with better error handling
1240
1270
  app.post('/approve', (req, res) => {
1241
1271
  try {
1242
- const { testCases: approvedTestCases } = req.body;
1272
+ const { testCases: approvedTestCases, sessionId: receivedSessionId } = req.body;
1273
+
1274
+ if (receivedSessionId !== sessionId) {
1275
+ return res.status(400).json({ success: false, message: 'Invalid session ID' });
1276
+ }
1277
+
1243
1278
  finalTestCases = approvedTestCases;
1244
1279
  approvalStatus = 'approved';
1245
1280
 
@@ -1258,6 +1293,7 @@ tool(
1258
1293
  server.close();
1259
1294
  }, 3000);
1260
1295
  } catch (error) {
1296
+ console.error('Approval error:', error);
1261
1297
  res.status(500).json({ success: false, message: error.message });
1262
1298
  }
1263
1299
  });
@@ -1267,8 +1303,26 @@ tool(
1267
1303
  console.log(`Test case review server running at http://localhost:${port}`);
1268
1304
  });
1269
1305
 
1270
- // Open browser
1271
- await open(`http://localhost:${port}`);
1306
+ // Handle server errors
1307
+ server.on('error', (error) => {
1308
+ if (error.code === 'EADDRINUSE') {
1309
+ // Try a different port
1310
+ const newPort = port + Math.floor(Math.random() * 100);
1311
+ return app.listen(newPort, () => {
1312
+ console.log(`Test case review server running at http://localhost:${newPort}`);
1313
+ open(`http://localhost:${newPort}`).catch(err => console.error('Failed to open browser:', err));
1314
+ });
1315
+ }
1316
+ throw error;
1317
+ });
1318
+
1319
+ // Open browser with proper error handling
1320
+ try {
1321
+ await open(`http://localhost:${port}`);
1322
+ } catch (openError) {
1323
+ console.error('Failed to open browser automatically:', openError.message);
1324
+ // Continue without opening browser - user can manually navigate to the URL
1325
+ }
1272
1326
 
1273
1327
  // Store session globally for status checking
1274
1328
  global.approvalSessions = global.approvalSessions || {};
@@ -1278,15 +1332,18 @@ tool(
1278
1332
  timestamp: Date.now()
1279
1333
  };
1280
1334
 
1281
- return `✅ Test case review session started. Session ID: ${sessionId}. Browser opened for manual review and approval.`;
1335
+ return `✅ Test case review session started. Session ID: ${sessionId}.
1336
+ Server running at http://localhost:${port}
1337
+ ${openError ? 'Please manually open the URL in your browser.' : 'Browser should open automatically.'}`;
1282
1338
 
1283
1339
  } catch (err) {
1340
+ console.error('Review tool error:', err);
1284
1341
  return `❌ Error starting test case review: ${err.message}`;
1285
1342
  }
1286
1343
  }
1287
1344
  );
1288
1345
 
1289
- // Tool 2: Check Approval Status
1346
+ // Fix 3: Updated check_approval_status tool with better error handling
1290
1347
  tool(
1291
1348
  "check_approval_status",
1292
1349
  "Check the approval status of test cases review session (waits 25 seconds before checking)",
@@ -1313,25 +1370,28 @@ tool(
1313
1370
  sessionId: sessionId
1314
1371
  };
1315
1372
 
1373
+ // Format the approved test cases properly
1374
+ const formattedTestCases = result.testCases.map((tc, index) => {
1375
+ const title = Array.isArray(tc) ? tc[0] : String(tc);
1376
+ const status = Array.isArray(tc) && tc[2] ? ` (${tc[2]})` : '';
1377
+ return `${index + 1}. ${title}${status}`;
1378
+ }).join('\n');
1379
+
1316
1380
  // Clean up session after returning result
1317
1381
  delete global.approvalSessions[sessionId];
1318
1382
 
1319
- return `✅ Test cases approved successfully!\n\nApproved ${result.approvedCount} test cases:\n\n${
1320
- result.testCases.map((tc, index) =>
1321
- `${index + 1}. ${tc[0]}${tc[2] ? ` (${tc[2]})` : ''}`
1322
- ).join('\n')
1323
- }\n\nSession ID: ${sessionId}`;
1383
+ return `✅ Test cases approved successfully!\n\nApproved ${result.approvedCount} test cases:\n\n${formattedTestCases}\n\nSession completed: ${sessionId}`;
1324
1384
  } else {
1325
1385
  return "⏳ Still waiting for approval. The review session is active but not yet approved. Please complete the review in the browser.";
1326
1386
  }
1327
1387
 
1328
1388
  } catch (err) {
1389
+ console.error('Check approval status error:', err);
1329
1390
  return `❌ Error checking approval status: ${err.message}`;
1330
1391
  }
1331
1392
  }
1332
1393
  );
1333
1394
 
1334
-
1335
1395
  return server;
1336
1396
  };
1337
1397
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbakka/mcp-appium",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"