chrometools-mcp 3.3.6 → 3.3.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrometools-mcp",
3
- "version": "3.3.6",
3
+ "version": "3.3.9",
4
4
  "description": "MCP (Model Context Protocol) server for Chrome automation using Puppeteer. Persistent browser sessions, UI framework detection (MUI, Ant Design, etc.), Page Object support, visual testing, Figma comparison. Works seamlessly in WSL, Linux, macOS, and Windows.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -48,6 +48,7 @@
48
48
  "dependencies": {
49
49
  "@modelcontextprotocol/sdk": "^1.20.2",
50
50
  "jimp": "^0.22.12",
51
+ "js-yaml": "^4.1.1",
51
52
  "pixelmatch": "^7.1.0",
52
53
  "puppeteer": "^24.27.0",
53
54
  "ws": "^8.18.0",
@@ -10,9 +10,10 @@
10
10
  * Runs in browser context via page.evaluate()
11
11
  *
12
12
  * @param {boolean} interactiveOnly - Only include interactive elements and their parents
13
+ * @param {boolean} viewportOnly - Only include elements visible in current viewport
13
14
  * @returns {Object} APOM tree structure
14
15
  */
15
- function buildAPOMTree(interactiveOnly = true) {
16
+ function buildAPOMTree(interactiveOnly = true, viewportOnly = false) {
16
17
  const pageId = `page_${btoa(window.location.href).replace(/[^a-zA-Z0-9]/g, '').substring(0, 20)}_${Date.now()}`;
17
18
 
18
19
  const result = {
@@ -26,7 +27,14 @@ function buildAPOMTree(interactiveOnly = true) {
26
27
  interactiveCount: 0,
27
28
  formCount: 0,
28
29
  modalCount: 0,
29
- maxDepth: 0
30
+ maxDepth: 0,
31
+ viewportOnly: viewportOnly || undefined,
32
+ viewport: viewportOnly ? {
33
+ width: window.innerWidth || document.documentElement.clientWidth,
34
+ height: window.innerHeight || document.documentElement.clientHeight,
35
+ scrollX: window.scrollX || window.pageXOffset,
36
+ scrollY: window.scrollY || window.pageYOffset
37
+ } : undefined
30
38
  }
31
39
  };
32
40
 
@@ -51,8 +59,122 @@ function buildAPOMTree(interactiveOnly = true) {
51
59
  // Collect radio and checkbox groups for easier agent access
52
60
  result.groups = collectInputGroups(result.tree);
53
61
 
62
+ // Detect page alerts, warnings, errors, and restrictions
63
+ result.alerts = detectPageAlerts();
64
+
54
65
  return result;
55
66
 
67
+ /**
68
+ * Detect page alerts, warnings, errors, toasts, and restrictions
69
+ * Uses heuristics to find notification elements
70
+ */
71
+ function detectPageAlerts() {
72
+ const alerts = [];
73
+ const seenTexts = new Set();
74
+ const MIN_TEXT_LENGTH = 10; // Ignore very short texts like "19", "OK"
75
+
76
+ // Helper to add alert avoiding duplicates and substrings
77
+ function addAlert(type, text, source) {
78
+ text = text?.trim();
79
+ if (!text || text.length < MIN_TEXT_LENGTH || text.length > 300) return;
80
+
81
+ // Normalize text
82
+ const normalizedText = text.substring(0, 200);
83
+
84
+ // Skip if already seen or is substring of existing
85
+ if (seenTexts.has(normalizedText)) return;
86
+ for (const seen of seenTexts) {
87
+ if (seen.includes(normalizedText) || normalizedText.includes(seen)) return;
88
+ }
89
+
90
+ seenTexts.add(normalizedText);
91
+ alerts.push({ type, text: normalizedText, source });
92
+ }
93
+
94
+ // 1. Elements with role="alert" or aria-live (highest priority)
95
+ document.querySelectorAll('[role="alert"], [aria-live="assertive"]').forEach(el => {
96
+ const text = el.textContent?.trim();
97
+ addAlert('alert', text, 'aria');
98
+ });
99
+
100
+ // 2. Classes containing restriction keywords (high priority - affects functionality)
101
+ const restrictionSelector = '[class*="deactivat"], [class*="disabled"], [class*="blocked"], [class*="restrict"], [class*="suspend"], [class*="inactive"]';
102
+ document.querySelectorAll(restrictionSelector).forEach(el => {
103
+ if (!isVisibleForAlert(el)) return;
104
+ const text = el.textContent?.trim();
105
+ addAlert('restriction', text, 'class');
106
+ });
107
+
108
+ // 3. Error classes
109
+ document.querySelectorAll('[class*="error"], [class*="danger"], [class*="invalid"]').forEach(el => {
110
+ if (!isVisibleForAlert(el)) return;
111
+ const text = el.textContent?.trim();
112
+ addAlert('error', text, 'class');
113
+ });
114
+
115
+ // 4. Warning classes
116
+ document.querySelectorAll('[class*="warning"], [class*="warn"], [class*="caution"]').forEach(el => {
117
+ if (!isVisibleForAlert(el)) return;
118
+ const text = el.textContent?.trim();
119
+ addAlert('warning', text, 'class');
120
+ });
121
+
122
+ // 5. Toast/Snackbar notifications (usually important messages)
123
+ document.querySelectorAll('[class*="toast"], [class*="snackbar"], [class*="alert-banner"]').forEach(el => {
124
+ if (!isVisibleForAlert(el)) return;
125
+ const text = el.textContent?.trim();
126
+ addAlert('notification', text, 'toast');
127
+ });
128
+
129
+ // 6. SVG icons with warning colors - only for restriction/deactivated contexts
130
+ const warningColors = ['#FF3B30', '#FAB32F', '#FFCC00', '#FF9500', '#F44336'];
131
+ document.querySelectorAll('svg').forEach(svg => {
132
+ const svgHtml = svg.outerHTML.toLowerCase();
133
+ const hasWarningColor = warningColors.some(c => svgHtml.includes(c.toLowerCase()));
134
+ if (!hasWarningColor) return;
135
+
136
+ // Only consider if parent has restriction-related class or text
137
+ let parent = svg.parentElement;
138
+ let attempts = 0;
139
+ while (parent && attempts < 3) {
140
+ const className = parent.className?.toLowerCase() || '';
141
+ const text = parent.textContent?.trim() || '';
142
+
143
+ // Check if context suggests restriction/warning
144
+ const isRestrictionContext =
145
+ /deactivat|disabled|blocked|restrict|suspend|приостановлен|заблокирован/.test(className + ' ' + text.toLowerCase());
146
+
147
+ if (isRestrictionContext && text.length >= MIN_TEXT_LENGTH) {
148
+ addAlert('restriction', text, 'icon-color');
149
+ break;
150
+ }
151
+ parent = parent.parentElement;
152
+ attempts++;
153
+ }
154
+ });
155
+
156
+ // 7. Elements with semantic key/name attributes
157
+ document.querySelectorAll('[key*="error"], [key*="warning"], [key*="deactivat"], [key*="block"]').forEach(el => {
158
+ const parent = el.closest('div, span, p');
159
+ if (parent) {
160
+ const text = parent.textContent?.trim();
161
+ addAlert('warning', text, 'semantic-attr');
162
+ }
163
+ });
164
+
165
+ // Return only if there are meaningful alerts
166
+ return alerts.length > 0 ? alerts : undefined;
167
+ }
168
+
169
+ /**
170
+ * Check if element is visible (for alert detection)
171
+ */
172
+ function isVisibleForAlert(el) {
173
+ if (el.offsetWidth === 0 || el.offsetHeight === 0) return false;
174
+ const style = window.getComputedStyle(el);
175
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
176
+ }
177
+
56
178
  /**
57
179
  * Collect radio and checkbox groups from the tree
58
180
  */
@@ -148,6 +270,32 @@ function buildAPOMTree(interactiveOnly = true) {
148
270
  return node;
149
271
  }
150
272
 
273
+ /**
274
+ * Filter out null, undefined, and empty string values from object
275
+ * to reduce JSON output size
276
+ */
277
+ function filterNullValues(obj) {
278
+ if (!obj || typeof obj !== 'object') return obj;
279
+
280
+ const result = {};
281
+ for (const [key, value] of Object.entries(obj)) {
282
+ // Skip null, undefined, and empty strings
283
+ if (value === null || value === undefined || value === '') continue;
284
+ // Skip false for boolean fields (keep true)
285
+ if (value === false) continue;
286
+ // Recursively filter nested objects (but not arrays)
287
+ if (typeof value === 'object' && !Array.isArray(value)) {
288
+ const filtered = filterNullValues(value);
289
+ if (Object.keys(filtered).length > 0) {
290
+ result[key] = filtered;
291
+ }
292
+ } else {
293
+ result[key] = value;
294
+ }
295
+ }
296
+ return result;
297
+ }
298
+
151
299
  /**
152
300
  * Check if cursor:pointer is explicitly set (not inherited)
153
301
  */
@@ -206,9 +354,9 @@ function buildAPOMTree(interactiveOnly = true) {
206
354
  // tabindex (except -1)
207
355
  (el.hasAttribute('tabindex') && el.getAttribute('tabindex') !== '-1') ||
208
356
  // contenteditable
209
- el.getAttribute('contenteditable') === 'true'
210
- // Note: We skip event listener check here for performance
211
- // as querySelectorAll can return thousands of elements
357
+ el.getAttribute('contenteditable') === 'true' ||
358
+ // Click event listeners (Angular, React, Vue) via monkey-patched addEventListener tracker
359
+ hasExplicitClickBinding(el)
212
360
  );
213
361
 
214
362
  if (isInteractive && isVisible(el)) {
@@ -229,6 +377,23 @@ function buildAPOMTree(interactiveOnly = true) {
229
377
  interactiveElements.add(document.body);
230
378
  }
231
379
 
380
+ /**
381
+ * Check if element is in viewport
382
+ */
383
+ function isInViewport(el) {
384
+ const rect = el.getBoundingClientRect();
385
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
386
+ const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
387
+
388
+ // Element is in viewport if any part of it is visible
389
+ return (
390
+ rect.bottom > 0 &&
391
+ rect.right > 0 &&
392
+ rect.top < viewportHeight &&
393
+ rect.left < viewportWidth
394
+ );
395
+ }
396
+
232
397
  /**
233
398
  * Check if element is visible
234
399
  * More reliable check that works with position:fixed elements (Angular Material, etc.)
@@ -248,6 +413,11 @@ function buildAPOMTree(interactiveOnly = true) {
248
413
  // For body element, always consider visible if dimensions > 0
249
414
  if (el === document.body) return true;
250
415
 
416
+ // Check viewport if viewportOnly mode is enabled
417
+ if (viewportOnly && !isInViewport(el)) {
418
+ return false;
419
+ }
420
+
251
421
  // Additional check: element should be in viewport or have offsetParent
252
422
  // This handles elements inside position:fixed containers (Angular Material)
253
423
  return el.offsetParent !== null || style.position === 'fixed' || style.position === 'sticky';
@@ -297,19 +467,23 @@ function buildAPOMTree(interactiveOnly = true) {
297
467
  node = {
298
468
  id,
299
469
  tag: element.tagName.toLowerCase(),
300
- position,
301
470
  type: elementType.type,
302
471
  children: []
303
472
  };
304
473
 
474
+ // Only include position if not static (position is null for static)
475
+ if (position) {
476
+ node.position = position;
477
+ }
478
+
305
479
  // Add selector only in includeAll mode
306
480
  if (!interactiveOnly) {
307
481
  node.selector = selector;
308
482
  }
309
483
 
310
- // Add metadata for interactive elements
484
+ // Add metadata for interactive elements, filtering out null/undefined values
311
485
  if (elementType.metadata) {
312
- node.metadata = elementType.metadata;
486
+ node.metadata = filterNullValues(elementType.metadata);
313
487
  }
314
488
  } else {
315
489
  // Containers: compact format "tag_id": [children] when interactiveOnly
@@ -340,8 +514,8 @@ function buildAPOMTree(interactiveOnly = true) {
340
514
  if (elementType.type === 'form') {
341
515
  result.metadata.formCount++;
342
516
  }
343
- if (position.type === 'fixed' || position.type === 'absolute') {
344
- if (position.zIndex >= 100) {
517
+ if (position && (position.type === 'fixed' || position.type === 'absolute')) {
518
+ if (position.zIndex && position.zIndex >= 100) {
345
519
  result.metadata.modalCount++;
346
520
  }
347
521
  }
@@ -365,6 +539,11 @@ function buildAPOMTree(interactiveOnly = true) {
365
539
  return compactNode;
366
540
  }
367
541
 
542
+ // Remove empty children array to reduce output size
543
+ if (node.children && node.children.length === 0) {
544
+ delete node.children;
545
+ }
546
+
368
547
  return node;
369
548
  }
370
549
 
@@ -379,37 +558,30 @@ function buildAPOMTree(interactiveOnly = true) {
379
558
 
380
559
  /**
381
560
  * Get positioning information
561
+ * Returns null for static position (default) to reduce output size
382
562
  */
383
563
  function getPositionInfo(element) {
384
564
  const style = window.getComputedStyle(element);
385
565
  const position = style.position;
386
- const zIndex = style.zIndex === 'auto' ? 'auto' : parseInt(style.zIndex, 10);
387
566
 
388
- // Check if creates stacking context
389
- const isStacking =
390
- position === 'fixed' ||
391
- position === 'sticky' ||
392
- (position === 'absolute' && zIndex !== 'auto') ||
393
- (position === 'relative' && zIndex !== 'auto') ||
394
- parseFloat(style.opacity) < 1 ||
395
- style.transform !== 'none' ||
396
- style.filter !== 'none' ||
397
- style.perspective !== 'none' ||
398
- style.clipPath !== 'none' ||
399
- style.mask !== 'none' ||
400
- style.mixBlendMode !== 'normal' ||
401
- style.isolation === 'isolate';
567
+ // Skip static position (default) - no need to include
568
+ if (position === 'static') {
569
+ return null;
570
+ }
402
571
 
403
- return {
404
- type: position,
405
- zIndex: zIndex,
406
- isStacking: isStacking,
407
- // Additional positioning properties for modals/overlays detection
408
- hasBackdrop: style.backgroundColor !== 'rgba(0, 0, 0, 0)' &&
409
- (position === 'fixed' || position === 'absolute'),
410
- isFullscreen: element.offsetWidth >= window.innerWidth * 0.9 &&
411
- element.offsetHeight >= window.innerHeight * 0.9
572
+ const zIndex = style.zIndex === 'auto' ? 'auto' : parseInt(style.zIndex, 10);
573
+
574
+ // Only include non-default values
575
+ const result = {
576
+ type: position
412
577
  };
578
+
579
+ // Only include zIndex if it's not 'auto'
580
+ if (zIndex !== 'auto') {
581
+ result.zIndex = zIndex;
582
+ }
583
+
584
+ return result;
413
585
  }
414
586
 
415
587
  /**
@@ -426,24 +598,80 @@ function buildAPOMTree(interactiveOnly = true) {
426
598
  }
427
599
 
428
600
  /**
429
- * Check if element has click event listeners
601
+ * Check if tag is a custom element (Web Component / Framework component)
602
+ */
603
+ function isCustomElement(tag) {
604
+ // Custom elements must contain a hyphen
605
+ return tag.includes('-');
606
+ }
607
+
608
+ /**
609
+ * Check if element has explicit click binding (framework attributes or addEventListener)
610
+ * Uses monkey-patched addEventListener tracker for framework detection (Angular, React, Vue)
430
611
  */
431
- function hasClickListener(element) {
432
- try {
433
- // Check for getEventListeners (available in Chrome DevTools context)
434
- if (typeof getEventListeners === 'function') {
435
- const listeners = getEventListeners(element);
436
- return listeners && listeners.click && listeners.click.length > 0;
612
+ function hasExplicitClickBinding(element) {
613
+ // Check for framework-specific click bindings (attributes)
614
+ const attrs = element.attributes;
615
+ for (let i = 0; i < attrs.length; i++) {
616
+ const name = attrs[i].name.toLowerCase();
617
+ // Angular: (click), Angular.js: ng-click
618
+ // Vue: @click, v-on:click
619
+ // React: onClick (but this is a property, not attribute)
620
+ if (name === '(click)' || name === 'ng-click' ||
621
+ name === '@click' || name === 'v-on:click' ||
622
+ name.startsWith('on') && name.includes('click')) {
623
+ return true;
437
624
  }
625
+ }
626
+ // Check onclick property
627
+ if (element.onclick) return true;
628
+ // Check onclick attribute
629
+ if (element.hasAttribute('onclick')) return true;
630
+
631
+ // Check for click listeners added via addEventListener (framework detection)
632
+ // This is injected by page-manager.js via evaluateOnNewDocument
633
+ if (typeof window.__hasClickListener === 'function' && window.__hasClickListener(element)) {
634
+ return true;
635
+ }
438
636
 
439
- // Fallback: check for common event listener markers
440
- // Note: This is not 100% reliable but catches common cases
441
- return element._events?.click ||
442
- element.__listeners?.click ||
443
- element.__eventListeners?.click;
444
- } catch (e) {
445
- return false;
637
+ return false;
638
+ }
639
+
640
+ /**
641
+ * Find click handler for interactive elements (bubbling search)
642
+ * Searches UP from element to find ancestor with explicit click binding
643
+ * Returns "tag:id" format like "div:container_19" or null if not found
644
+ */
645
+ function findClickHandler(element) {
646
+ const tag = element.tagName.toLowerCase();
647
+
648
+ // Check self first - native interactive elements handle their own clicks
649
+ if (hasExplicitClickBinding(element) ||
650
+ ['button', 'a', 'input', 'select'].includes(tag) ||
651
+ element.getAttribute('role') === 'button') {
652
+ return null; // element handles its own click, no need for clickTarget
653
+ }
654
+
655
+ // Search UP (like event bubbling) - find ancestor with explicit click handler
656
+ let parent = element.parentElement;
657
+ let depth = 0;
658
+ const maxDepth = 5; // limit to prevent hanging
659
+
660
+ while (parent && depth < maxDepth) {
661
+ if (hasExplicitClickBinding(parent)) {
662
+ // Found real click handler
663
+ const parentId = elementIds.get(parent);
664
+ if (parentId) {
665
+ const parentTag = parent.tagName.toLowerCase();
666
+ return `${parentTag}:${parentId}`;
667
+ }
668
+ }
669
+ parent = parent.parentElement;
670
+ depth++;
446
671
  }
672
+
673
+ // No click handler found - return null (no fallback!)
674
+ return null;
447
675
  }
448
676
 
449
677
  /**
@@ -483,8 +711,8 @@ function buildAPOMTree(interactiveOnly = true) {
483
711
  return { isInteractive: true, reason: 'cursor-pointer' };
484
712
  }
485
713
 
486
- // 6. Elements with click event listeners
487
- if (hasClickListener(element)) {
714
+ // 6. Elements with click event listeners (framework handlers: Angular, React, Vue)
715
+ if (hasExplicitClickBinding(element)) {
488
716
  return { isInteractive: true, reason: 'event-listener' };
489
717
  }
490
718
 
@@ -680,12 +908,20 @@ function buildAPOMTree(interactiveOnly = true) {
680
908
 
681
909
  // Generic container - check for JavaScript interactivity
682
910
  const interactivityCheck = checkInteractivity(element);
911
+
912
+ // For ALL interactive elements, search for click handler (bubbling)
913
+ // Returns "tag:id" format or null if not found
914
+ const clickTarget = interactivityCheck.isInteractive
915
+ ? findClickHandler(element)
916
+ : null;
917
+
683
918
  return {
684
919
  type: 'container',
685
920
  isInteractive: interactivityCheck.isInteractive,
686
921
  metadata: interactivityCheck.isInteractive ? {
687
922
  text: element.textContent?.trim().substring(0, 100) || '',
688
- interactivityReason: interactivityCheck.reason
923
+ interactivityReason: interactivityCheck.reason,
924
+ clickTarget: clickTarget || undefined
689
925
  } : null
690
926
  };
691
927
  }
@@ -33,6 +33,19 @@ export async function generatePageObject(page, options = {}) {
33
33
  // Generate code based on framework
34
34
  const code = await generateCode(finalClassName, elementGroups, pageAnalysis, framework, includeComments);
35
35
 
36
+ // Build structured elements metadata for POM integration
37
+ const allElements = Object.values(elementGroups).flat();
38
+ const uniqueElements = deduplicateElements(allElements);
39
+ const lang = framework.includes('python') ? 'python' : framework.includes('java') ? 'java' : 'typescript';
40
+ const elements = uniqueElements.map(el => ({
41
+ name: sanitizeIdentifier(el.name, lang),
42
+ selector: el.selector,
43
+ tag: el.tag,
44
+ type: el.type,
45
+ methodName: generateMethodName(el, framework),
46
+ methodType: getMethodType(el)
47
+ }));
48
+
36
49
  return {
37
50
  success: true,
38
51
  className: finalClassName,
@@ -41,7 +54,8 @@ export async function generatePageObject(page, options = {}) {
41
54
  elementCount: pageAnalysis.elements.length,
42
55
  groups: Object.keys(elementGroups),
43
56
  framework,
44
- code
57
+ code,
58
+ elements
45
59
  };
46
60
  }
47
61
 
@@ -728,6 +742,36 @@ function generateSeleniumJavaActionMethods(lines, elements) {
728
742
  });
729
743
  }
730
744
 
745
+ /**
746
+ * Helper: Determine method type for element
747
+ * @param {Object} el - Element info
748
+ * @returns {string} - "fill" | "click" | "select"
749
+ */
750
+ function getMethodType(el) {
751
+ if (el.tag === 'select') return 'select';
752
+ if (el.tag === 'input' || el.tag === 'textarea') return 'fill';
753
+ return 'click';
754
+ }
755
+
756
+ /**
757
+ * Helper: Generate method name for element based on framework
758
+ * @param {Object} el - Element info
759
+ * @param {string} framework - Target framework
760
+ * @returns {string} - Method name (e.g., "fillUsername", "clickSubmit", "fill_username")
761
+ */
762
+ function generateMethodName(el, framework) {
763
+ const methodType = getMethodType(el);
764
+ const isPython = framework.includes('python');
765
+ const lang = isPython ? 'python' : framework.includes('java') ? 'java' : 'typescript';
766
+ const name = sanitizeIdentifier(el.name, lang);
767
+
768
+ if (isPython) {
769
+ return `${methodType}_${name}`;
770
+ }
771
+ // TypeScript/Java: camelCase
772
+ return `${methodType}${capitalize(name)}`;
773
+ }
774
+
731
775
  /**
732
776
  * Helper: Capitalize first letter
733
777
  */
@@ -238,7 +238,7 @@ export const toolDefinitions = [
238
238
  },
239
239
  {
240
240
  name: "drag",
241
- description: "Drag element in any direction. For maps, charts, SVG, canvas, sliders. Use scrollHorizontal for scrollbars.",
241
+ description: "Drag element in any direction. For maps, charts, SVG, canvas, sliders. Use mode='synthetic' for JS libraries (frappe-gantt, jQuery UI). Use scrollHorizontal for scrollbars.",
242
242
  inputSchema: {
243
243
  type: "object",
244
244
  properties: {
@@ -246,6 +246,7 @@ export const toolDefinitions = [
246
246
  direction: { type: "string", enum: ["up", "down", "left", "right", "up-left", "up-right", "down-left", "down-right"], description: "Drag direction" },
247
247
  distance: { type: "number", description: "Distance in pixels (default: 100)" },
248
248
  duration: { type: "number", description: "Drag duration in ms (default: 500)" },
249
+ mode: { type: "string", enum: ["native", "synthetic"], description: "Drag mode: 'native' (default, faster) or 'synthetic' (better for JS libraries)" },
249
250
  },
250
251
  required: ["selector", "direction"],
251
252
  },
@@ -477,7 +478,7 @@ export const toolDefinitions = [
477
478
  },
478
479
  {
479
480
  name: "analyzePage",
480
- description: "PRIMARY tool for reading page state. Returns APOM tree: {tree, metadata, groups}. Compact format (default): containers as \"tag_id\":[children] keys, interactive elements as {id, tag, type, position, metadata} without selectors. Use element IDs (e.g., button_45, input_20) with click/type tools. Selectors registered internally for resolution. Use refresh:true after clicks. Efficient: 8-10k tokens vs screenshot 15-25k.",
481
+ description: "PRIMARY tool for reading page state. Returns APOM tree: {tree, metadata, groups}. Compact format (default): containers as \"tag_id\":[children] keys, interactive elements as {id, tag, type, position, metadata} without selectors. Use element IDs (e.g., button_45, input_20) with click/type tools. Selectors registered internally for resolution. Use refresh:true after clicks. Efficient: 8-10k tokens vs screenshot 15-25k. Legend: clickTarget format is \"tag:id\" (e.g., \"kp-chats-list-item:container_58\") - use the id part for clicking. No clickTarget = element handles its own click.",
481
482
  inputSchema: {
482
483
  type: "object",
483
484
  properties: {
@@ -486,6 +487,8 @@ export const toolDefinitions = [
486
487
  useLegacyFormat: { type: "boolean", description: "Return legacy format instead of APOM (default: false - APOM is now default)" },
487
488
  registerElements: { type: "boolean", description: "Auto-register elements in selector resolver (default: true)" },
488
489
  groupBy: { type: "string", description: "Group elements: 'type' or 'flat' (default: 'type')", enum: ["type", "flat"] },
490
+ viewportOnly: { type: "boolean", description: "Only analyze elements in current viewport (default: false). Reduces output for long pages." },
491
+ diff: { type: "boolean", description: "Return only changes since last analysis: {added, removed, changed} (default: false)." },
489
492
  },
490
493
  },
491
494
  },
@@ -683,7 +686,7 @@ export const toolDefinitions = [
683
686
  },
684
687
  {
685
688
  name: "exportScenarioAsCode",
686
- description: "Export scenario as test code for NEW file. Cleans unstable selectors, optionally generates Page Object. Use appendScenarioToFile for existing files.",
689
+ description: "Export scenario as test code for NEW file. Supports Page Object integration: 'generate-integrated' generates POM + test using it, 'use-existing' generates test using existing POM file. Use appendScenarioToFile for existing files.",
687
690
  inputSchema: {
688
691
  type: "object",
689
692
  properties: {
@@ -706,19 +709,28 @@ export const toolDefinitions = [
706
709
  },
707
710
  generatePageObject: {
708
711
  type: "boolean",
709
- description: "Also generate Page Object class for the page (default: false)"
712
+ description: "Also generate Page Object class for the page (default: false). Legacy - use pageObjectMode instead."
710
713
  },
711
714
  pageObjectClassName: {
712
715
  type: "string",
713
716
  description: "Page Object class name (optional, auto-generated if not provided)"
714
717
  },
718
+ pageObjectMode: {
719
+ type: "string",
720
+ enum: ["none", "generate", "generate-integrated", "use-existing"],
721
+ description: "POM integration: 'none' (default), 'generate' (separate POM), 'generate-integrated' (POM + test using it), 'use-existing' (test uses existing POM file)"
722
+ },
723
+ pageObjectFile: {
724
+ type: "string",
725
+ description: "Path to existing POM file (required for 'use-existing' mode)"
726
+ },
715
727
  },
716
728
  required: ["scenarioName", "language"],
717
729
  },
718
730
  },
719
731
  {
720
732
  name: "appendScenarioToFile",
721
- description: "Append scenario as test code to EXISTING file. Cleans unstable selectors, optionally generates Page Object. Use exportScenarioAsCode for new files.",
733
+ description: "Append scenario as test code to EXISTING file. Supports Page Object integration: 'generate-integrated' generates POM + test using it, 'use-existing' generates test using existing POM file. Use exportScenarioAsCode for new files.",
722
734
  inputSchema: {
723
735
  type: "object",
724
736
  properties: {
@@ -758,7 +770,16 @@ export const toolDefinitions = [
758
770
  },
759
771
  generatePageObject: {
760
772
  type: "boolean",
761
- description: "Also generate Page Object class for the page (default: false)"
773
+ description: "Also generate Page Object class for the page (default: false). Legacy - use pageObjectMode instead."
774
+ },
775
+ pageObjectMode: {
776
+ type: "string",
777
+ enum: ["none", "generate", "generate-integrated", "use-existing"],
778
+ description: "POM integration: 'none' (default), 'generate' (separate POM), 'generate-integrated' (POM + test using it), 'use-existing' (test uses existing POM file)"
779
+ },
780
+ pageObjectFile: {
781
+ type: "string",
782
+ description: "Path to existing POM file (required for 'use-existing' mode)"
762
783
  },
763
784
  pageObjectClassName: {
764
785
  type: "string",
@@ -819,4 +840,34 @@ export const toolDefinitions = [
819
840
  required: ["tab"],
820
841
  },
821
842
  },
843
+ {
844
+ name: "loadSwagger",
845
+ description: "Load and parse OpenAPI/Swagger spec from URL or local file. Returns structured summary: endpoints, schemas, auth types, base URL. Supports both OpenAPI 2.0 (Swagger) and 3.x, JSON and YAML formats. Use this first to understand an API before generating models or client code.",
846
+ inputSchema: {
847
+ type: "object",
848
+ properties: {
849
+ source: { type: "string", description: "URL (http/https) or local file path to swagger.json / openapi.yaml" },
850
+ format: { type: "string", enum: ["auto", "json", "yaml"], description: "Spec format. 'auto' (default) detects from extension/content" },
851
+ },
852
+ required: ["source"],
853
+ },
854
+ },
855
+ {
856
+ name: "generateApiModels",
857
+ description: "Generate typed data models from OpenAPI/Swagger spec. Creates TypeScript interfaces/types or Python dataclasses/pydantic/TypedDict from API schemas. Handles $ref resolution, enums, allOf/oneOf, nested objects. Use after loadSwagger to generate model files.",
858
+ inputSchema: {
859
+ type: "object",
860
+ properties: {
861
+ source: { type: "string", description: "URL or file path to OpenAPI spec" },
862
+ language: { type: "string", enum: ["typescript", "python"], description: "Target language for models" },
863
+ format: { type: "string", enum: ["auto", "json", "yaml"], description: "Spec format (default: auto)" },
864
+ style: { type: "string", enum: ["interface", "type"], description: "TypeScript only: 'interface' (default) or 'type' aliases" },
865
+ pythonStyle: { type: "string", enum: ["dataclass", "pydantic", "typeddict"], description: "Python only: 'dataclass' (default), 'pydantic', or 'typeddict'" },
866
+ includeEnums: { type: "boolean", description: "Generate enum types (default: true)" },
867
+ includeValidation: { type: "boolean", description: "Include validation constraints as comments (default: false)" },
868
+ schemas: { type: "array", items: { type: "string" }, description: "Generate only these schemas (default: all)" },
869
+ },
870
+ required: ["source", "language"],
871
+ },
872
+ },
822
873
  ];