chrometools-mcp 3.1.6 → 3.2.4

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.
@@ -43,6 +43,11 @@ function buildAPOMTree(interactiveOnly = true) {
43
43
  // Build tree from body
44
44
  result.tree = buildNode(document.body, null, 0, []);
45
45
 
46
+ // Prune empty branches (containers without interactive elements)
47
+ if (interactiveOnly && result.tree) {
48
+ result.tree = pruneEmptyBranches(result.tree);
49
+ }
50
+
46
51
  // Collect radio and checkbox groups for easier agent access
47
52
  result.groups = collectInputGroups(result.tree);
48
53
 
@@ -102,29 +107,109 @@ function buildAPOMTree(interactiveOnly = true) {
102
107
  }
103
108
 
104
109
  /**
105
- * Mark interactive elements and their ancestors
110
+ * Prune empty branches (containers without interactive elements)
111
+ * Bottom-up traversal: remove container branches that don't end with interactive leaves
106
112
  */
107
- function markInteractiveElements(root) {
108
- const interactiveTags = new Set([
109
- 'A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'LABEL', 'FORM'
110
- ]);
113
+ function pruneEmptyBranches(node) {
114
+ if (!node) return null;
115
+
116
+ // Handle compact format containers
117
+ if (typeof node === 'object' && !node.id && !node.tag) {
118
+ // Compact format: { "tag_id": [children] }
119
+ const keys = Object.keys(node);
120
+ if (keys.length === 1 && Array.isArray(node[keys[0]])) {
121
+ const key = keys[0];
122
+ const prunedChildren = node[key]
123
+ .map(child => pruneEmptyBranches(child))
124
+ .filter(child => child !== null);
125
+
126
+ // If no children left after pruning, remove this container
127
+ if (prunedChildren.length === 0) {
128
+ return null;
129
+ }
111
130
 
112
- const interactiveRoles = new Set([
113
- 'button', 'link', 'textbox', 'checkbox', 'radio', 'combobox', 'listbox',
114
- 'menuitem', 'tab', 'switch', 'slider', 'searchbox'
115
- ]);
131
+ return { [key]: prunedChildren };
132
+ }
133
+ }
116
134
 
117
- // Find all interactive elements
135
+ // Handle regular format (interactive elements or full-mode containers)
136
+ if (node.children && Array.isArray(node.children)) {
137
+ // Prune children recursively
138
+ node.children = node.children
139
+ .map(child => pruneEmptyBranches(child))
140
+ .filter(child => child !== null);
141
+ }
142
+
143
+ // If this is a container (no type = not interactive) with no children, remove it
144
+ if (!node.type && node.children && node.children.length === 0) {
145
+ return null;
146
+ }
147
+
148
+ return node;
149
+ }
150
+
151
+ /**
152
+ * Check if cursor:pointer is explicitly set (not inherited)
153
+ */
154
+ function hasCursorPointerExplicit(element) {
155
+ const computedStyle = window.getComputedStyle(element);
156
+ if (computedStyle.cursor !== 'pointer') {
157
+ return false;
158
+ }
159
+
160
+ // Check if cursor is set via inline style
161
+ if (element.style.cursor === 'pointer') {
162
+ return true;
163
+ }
164
+
165
+ // Check if cursor is set via CSS class or direct CSS rule (not inherited)
166
+ // If parent also has cursor:pointer computed, then it's likely inherited
167
+ const parent = element.parentElement;
168
+ if (parent) {
169
+ const parentStyle = window.getComputedStyle(parent);
170
+ if (parentStyle.cursor === 'pointer') {
171
+ // Parent has cursor:pointer, so this is inherited
172
+ return false;
173
+ }
174
+ }
175
+
176
+ // Element has cursor:pointer but parent doesn't - it's explicitly set
177
+ return true;
178
+ }
179
+
180
+ /**
181
+ * Mark interactive elements and their ancestors
182
+ * NOTE: This function is defined before checkInteractivity,
183
+ * so we need to inline the checks or move function definitions
184
+ */
185
+ function markInteractiveElements(root) {
186
+ // Find all interactive elements using the same logic as checkInteractivity
118
187
  const elements = root.querySelectorAll('*');
119
188
  const interactiveList = [];
120
189
 
121
190
  elements.forEach(el => {
122
- const isInteractive =
123
- interactiveTags.has(el.tagName) ||
124
- interactiveRoles.has(el.getAttribute('role')) ||
191
+ // Use inline checks (same logic as checkInteractivity)
192
+ const tag = el.tagName.toLowerCase();
193
+ const role = el.getAttribute('role');
194
+
195
+ const isInteractive = (
196
+ // Native HTML interactive elements
197
+ ['a', 'button', 'input', 'select', 'textarea', 'label', 'form'].includes(tag) ||
198
+ // Interactive ARIA roles
199
+ (role && ['button', 'link', 'checkbox', 'radio', 'tab', 'menuitem', 'option', 'switch', 'textbox'].includes(role)) ||
200
+ // onclick attribute
125
201
  el.hasAttribute('onclick') ||
126
- el.hasAttribute('tabindex') && el.getAttribute('tabindex') !== '-1' ||
127
- (el.tagName === 'DIV' && el.getAttribute('contenteditable') === 'true');
202
+ // onclick property
203
+ (el.onclick !== null && el.onclick !== undefined) ||
204
+ // cursor: pointer (only if explicitly set, not inherited)
205
+ hasCursorPointerExplicit(el) ||
206
+ // tabindex (except -1)
207
+ (el.hasAttribute('tabindex') && el.getAttribute('tabindex') !== '-1') ||
208
+ // contenteditable
209
+ el.getAttribute('contenteditable') === 'true'
210
+ // Note: We skip event listener check here for performance
211
+ // as querySelectorAll can return thousands of elements
212
+ );
128
213
 
129
214
  if (isInteractive && isVisible(el)) {
130
215
  interactiveList.push(el);
@@ -169,10 +254,17 @@ function buildAPOMTree(interactiveOnly = true) {
169
254
  return null;
170
255
  }
171
256
 
172
- // Generate unique ID
257
+ // Generate unique ID and CSS selector
173
258
  const id = generateElementId(element);
259
+ const selector = generateSelector(element);
174
260
  elementIds.set(element, id);
175
261
 
262
+ // Register element in selector resolver (internal only)
263
+ if (typeof window !== 'undefined' && typeof window.registerElement === 'function') {
264
+ const tag = element.tagName.toLowerCase();
265
+ window.registerElement(id, selector, { tag, depth });
266
+ }
267
+
176
268
  const currentPath = [...path, id];
177
269
 
178
270
  // Get positioning info
@@ -184,26 +276,47 @@ function buildAPOMTree(interactiveOnly = true) {
184
276
  // Build node - minimize non-interactive parents
185
277
  const isInteractive = elementType.isInteractive;
186
278
 
187
- const node = {
188
- id,
189
- tag: element.tagName.toLowerCase(),
190
- selector: generateSelector(element),
191
- position,
192
- children: []
193
- };
279
+ // Build node structure based on mode
280
+ let node;
194
281
 
195
- // Add full info only for interactive elements
196
282
  if (isInteractive) {
197
- node.type = elementType.type;
198
- node.bounds = getBounds(element);
283
+ // Interactive elements: full structure without selector (unless includeAll)
284
+ node = {
285
+ id,
286
+ tag: element.tagName.toLowerCase(),
287
+ position,
288
+ type: elementType.type,
289
+ children: []
290
+ };
199
291
 
200
- // Add metadata based on element type
292
+ // Add selector only in includeAll mode
293
+ if (!interactiveOnly) {
294
+ node.selector = selector;
295
+ }
296
+
297
+ // Add metadata for interactive elements
201
298
  if (elementType.metadata) {
202
299
  node.metadata = elementType.metadata;
203
300
  }
204
301
  } else {
205
- // For containers (parents), keep it minimal
206
- node.type = elementType.type;
302
+ // Containers: compact format "tag_id": [children] when interactiveOnly
303
+ // or full format when includeAll
304
+ if (interactiveOnly) {
305
+ // Compact format - will be converted after processing children
306
+ node = {
307
+ _compact: true,
308
+ _key: `${element.tagName.toLowerCase()}_${id}`,
309
+ children: []
310
+ };
311
+ } else {
312
+ // Full format with selector
313
+ node = {
314
+ id,
315
+ tag: element.tagName.toLowerCase(),
316
+ selector,
317
+ children: []
318
+ };
319
+ }
207
320
  }
208
321
 
209
322
  // Update metadata counters
@@ -231,6 +344,14 @@ function buildAPOMTree(interactiveOnly = true) {
231
344
  }
232
345
  }
233
346
 
347
+ // Convert compact containers to final format
348
+ if (node._compact) {
349
+ // Return compact format: { "tag_id": [children] }
350
+ const compactNode = {};
351
+ compactNode[node._key] = node.children;
352
+ return compactNode;
353
+ }
354
+
234
355
  return node;
235
356
  }
236
357
 
@@ -291,6 +412,87 @@ function buildAPOMTree(interactiveOnly = true) {
291
412
  };
292
413
  }
293
414
 
415
+ /**
416
+ * Check if element has click event listeners
417
+ */
418
+ function hasClickListener(element) {
419
+ try {
420
+ // Check for getEventListeners (available in Chrome DevTools context)
421
+ if (typeof getEventListeners === 'function') {
422
+ const listeners = getEventListeners(element);
423
+ return listeners && listeners.click && listeners.click.length > 0;
424
+ }
425
+
426
+ // Fallback: check for common event listener markers
427
+ // Note: This is not 100% reliable but catches common cases
428
+ return element._events?.click ||
429
+ element.__listeners?.click ||
430
+ element.__eventListeners?.click;
431
+ } catch (e) {
432
+ return false;
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Check if element is interactive based on various signals
438
+ */
439
+ function checkInteractivity(element) {
440
+ const tag = element.tagName.toLowerCase();
441
+ const role = element.getAttribute('role');
442
+
443
+ // 1. Standard interactive HTML elements
444
+ const interactiveTags = ['a', 'button', 'input', 'select', 'textarea'];
445
+ if (interactiveTags.includes(tag)) {
446
+ return { isInteractive: true, reason: 'native-html' };
447
+ }
448
+
449
+ // 2. Interactive ARIA roles
450
+ const interactiveRoles = [
451
+ 'button', 'link', 'checkbox', 'radio', 'tab',
452
+ 'menuitem', 'option', 'switch', 'textbox'
453
+ ];
454
+ if (role && interactiveRoles.includes(role)) {
455
+ return { isInteractive: true, reason: 'aria-role' };
456
+ }
457
+
458
+ // 3. Elements with onclick attribute
459
+ if (element.hasAttribute('onclick')) {
460
+ return { isInteractive: true, reason: 'onclick-attr' };
461
+ }
462
+
463
+ // 4. Elements with onclick property set via JavaScript
464
+ if (element.onclick !== null && element.onclick !== undefined) {
465
+ return { isInteractive: true, reason: 'onclick-prop' };
466
+ }
467
+
468
+ // 5. Elements with cursor: pointer (only if explicitly set, not inherited)
469
+ if (hasCursorPointerExplicit(element)) {
470
+ return { isInteractive: true, reason: 'cursor-pointer' };
471
+ }
472
+
473
+ // 6. Elements with click event listeners
474
+ if (hasClickListener(element)) {
475
+ return { isInteractive: true, reason: 'event-listener' };
476
+ }
477
+
478
+ // 7. Elements with tabindex (except -1)
479
+ const tabindex = element.getAttribute('tabindex');
480
+ if (tabindex !== null && tabindex !== '-1') {
481
+ return { isInteractive: true, reason: 'tabindex' };
482
+ }
483
+
484
+ // 8. Contenteditable elements
485
+ if (element.getAttribute('contenteditable') === 'true') {
486
+ return { isInteractive: true, reason: 'contenteditable' };
487
+ }
488
+
489
+ return { isInteractive: false, reason: null };
490
+ }
491
+
492
+ /**
493
+ * Detect framework-specific attributes on element
494
+ * Returns framework info or null
495
+ */
294
496
  /**
295
497
  * Determine element type and metadata
296
498
  */
@@ -301,14 +503,16 @@ function buildAPOMTree(interactiveOnly = true) {
301
503
 
302
504
  // Form
303
505
  if (tag === 'form') {
506
+ const metadata = {
507
+ method: element.method?.toUpperCase() || 'GET',
508
+ action: element.action || '',
509
+ name: element.name || null
510
+ };
511
+
304
512
  return {
305
513
  type: 'form',
306
514
  isInteractive: true,
307
- metadata: {
308
- method: element.method?.toUpperCase() || 'GET',
309
- action: element.action || '',
310
- name: element.name || null
311
- }
515
+ metadata
312
516
  };
313
517
  }
314
518
 
@@ -450,25 +654,32 @@ function buildAPOMTree(interactiveOnly = true) {
450
654
 
451
655
  // Container with semantic role
452
656
  if (role) {
657
+ const interactivityCheck = checkInteractivity(element);
453
658
  return {
454
659
  type: role,
455
- isInteractive: false,
660
+ isInteractive: interactivityCheck.isInteractive,
456
661
  metadata: {
457
- ariaLabel: element.getAttribute('aria-label') || null
662
+ ariaLabel: element.getAttribute('aria-label') || null,
663
+ interactivityReason: interactivityCheck.reason || undefined
458
664
  }
459
665
  };
460
666
  }
461
667
 
462
- // Generic container
668
+ // Generic container - check for JavaScript interactivity
669
+ const interactivityCheck = checkInteractivity(element);
463
670
  return {
464
671
  type: 'container',
465
- isInteractive: false,
466
- metadata: null
672
+ isInteractive: interactivityCheck.isInteractive,
673
+ metadata: interactivityCheck.isInteractive ? {
674
+ text: element.textContent?.trim().substring(0, 100) || '',
675
+ interactivityReason: interactivityCheck.reason
676
+ } : null
467
677
  };
468
678
  }
469
679
 
470
680
  /**
471
681
  * Generate unique CSS selector
682
+ * Excludes framework-specific dynamic attributes (React, Vue, Angular)
472
683
  */
473
684
  function generateSelector(element) {
474
685
  // Use ID if available and unique
@@ -476,6 +687,19 @@ function buildAPOMTree(interactiveOnly = true) {
476
687
  return `#${element.id}`;
477
688
  }
478
689
 
690
+ // Try to find stable class name (excluding framework-specific dynamic classes)
691
+ const stableClass = getStableClassName(element);
692
+ if (stableClass) {
693
+ const classSelector = `.${stableClass}`;
694
+ // Verify it's unique within parent context
695
+ if (element.parentElement) {
696
+ const matches = element.parentElement.querySelectorAll(classSelector);
697
+ if (matches.length === 1 && matches[0] === element) {
698
+ return classSelector;
699
+ }
700
+ }
701
+ }
702
+
479
703
  // Build path from parent
480
704
  const path = [];
481
705
  let current = element;
@@ -483,6 +707,12 @@ function buildAPOMTree(interactiveOnly = true) {
483
707
  while (current && current !== document.body) {
484
708
  let selector = current.tagName.toLowerCase();
485
709
 
710
+ // Add stable class if available
711
+ const stableClass = getStableClassName(current);
712
+ if (stableClass) {
713
+ selector += `.${stableClass}`;
714
+ }
715
+
486
716
  // Add nth-of-type if needed
487
717
  if (current.parentElement) {
488
718
  const siblings = Array.from(current.parentElement.children).filter(
@@ -500,6 +730,39 @@ function buildAPOMTree(interactiveOnly = true) {
500
730
 
501
731
  return path.join(' > ');
502
732
  }
733
+
734
+ /**
735
+ * Get stable class name excluding framework-specific dynamic classes
736
+ * Returns first stable class or null
737
+ */
738
+ function getStableClassName(element) {
739
+ if (!element.className || typeof element.className !== 'string') {
740
+ return null;
741
+ }
742
+
743
+ const classes = element.className.split(/\s+/).filter(c => c);
744
+
745
+ // Filter out framework-specific classes
746
+ const stableClasses = classes.filter(className => {
747
+ // React: CSS Modules, Styled Components, Emotion
748
+ if (/^[a-zA-Z0-9_-]+-[a-zA-Z0-9_-]{5,}$/.test(className)) return false;
749
+ if (/^css-[a-z0-9]+(-[a-z0-9]+)?$/i.test(className)) return false;
750
+ if (/^sc-[a-z0-9]+-[a-z0-9]+$/i.test(className)) return false;
751
+
752
+ // Vue: scoped styles
753
+ if (/^data-v-[a-f0-9]{8}$/i.test(className)) return false;
754
+
755
+ // Angular: component styles (no classes starting with _ng)
756
+ if (/^_ng/.test(className)) return false;
757
+
758
+ // Generic hash patterns
759
+ if (/^[a-z0-9]{32,}$/i.test(className)) return false;
760
+
761
+ return true;
762
+ });
763
+
764
+ return stableClasses.length > 0 ? stableClasses[0] : null;
765
+ }
503
766
  }
504
767
 
505
768
  // Export for use in both Node.js and browser context
@@ -28,7 +28,7 @@ export const toolDefinitions = [
28
28
  },
29
29
  {
30
30
  name: "click",
31
- description: "PRIMARY tool for clicking elements. PREFERRED: Use APOM ID (e.g., id: 'button_45') from analyzePage for reliable element targeting. ALTERNATIVE: CSS selector for ad-hoc usage. Works correctly with React/Vue/Angular synthetic events. DO NOT use executeScript for clicks - use this tool instead. Waits for animations and navigation.",
31
+ description: "Click element by APOM ID (preferred) or CSS selector. Handles React/Vue/Angular events, waits for navigation.",
32
32
  inputSchema: {
33
33
  type: "object",
34
34
  properties: {
@@ -42,7 +42,7 @@ export const toolDefinitions = [
42
42
  },
43
43
  {
44
44
  name: "type",
45
- description: "PRIMARY tool for filling input fields. PREFERRED: Use APOM ID (e.g., id: 'input_20') from analyzePage for reliable element targeting. ALTERNATIVE: CSS selector for ad-hoc usage. Works correctly with React/Vue/Angular state management. DO NOT use executeScript for typing - use this tool instead. Automatically updates framework state (React hooks, Vue reactive data).",
45
+ description: "Type text into input by APOM ID (preferred) or CSS selector. Updates React/Vue/Angular state automatically.",
46
46
  inputSchema: {
47
47
  type: "object",
48
48
  properties: {
@@ -55,16 +55,6 @@ export const toolDefinitions = [
55
55
  required: ["text"],
56
56
  },
57
57
  },
58
- {
59
- name: "getElement",
60
- description: "Get HTML markup of element. Prefer analyzePage for better efficiency.",
61
- inputSchema: {
62
- type: "object",
63
- properties: {
64
- selector: { type: "string", description: "CSS selector (default: body)" },
65
- },
66
- },
67
- },
68
58
  {
69
59
  name: "getComputedCss",
70
60
  description: "Get computed CSS styles for element. For layout debugging and responsive design.",
@@ -102,7 +92,7 @@ export const toolDefinitions = [
102
92
  },
103
93
  {
104
94
  name: "screenshot",
105
- description: "Capture element image (15-25k tokens). For visual comparison. Use analyzePage for form data/validation (2-5k tokens).",
95
+ description: "Capture element image (15-25k tokens). Use analyzePage for form data/validation (8-10k tokens).",
106
96
  inputSchema: {
107
97
  type: "object",
108
98
  properties: {
@@ -223,7 +213,7 @@ export const toolDefinitions = [
223
213
  },
224
214
  {
225
215
  name: "hover",
226
- description: "Hover over element. PREFERRED: Use APOM ID from analyzePage. ALTERNATIVE: CSS selector. For testing hover effects, tooltips, and CSS :hover states.",
216
+ description: "Hover over element by APOM ID or CSS selector. For hover effects, tooltips, :hover states.",
227
217
  inputSchema: {
228
218
  type: "object",
229
219
  properties: {
@@ -234,7 +224,7 @@ export const toolDefinitions = [
234
224
  },
235
225
  {
236
226
  name: "selectOption",
237
- description: "Select option in dropdown. PREFERRED: Use APOM ID from analyzePage. ALTERNATIVE: CSS selector. Works with HTML select elements. Specify value, text, or index to choose option.",
227
+ description: "Select dropdown option by APOM ID or CSS selector. Specify value, text, or index.",
238
228
  inputSchema: {
239
229
  type: "object",
240
230
  properties: {
@@ -248,7 +238,7 @@ export const toolDefinitions = [
248
238
  },
249
239
  {
250
240
  name: "drag",
251
- description: "Drag element by mouse (click-hold-move-release). Simulates real mouse drag in any direction. Works with interactive maps, Gantt charts, SVG diagrams, canvas, sliders. Does NOT work with standard overflow scrollbars - use scrollTo/scrollHorizontal instead.",
241
+ description: "Drag element in any direction. For maps, charts, SVG, canvas, sliders. Use scrollHorizontal for scrollbars.",
252
242
  inputSchema: {
253
243
  type: "object",
254
244
  properties: {
@@ -262,7 +252,7 @@ export const toolDefinitions = [
262
252
  },
263
253
  {
264
254
  name: "scrollHorizontal",
265
- description: "Scroll element horizontally. For tables, carousels, and horizontally scrollable containers. Can scroll by pixels or to the end.",
255
+ description: "Scroll element horizontally by pixels or to end. For tables, carousels, scrollable containers.",
266
256
  inputSchema: {
267
257
  type: "object",
268
258
  properties: {
@@ -487,12 +477,12 @@ export const toolDefinitions = [
487
477
  },
488
478
  {
489
479
  name: "analyzePage",
490
- description: "PRIMARY tool for reading page state. Returns APOM (Agent Page Object Model) format by default with unique element IDs. Use element IDs with click/type tools instead of CSS selectors. Use refresh:true after clicks/submissions to see updated state. Efficient: 2-5k tokens vs screenshot 15-25k. Set useLegacyFormat:true for old format.",
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.",
491
481
  inputSchema: {
492
482
  type: "object",
493
483
  properties: {
494
484
  refresh: { type: "boolean", description: "Refresh cache (default: false)" },
495
- includeAll: { type: "boolean", description: "Include all elements on page, not just interactive ones (default: false)" },
485
+ includeAll: { type: "boolean", description: "Include all elements with selectors - full debug format (default: false for compact format)" },
496
486
  useLegacyFormat: { type: "boolean", description: "Return legacy format instead of APOM (default: false - APOM is now default)" },
497
487
  registerElements: { type: "boolean", description: "Auto-register elements in selector resolver (default: true)" },
498
488
  groupBy: { type: "string", description: "Group elements: 'type' or 'flat' (default: 'type')", enum: ["type", "flat"] },
@@ -500,12 +490,15 @@ export const toolDefinitions = [
500
490
  },
501
491
  },
502
492
  {
503
- name: "getElementByApomId",
504
- description: "Get detailed information about element by its APOM ID from analyzePage. Returns full element details including bounds, position, attributes, computed styles. Use this to inspect specific elements without re-analyzing entire page.",
493
+ name: "getElementDetails",
494
+ description: "Get detailed information about element by its APOM ID. Returns full element details including bounds, CSS selector, position, attributes, and computed styles. Can also analyze children elements tree structure. Use this when analyzePage output was simplified and you need complete information about specific element or analyze specific sections in detail.",
505
495
  inputSchema: {
506
496
  type: "object",
507
497
  properties: {
508
498
  id: { type: "string", description: "APOM element ID (e.g., 'input_20', 'button_45') from analyzePage result" },
499
+ analyzeChildren: { type: "boolean", description: "Analyze children elements tree structure (default: false)" },
500
+ includeAll: { type: "boolean", description: "When analyzing children, include all elements, not just interactive ones (default: false)" },
501
+ refresh: { type: "boolean", description: "Force refresh of cached analysis (default: false)" },
509
502
  },
510
503
  required: ["id"],
511
504
  },
@@ -564,7 +557,7 @@ export const toolDefinitions = [
564
557
  },
565
558
  {
566
559
  name: "enableRecorder",
567
- description: "Check ChromeTools Extension connection status for scenario recording. Recording is now handled via Chrome Extension popup (click CT icon in Chrome toolbar). Scenarios are stored in ~/.config/chrometools-mcp/projects/{projectName}/scenarios/.",
560
+ description: "Check ChromeTools Extension connection for scenario recording. Use Chrome Extension popup (CT icon) for recording.",
568
561
  inputSchema: {
569
562
  type: "object",
570
563
  properties: {},
@@ -572,7 +565,7 @@ export const toolDefinitions = [
572
565
  },
573
566
  {
574
567
  name: "startRecording",
575
- description: "Start recording user actions into a scenario. Recording follows the active tab automatically. Use stopRecording to finish and save the scenario.",
568
+ description: "Start recording user actions. Follows active tab automatically. Use stopRecording to finish.",
576
569
  inputSchema: {
577
570
  type: "object",
578
571
  properties: {
@@ -641,7 +634,7 @@ export const toolDefinitions = [
641
634
  },
642
635
  {
643
636
  name: "executeScenario",
644
- description: "Execute recorded scenario by name. Runs actions with dependency resolution. Scenarios are organized by domain in ~/.config/chrometools-mcp/projects/{domain}/scenarios/. If multiple scenarios have the same name across different domains, specify projectId to disambiguate.",
637
+ description: "Execute scenario by name with dependency resolution. Use projectId to disambiguate duplicate names.",
645
638
  inputSchema: {
646
639
  type: "object",
647
640
  properties: {
@@ -655,7 +648,7 @@ export const toolDefinitions = [
655
648
  },
656
649
  {
657
650
  name: "listScenarios",
658
- description: "List all scenarios with metadata. Scenarios are stored in ~/.config/chrometools-mcp/projects/{projectName}/scenarios/. Use global index at ~/.config/chrometools-mcp/index.json to discover available projects and scenarios.",
651
+ description: "List all scenarios with metadata.",
659
652
  inputSchema: {
660
653
  type: "object",
661
654
  properties: {
@@ -665,7 +658,7 @@ export const toolDefinitions = [
665
658
  },
666
659
  {
667
660
  name: "searchScenarios",
668
- description: "Search scenarios by text or tags. Scenarios are stored in ~/.config/chrometools-mcp/projects/{projectName}/scenarios/. Use global index at ~/.config/chrometools-mcp/index.json to discover available projects and scenarios.",
661
+ description: "Search scenarios by text or tags.",
669
662
  inputSchema: {
670
663
  type: "object",
671
664
  properties: {
@@ -677,7 +670,7 @@ export const toolDefinitions = [
677
670
  },
678
671
  {
679
672
  name: "getScenarioInfo",
680
- description: "Get scenario details: actions, parameters, dependencies. Scenarios are stored in ~/.config/chrometools-mcp/projects/{projectName}/scenarios/. Use global index at ~/.config/chrometools-mcp/index.json to discover available projects and scenarios.",
673
+ description: "Get scenario details: actions, parameters, dependencies.",
681
674
  inputSchema: {
682
675
  type: "object",
683
676
  properties: {
@@ -689,7 +682,7 @@ export const toolDefinitions = [
689
682
  },
690
683
  {
691
684
  name: "deleteScenario",
692
- description: "Delete scenario and secrets. Scenarios are stored in ~/.config/chrometools-mcp/projects/{projectName}/scenarios/. Use global index at ~/.config/chrometools-mcp/index.json to discover available projects and scenarios.",
685
+ description: "Delete scenario and secrets.",
693
686
  inputSchema: {
694
687
  type: "object",
695
688
  properties: {
@@ -700,7 +693,7 @@ export const toolDefinitions = [
700
693
  },
701
694
  {
702
695
  name: "exportScenarioAsCode",
703
- description: "Export recorded scenario as executable test code for creating a NEW test file. Automatically cleans unstable selectors (CSS modules, styled-components). Optionally generates Page Object class. Returns JSON with code and suggested filename - Claude Code will create the file. To add tests to EXISTING files, use 'appendScenarioToFile' instead. Scenarios are stored in ~/.config/chrometools-mcp/projects/{projectName}/scenarios/. Use global index at ~/.config/chrometools-mcp/index.json to discover available projects and scenarios.",
696
+ description: "Export scenario as test code for NEW file. Cleans unstable selectors, optionally generates Page Object. Use appendScenarioToFile for existing files.",
704
697
  inputSchema: {
705
698
  type: "object",
706
699
  properties: {
@@ -735,7 +728,7 @@ export const toolDefinitions = [
735
728
  },
736
729
  {
737
730
  name: "appendScenarioToFile",
738
- description: "Append recorded scenario as test code to an EXISTING test file. Automatically cleans unstable selectors (CSS modules, styled-components). Optionally generates Page Object class. Returns JSON with test code and target file - Claude Code will append to the file without overwriting existing tests. To create NEW test files, use 'exportScenarioAsCode' instead. Scenarios are stored in ~/.config/chrometools-mcp/projects/{projectName}/scenarios/. Use global index at ~/.config/chrometools-mcp/index.json to discover available projects and scenarios.",
731
+ description: "Append scenario as test code to EXISTING file. Cleans unstable selectors, optionally generates Page Object. Use exportScenarioAsCode for new files.",
739
732
  inputSchema: {
740
733
  type: "object",
741
734
  properties: {
@@ -35,10 +35,6 @@ export const TypeSchema = z.object({
35
35
  message: "Either 'id' or 'selector' must be provided, but not both"
36
36
  });
37
37
 
38
- export const GetElementSchema = z.object({
39
- selector: z.string().optional().describe("CSS selector (optional, defaults to body)"),
40
- });
41
-
42
38
  export const HoverSchema = z.object({
43
39
  id: z.string().optional().describe("APOM element ID from analyzePage. Mutually exclusive with selector."),
44
40
  selector: z.string().optional().describe("CSS selector for element to hover. Mutually exclusive with id."),
@@ -260,14 +256,17 @@ export const SmartFindElementSchema = z.object({
260
256
 
261
257
  export const AnalyzePageSchema = z.object({
262
258
  refresh: z.boolean().optional().describe("Force refresh of cached analysis (default: false)"),
263
- includeAll: z.boolean().optional().describe("Include all elements on page, not just interactive ones (default: false). Useful for layout work and finding non-interactive elements to style."),
259
+ includeAll: z.boolean().optional().describe("Include all elements on page, not just interactive ones (default: false). When false (default), returns compact format: containers as \"tag_id\":[children], interactive elements without selectors. When true, returns full format with selectors for debugging."),
264
260
  useLegacyFormat: z.boolean().optional().describe("Return legacy format instead of APOM (default: false - APOM is now the default format)"),
265
261
  registerElements: z.boolean().optional().describe("Automatically register elements in selector resolver (default: true)"),
266
262
  groupBy: z.enum(['type', 'flat']).optional().describe("Group elements by type or return flat structure (default: 'type')"),
267
263
  });
268
264
 
269
- export const GetElementByApomIdSchema = z.object({
265
+ export const GetElementDetailsSchema = z.object({
270
266
  id: z.string().describe("APOM element ID (e.g., 'input_20', 'button_45') from analyzePage result"),
267
+ analyzeChildren: z.boolean().optional().describe("Analyze children elements tree structure (default: false)"),
268
+ includeAll: z.boolean().optional().describe("When analyzing children, include all elements, not just interactive ones (default: false)"),
269
+ refresh: z.boolean().optional().describe("Force refresh of cached analysis (default: false)"),
271
270
  });
272
271
 
273
272
  export const GetAllInteractiveElementsSchema = z.object({