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