viewgate-mcp 1.0.61 → 1.0.63

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/dist/index.js +464 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -81,7 +81,11 @@ function createMcpServer(apiKey, personalKey) {
81
81
  return "sync";
82
82
  case "get_synced_endpoints":
83
83
  case "get_ai_resolved_tickets":
84
+ case "get_e2e_flows":
85
+ case "export_e2e_flow":
84
86
  return "idle";
87
+ case "process_e2e_events":
88
+ return "e2e";
85
89
  default:
86
90
  return "idle";
87
91
  }
@@ -126,6 +130,10 @@ function createMcpServer(apiKey, personalKey) {
126
130
  guard.flow = "sync";
127
131
  guard.step = 1;
128
132
  break;
133
+ case "process_e2e_events":
134
+ guard.flow = "e2e";
135
+ guard.step = 1;
136
+ break;
129
137
  default:
130
138
  if (guard.flow === "idle") {
131
139
  throw new Error(`TOOL_CALL_BLOCKED: tool '${toolName}' not allowed in idle. Please start a workflow with 'get_annotations', 'get_ui_components', or 'get_ui_improvements'.`);
@@ -148,10 +156,8 @@ function createMcpServer(apiKey, personalKey) {
148
156
  }
149
157
  else if (toolName === "generate_ui_components") {
150
158
  guard.step = 2;
151
- }
152
- else if (toolName === "mark_ui_component_generated") {
153
- // Allow finishing if it was started in this flow
154
- resetGuard();
159
+ // Step 3 is now terminal, but we reset after success in the handler
160
+ guard.step = 3;
155
161
  }
156
162
  else {
157
163
  throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
@@ -165,8 +171,8 @@ function createMcpServer(apiKey, personalKey) {
165
171
  else if (toolName === "mark_annotations_as_live") {
166
172
  if (guard.step !== 1)
167
173
  throw new Error("TOOL_CALL_BLOCKED: unexpected step");
168
- // Stop here and reset flow as per USER_REQUEST (MCP only reaches 'applied')
169
- resetGuard();
174
+ // Reset moved to success handler
175
+ guard.step = 2;
170
176
  }
171
177
  else {
172
178
  throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
@@ -179,7 +185,7 @@ function createMcpServer(apiKey, personalKey) {
179
185
  else if (toolName === "mark_annotations_as_live") {
180
186
  if (guard.step !== 1)
181
187
  throw new Error("TOOL_CALL_BLOCKED: unexpected step");
182
- resetGuard();
188
+ guard.step = 2;
183
189
  }
184
190
  else {
185
191
  throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
@@ -198,7 +204,8 @@ function createMcpServer(apiKey, personalKey) {
198
204
  else {
199
205
  if (guard.step !== 1)
200
206
  throw new Error("TOOL_CALL_BLOCKED: unexpected step");
201
- resetGuard();
207
+ // Reset moved to success handler
208
+ guard.step = 2;
202
209
  }
203
210
  }
204
211
  else if (guard.flow === "sync") {
@@ -215,6 +222,14 @@ function createMcpServer(apiKey, personalKey) {
215
222
  throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
216
223
  }
217
224
  }
225
+ else if (guard.flow === "e2e") {
226
+ if (toolName === "process_e2e_events") {
227
+ guard.step = 1;
228
+ }
229
+ else {
230
+ throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active e2e flow");
231
+ }
232
+ }
218
233
  guard.lastTool = toolName;
219
234
  guard.lastActivityAt = now;
220
235
  }
@@ -369,6 +384,41 @@ function createMcpServer(apiKey, personalKey) {
369
384
  name: "get_ai_resolved_tickets",
370
385
  description: "Retrieves the list of ticket IDs that have already been resolved by the AI in the current sprint.",
371
386
  inputSchema: { type: "object", properties: {} },
387
+ },
388
+ {
389
+ name: "get_e2e_flows",
390
+ description: "Fetch all E2E test flows for a project. Returns generated test flows from user interactions.",
391
+ inputSchema: {
392
+ type: "object",
393
+ properties: {
394
+ projectId: { type: "string", description: "Project ID to filter flows." },
395
+ status: { type: "string", description: "Filter by status (active, degraded, needs_review, archived).", default: "active" }
396
+ }
397
+ }
398
+ },
399
+ {
400
+ name: "process_e2e_events",
401
+ description: "Process raw DOM events and generate a Test Flow. Transforms captured user interactions into structured E2E tests.",
402
+ inputSchema: {
403
+ type: "object",
404
+ properties: {
405
+ sessionId: { type: "string", description: "Session ID containing the events to process." },
406
+ flowName: { type: "string", description: "Suggested name for the flow (e.g., 'Login Flow')." },
407
+ projectId: { type: "string", description: "Project ID to associate with the flow." }
408
+ },
409
+ required: ["sessionId", "flowName"]
410
+ }
411
+ },
412
+ {
413
+ name: "export_e2e_flow",
414
+ description: "Export a test flow as Playwright code. Generates executable TypeScript test code.",
415
+ inputSchema: {
416
+ type: "object",
417
+ properties: {
418
+ flowId: { type: "string", description: "ID of the flow to export." }
419
+ },
420
+ required: ["flowId"]
421
+ }
372
422
  }
373
423
  ],
374
424
  };
@@ -398,6 +448,7 @@ function createMcpServer(apiKey, personalKey) {
398
448
  const errorBody = await response.text();
399
449
  throw new Error(`Backend responded with ${response.status}: ${errorBody}`);
400
450
  }
451
+ resetGuard();
401
452
  const data = (await response.json());
402
453
  const styleHandler = data?.settings?.styleHandler || 'unknown';
403
454
  const language = data?.settings?.language || 'en';
@@ -489,6 +540,7 @@ function createMcpServer(apiKey, personalKey) {
489
540
  'MUST use Tailwind standard "primary" class (e.g. bg-primary, text-primary-foreground) as these are dynamically injected in the dashboard.',
490
541
  'The theme prop MUST be optional and typed in the component interface.',
491
542
  'Micro-animations MUST be implemented with Framer Motion (use whileHover, whileTap, etc.)',
543
+ 'AESTHETIC PERFECTION: The generated component MUST be an EXACT visual match of the source design. Pay extreme attention to spacing, shadows, and subtle gradients.',
492
544
  ]
493
545
  },
494
546
  constraints: {
@@ -501,7 +553,9 @@ function createMcpServer(apiKey, personalKey) {
501
553
  mustBeProductionReady: true,
502
554
  mustUseFramerMotionForAnimations: true,
503
555
  themingIsMandatory: true,
504
- stylingHandlerIsStrict: true
556
+ stylingHandlerIsStrict: true,
557
+ mustBeAestheticallyIdentical: true,
558
+ visualFidelityInstruction: "You MUST ensure 100% visual parity. The output component must be PREMIUM and indistinguishable from the provided Figma design in terms of layout, spacing, and styling tokens."
505
559
  }
506
560
  };
507
561
  results.push({
@@ -528,6 +582,7 @@ function createMcpServer(apiKey, personalKey) {
528
582
  2) REGISTRAR LA PREVIEW usando 'mark_ui_component_generated'.
529
583
 
530
584
  REQUISITO TECNOLÓGICO OBLIGATORIO: Usar ${styleLabel} para TODOS los estilos.
585
+ REQUISITO DE FIDELIDAD ESTÉTICA: El componente debe ser VISUALMENTE IDÉNTICO al original. Respeta cada píxel de espaciado, sombras y pesos de fuente.
531
586
  REQUISITO DE THEMING: DEBES respetar las themingTokens. NUNCA hardcodees colores (#hex, etc). Usa SIEMPRE la prop 'theme' o el fallback 'var(--vg-*)'.
532
587
  NOTA: Si styleHandler es '${styleHandler}', NO utilices otra tecnología distinta aunque los archivos originales (Figma HTML/CSS) digan lo contrario.`,
533
588
  results
@@ -710,6 +765,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
710
765
  });
711
766
  if (!response.ok)
712
767
  throw new Error(`Backend responded with ${response.status}`);
768
+ resetGuard();
713
769
  const data = (await response.json());
714
770
  return {
715
771
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
@@ -730,6 +786,9 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
730
786
  });
731
787
  if (!response.ok)
732
788
  throw new Error(`Backend responded with ${response.status}`);
789
+ if (args.results) {
790
+ resetGuard();
791
+ }
733
792
  const data = (await response.json());
734
793
  // Point: Version Storing
735
794
  if (data.backlogVersion)
@@ -829,6 +888,114 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
829
888
  : (data.preferredLanguage === 'en' ? "\n*** [INSTRUCTION: Provide all comments and analysis in English.] ***\n\n\n\n" : "");
830
889
  return { content: [{ type: "text", text: langHint + JSON.stringify(data, null, 2) }] };
831
890
  }
891
+ case "get_e2e_flows": {
892
+ const args = argsAny;
893
+ const fetchUrl = new URL(`${BACKEND_URL}/api/e2e/flows`);
894
+ if (args.projectId)
895
+ fetchUrl.searchParams.append("projectId", args.projectId);
896
+ if (args.status)
897
+ fetchUrl.searchParams.append("status", args.status);
898
+ const response = await fetch(fetchUrl, {
899
+ headers: { 'x-api-key': apiKey, 'x-mcp-tool-name': toolName, ...(personalKey ? { 'x-personal-key': personalKey } : {}) }
900
+ });
901
+ if (!response.ok)
902
+ throw new Error(`Backend responded with ${response.status}`);
903
+ const data = (await response.json());
904
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
905
+ }
906
+ case "process_e2e_events": {
907
+ const args = argsAny;
908
+ // Fetch UNPROCESSED events for the session (only new events)
909
+ const eventsResponse = await fetch(`${BACKEND_URL}/api/e2e/sessions/${args.sessionId}/events?unprocessed=true&limit=${args.limit || 50}`, {
910
+ headers: { 'x-api-key': apiKey, 'x-mcp-tool-name': toolName, ...(personalKey ? { 'x-personal-key': personalKey } : {}) }
911
+ });
912
+ if (!eventsResponse.ok)
913
+ throw new Error(`Failed to fetch events: ${eventsResponse.status}`);
914
+ const { events, totalUnprocessed } = await eventsResponse.json();
915
+ if (!events || events.length === 0) {
916
+ return { content: [{ type: "text", text: JSON.stringify({
917
+ message: "No new events to process - all events already in flow",
918
+ sessionId: args.sessionId,
919
+ processed: true
920
+ }, null, 2) }] };
921
+ }
922
+ console.log(`[MCP E2E] Processing ${events.length} unprocessed events (${totalUnprocessed} total pending)`);
923
+ const workspaceId = args.workspaceId || events[0]?.workspace?.toString() || '';
924
+ const projectId = args.projectId || events[0]?.project?.toString() || '';
925
+ if (!workspaceId || !projectId) {
926
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Could not determine workspace/project ID" }, null, 2) }] };
927
+ }
928
+ // Transform ONLY new events to steps
929
+ const newSteps = events.map(event => transformSingleEventToStep(event));
930
+ // Update flow with just the new steps
931
+ const updateResponse = await fetch(`${BACKEND_URL}/api/e2e/flows`, {
932
+ method: 'POST',
933
+ headers: {
934
+ 'Content-Type': 'application/json',
935
+ 'x-api-key': apiKey,
936
+ 'x-internal-key': process.env.INTERNAL_API_KEY || ''
937
+ },
938
+ body: JSON.stringify({
939
+ workspaceId,
940
+ projectId,
941
+ name: args.flowName || `E2E Flow - ${new Date().toLocaleString()}`,
942
+ steps: newSteps,
943
+ metadata: {
944
+ processedAt: new Date().toISOString(),
945
+ eventsProcessed: events.length,
946
+ mcpGenerated: true
947
+ },
948
+ sessionId: args.sessionId,
949
+ markAsProcessed: true // Tell backend to mark these events as processed
950
+ })
951
+ });
952
+ if (!updateResponse.ok) {
953
+ const error = await updateResponse.text();
954
+ throw new Error(`Failed to update flow: ${error}`);
955
+ }
956
+ const result = (await updateResponse.json());
957
+ resetGuard();
958
+ return { content: [{ type: "text", text: JSON.stringify({
959
+ success: true,
960
+ message: `Added ${newSteps.length} new steps to E2E flow (processed ${events.length} events)`,
961
+ flow: result.flow,
962
+ newStepsCount: newSteps.length,
963
+ remainingUnprocessed: Math.max(0, totalUnprocessed - events.length)
964
+ }, null, 2) }] };
965
+ }
966
+ case "export_e2e_flow": {
967
+ const args = argsAny;
968
+ // Fetch project settings to get default testing framework if not specified
969
+ let framework = args.testingFramework;
970
+ if (!framework) {
971
+ try {
972
+ const projectResponse = await fetch(`${BACKEND_URL}/api/projects/current`, {
973
+ headers: { 'x-api-key': apiKey, 'x-mcp-tool-name': toolName, ...(personalKey ? { 'x-personal-key': personalKey } : {}) }
974
+ });
975
+ if (projectResponse.ok) {
976
+ const projectData = await projectResponse.json();
977
+ framework = projectData.project?.settings?.testingFramework || 'playwright';
978
+ }
979
+ }
980
+ catch (e) {
981
+ framework = 'playwright';
982
+ }
983
+ }
984
+ const response = await fetch(`${BACKEND_URL}/api/e2e/flows/${args.flowId}/export`, {
985
+ method: 'POST',
986
+ headers: {
987
+ 'Content-Type': 'application/json',
988
+ 'x-api-key': apiKey,
989
+ 'x-mcp-tool-name': toolName,
990
+ ...(personalKey ? { 'x-personal-key': personalKey } : {})
991
+ },
992
+ body: JSON.stringify({ testingFramework: framework })
993
+ });
994
+ if (!response.ok)
995
+ throw new Error(`Backend responded with ${response.status}`);
996
+ const data = (await response.json());
997
+ return { content: [{ type: "text", text: JSON.stringify({ ...data, testingFramework: framework }, null, 2) }] };
998
+ }
832
999
  default:
833
1000
  throw new Error("Unknown tool");
834
1001
  }
@@ -840,6 +1007,294 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
840
1007
  });
841
1008
  return server;
842
1009
  }
1010
+ /**
1011
+ * Transform raw DOM events into a structured Test Flow (IR)
1012
+ * Following the priority: data-testid > getByRole > getByLabel > text > CSS > XPath
1013
+ */
1014
+ function transformEventsToTestFlow(events, flowName, workspaceId) {
1015
+ const steps = [];
1016
+ let lastUrl = '';
1017
+ for (const event of events) {
1018
+ const step = transformEventToStep(event, lastUrl);
1019
+ if (step) {
1020
+ steps.push(step);
1021
+ if (event.type === 'navigation' || step.type === 'goto') {
1022
+ lastUrl = event.url;
1023
+ }
1024
+ }
1025
+ }
1026
+ // Add automatic assertions for state changes
1027
+ const stepsWithAsserts = addImplicitAssertions(steps);
1028
+ // Calculate confidence based on selector quality
1029
+ const confidence = calculateFlowConfidence(stepsWithAsserts);
1030
+ return {
1031
+ name: flowName,
1032
+ workspaceId: workspaceId,
1033
+ steps: stepsWithAsserts,
1034
+ metadata: {
1035
+ createdAt: new Date().toISOString(),
1036
+ updatedAt: new Date().toISOString(),
1037
+ confidence,
1038
+ urlPattern: extractUrlPattern(events.map(e => e.url)),
1039
+ eventCount: events.length
1040
+ }
1041
+ };
1042
+ }
1043
+ /**
1044
+ * Infer step type from user description using keyword analysis
1045
+ * Supports English and Spanish keywords
1046
+ */
1047
+ function inferStepTypeFromDescription(description) {
1048
+ if (!description)
1049
+ return null;
1050
+ const desc = description.toLowerCase();
1051
+ // Assertion keywords (verify, validate, check, assert)
1052
+ if (/\b(verificar|validar|comprobar|asegurar|chequear|confirmar|assert|verify|validate|check|ensure|see|expect|should|must)\b/.test(desc)) {
1053
+ if (/\b(texto|text|contiene|contains|dice|says|muestra|shows|display)\b/.test(desc)) {
1054
+ return 'assertText';
1055
+ }
1056
+ return 'assertVisible';
1057
+ }
1058
+ // Fill/Type keywords
1059
+ if (/\b(escribir|type|rellenar|fill|ingresar|enter|input|escribe|completar)\b/.test(desc)) {
1060
+ return 'fill';
1061
+ }
1062
+ // Navigation keywords
1063
+ if (/\b(navegar|navega|ir a|go to|acceder|accede|entrar|entra|visit|open|abrir)\b/.test(desc)) {
1064
+ return 'goto';
1065
+ }
1066
+ // Wait keywords
1067
+ if (/\b(esperar|wait|pausa|pause|delay|sleep|detener|stop)\b/.test(desc)) {
1068
+ return 'wait';
1069
+ }
1070
+ // Select keywords
1071
+ if (/\b(seleccionar|select|selecciona|elegir|choose|dropdown|opción|option)\b/.test(desc)) {
1072
+ return 'select';
1073
+ }
1074
+ // Press key keywords
1075
+ if (/\b(presionar|press|presiona|tecla|key|enter|escape|tab|enviar|submit)\b/.test(desc)) {
1076
+ return 'press';
1077
+ }
1078
+ // Hover keywords
1079
+ if (/\b(hover|pasar|sobre|encima|mouse over|cursor)\b/.test(desc)) {
1080
+ return 'assertVisible'; // Hover usually implies checking visibility
1081
+ }
1082
+ return null;
1083
+ }
1084
+ function transformEventToStep(event, lastUrl) {
1085
+ const { type, element, value, url } = event;
1086
+ const userDescription = event.description || '';
1087
+ // Handle navigation
1088
+ if (type === 'navigation' || url !== lastUrl) {
1089
+ return {
1090
+ type: 'goto',
1091
+ selector: url,
1092
+ selectorType: 'css',
1093
+ value: url,
1094
+ description: userDescription || `Navigate to ${url}`,
1095
+ confidence: 1.0,
1096
+ originalEvent: event
1097
+ };
1098
+ }
1099
+ // Generate selector following priority order
1100
+ const selectorInfo = generateBestSelector(element);
1101
+ // Try to infer step type from user description
1102
+ const inferredType = inferStepTypeFromDescription(userDescription);
1103
+ // Log the inference for debugging
1104
+ if (inferredType && userDescription) {
1105
+ console.log(`[MCP E2E] Inferred step type "${inferredType}" from description: "${userDescription}"`);
1106
+ }
1107
+ // Use inferred type or fall back to event type
1108
+ const effectiveType = inferredType || type;
1109
+ switch (effectiveType) {
1110
+ case 'click':
1111
+ return {
1112
+ type: 'click',
1113
+ selector: selectorInfo.selector,
1114
+ selectorType: selectorInfo.type,
1115
+ description: userDescription || `Click on ${element.text || element.tag || 'element'}`,
1116
+ confidence: selectorInfo.confidence,
1117
+ originalEvent: event
1118
+ };
1119
+ case 'fill':
1120
+ return {
1121
+ type: 'fill',
1122
+ selector: selectorInfo.selector,
1123
+ selectorType: selectorInfo.type,
1124
+ value: value || '',
1125
+ description: userDescription || `Fill "${value}" in ${element.aria?.label || element.attributes?.placeholder || 'input'}`,
1126
+ confidence: selectorInfo.confidence,
1127
+ originalEvent: event
1128
+ };
1129
+ case 'press':
1130
+ return {
1131
+ type: 'press',
1132
+ selector: value || 'Enter',
1133
+ selectorType: 'css',
1134
+ value: value || 'Enter',
1135
+ description: userDescription || `Press ${value || 'Enter'} key`,
1136
+ confidence: 1.0,
1137
+ originalEvent: event
1138
+ };
1139
+ case 'assertVisible':
1140
+ return {
1141
+ type: 'assertVisible',
1142
+ selector: selectorInfo.selector,
1143
+ selectorType: selectorInfo.type,
1144
+ description: userDescription || `Assert ${element.text || 'element'} is visible`,
1145
+ confidence: selectorInfo.confidence,
1146
+ originalEvent: event
1147
+ };
1148
+ case 'assertText':
1149
+ return {
1150
+ type: 'assertText',
1151
+ selector: selectorInfo.selector,
1152
+ selectorType: selectorInfo.type,
1153
+ value: value || '',
1154
+ description: userDescription || `Assert text is "${value}"`,
1155
+ confidence: selectorInfo.confidence,
1156
+ originalEvent: event
1157
+ };
1158
+ case 'wait':
1159
+ return {
1160
+ type: 'wait',
1161
+ selector: 'body',
1162
+ selectorType: 'css',
1163
+ value: value || '1000',
1164
+ description: userDescription || `Wait ${value || '1000'}ms`,
1165
+ confidence: 1.0,
1166
+ originalEvent: event
1167
+ };
1168
+ case 'select':
1169
+ return {
1170
+ type: 'select',
1171
+ selector: selectorInfo.selector,
1172
+ selectorType: selectorInfo.type,
1173
+ value: value || '',
1174
+ description: userDescription || `Select "${value}"`,
1175
+ confidence: selectorInfo.confidence,
1176
+ originalEvent: event
1177
+ };
1178
+ default:
1179
+ // Default to click for unknown types
1180
+ return {
1181
+ type: 'click',
1182
+ selector: selectorInfo.selector,
1183
+ selectorType: selectorInfo.type,
1184
+ description: userDescription || `Click on ${element.text || element.tag || 'element'}`,
1185
+ confidence: selectorInfo.confidence * 0.8, // Lower confidence for defaulted steps
1186
+ originalEvent: event
1187
+ };
1188
+ }
1189
+ }
1190
+ // Wrapper for processing individual events without navigation context
1191
+ function transformSingleEventToStep(event) {
1192
+ // For single event processing, we treat navigation as goto if URL changed
1193
+ const lastUrl = '';
1194
+ return transformEventToStep(event, lastUrl);
1195
+ }
1196
+ function generateBestSelector(element) {
1197
+ const attrs = element.attributes || {};
1198
+ // Priority 1: data-testid (most stable)
1199
+ if (attrs['data-testid']) {
1200
+ return { selector: attrs['data-testid'], type: 'data-testid', confidence: 1.0 };
1201
+ }
1202
+ // Priority 2: ARIA role
1203
+ if (element.aria?.role && element.aria?.label) {
1204
+ return { selector: `${element.aria.role}[name="${element.aria.label}"]`, type: 'role', confidence: 0.95 };
1205
+ }
1206
+ if (element.aria?.role) {
1207
+ return { selector: element.aria.role, type: 'role', confidence: 0.9 };
1208
+ }
1209
+ // Priority 3: ARIA label
1210
+ if (element.aria?.label) {
1211
+ return { selector: element.aria.label, type: 'label', confidence: 0.9 };
1212
+ }
1213
+ // Priority 4: ID (if stable)
1214
+ if (attrs.id && !attrs.id.match(/\d+/)) {
1215
+ return { selector: `#${attrs.id}`, type: 'css', confidence: 0.85 };
1216
+ }
1217
+ // Priority 5: Text content (for buttons/links)
1218
+ if (element.text && element.text.length < 50 && ['button', 'a', 'span'].includes(element.tag || '')) {
1219
+ return { selector: element.text.trim(), type: 'text', confidence: 0.8 };
1220
+ }
1221
+ // Priority 6: CSS stable classes (avoiding dynamic classes)
1222
+ if (attrs.class) {
1223
+ const stableClasses = attrs.class
1224
+ .split(' ')
1225
+ .filter(c => !c.match(/\d+/) && c.length > 3)
1226
+ .slice(0, 3)
1227
+ .join('.');
1228
+ if (stableClasses) {
1229
+ return { selector: `.${stableClasses}`, type: 'css', confidence: 0.7 };
1230
+ }
1231
+ }
1232
+ // Priority 7: XPath (last resort)
1233
+ if (element.xpath) {
1234
+ return { selector: element.xpath, type: 'xpath', confidence: 0.5 };
1235
+ }
1236
+ // Fallback: tag name
1237
+ return { selector: element.tag || 'body', type: 'css', confidence: 0.4 };
1238
+ }
1239
+ function addImplicitAssertions(steps) {
1240
+ const result = [];
1241
+ for (let i = 0; i < steps.length; i++) {
1242
+ const step = steps[i];
1243
+ result.push(step);
1244
+ // After navigation, assert page loaded
1245
+ if (step.type === 'goto') {
1246
+ result.push({
1247
+ type: 'assertVisible',
1248
+ selector: 'body',
1249
+ selectorType: 'css',
1250
+ description: 'Assert page is loaded',
1251
+ confidence: 1.0
1252
+ });
1253
+ }
1254
+ // After fill on input, assert value changed
1255
+ if (step.type === 'fill' && i < steps.length - 1) {
1256
+ const nextStep = steps[i + 1];
1257
+ if (nextStep.type === 'click' || nextStep.type === 'press') {
1258
+ result.push({
1259
+ type: 'assertVisible',
1260
+ selector: step.selector,
1261
+ selectorType: step.selectorType,
1262
+ description: `Assert input has value "${step.value}"`,
1263
+ confidence: 0.9
1264
+ });
1265
+ }
1266
+ }
1267
+ }
1268
+ return result;
1269
+ }
1270
+ function calculateFlowConfidence(steps) {
1271
+ if (steps.length === 0)
1272
+ return 0;
1273
+ const totalConfidence = steps.reduce((sum, step) => sum + (step.confidence || 0.5), 0);
1274
+ return Math.round((totalConfidence / steps.length) * 100) / 100;
1275
+ }
1276
+ function extractUrlPattern(urls) {
1277
+ if (urls.length === 0)
1278
+ return '';
1279
+ // Find common pattern
1280
+ const uniqueUrls = [...new Set(urls)];
1281
+ if (uniqueUrls.length === 1)
1282
+ return uniqueUrls[0];
1283
+ // Try to extract route pattern (e.g., /user/:id)
1284
+ const patterns = uniqueUrls.map(url => {
1285
+ try {
1286
+ const pathname = new URL(url).pathname;
1287
+ return pathname.replace(/\/[0-9a-f]{24}/g, '/:id').replace(/\/\d+/g, '/:id');
1288
+ }
1289
+ catch {
1290
+ return url;
1291
+ }
1292
+ });
1293
+ return [...new Set(patterns)].join(' -> ');
1294
+ }
1295
+ // ============================================
1296
+ // END E2E TRANSFORMATION FUNCTIONS
1297
+ // ============================================
843
1298
  const useSSE = process.argv.includes("--sse") || process.env.MCP_TRANSPORT === "sse";
844
1299
  if (!useSSE) {
845
1300
  const apiKey = process.env.VIEWGATE_API_KEY || process.env.API_KEY || "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viewgate-mcp",
3
- "version": "1.0.61",
3
+ "version": "1.0.63",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "viewgate-mcp": "./dist/index.js"