mcp-web-inspector 0.4.5 → 0.5.3

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/README.md CHANGED
@@ -595,7 +595,7 @@ COMPARE TWO ELEMENTS: Get comprehensive alignment and dimension comparison in on
595
595
  - selector2 (string, required): CSS selector, text selector, or testid shorthand for the second element (e.g., 'testid:chat-header', '#secondary-header')
596
596
 
597
597
  - Output Format:
598
- - Optional warnings when a selector matched multiple elements (using first).
598
+ - Optional warnings when a selector matched multiple elements (uses first visible; suggests adding unique data-testid).
599
599
  - Header: Alignment: <elem1> vs <elem2>
600
600
  - Two lines with each element's position and size: @ (x,y) w×h px
601
601
  - Edges block: Top/Left/Right/Bottom with ✓/✗ and diffs
@@ -997,14 +997,13 @@ Upload a file to an input[type='file'] element on the page
997
997
  ### Content
998
998
 
999
999
  #### `get_html`
1000
- ⚠️ RARELY NEEDED: Get raw HTML markup from the page (no rendering, just source code). Most tasks need structured inspection instead. ONLY use get_html for: (1) checking specific HTML attributes or element nesting, (2) analyzing markup structure, (3) debugging SSR/HTML issues. For structured tasks, use: inspect_dom() to understand page structure with positions, query_selector() to find and inspect elements, get_computed_styles() for CSS values. Auto-returns HTML if <2000 chars (small elements), shows preview with token-based confirmation if larger. Scripts removed by default for security/size. Supports testid shortcuts.
1000
+ ⚠️ RARELY NEEDED: Get raw HTML markup from the page (no rendering, just source code). Most tasks need structured inspection instead. ONLY use get_html for: (1) checking specific HTML attributes or element nesting, (2) analyzing markup structure, (3) debugging SSR/HTML issues. For structured tasks, use: inspect_dom() to understand page structure with positions, query_selector() to find and inspect elements, get_computed_styles() for CSS values. Auto-returns HTML if <2000 chars (small elements); if larger, returns a preview and a one-time token to fetch the full output. Scripts removed by default for security/size. Supports testid shortcuts.
1001
1001
 
1002
1002
  - Parameters:
1003
1003
  - selector (string, optional): CSS selector, text selector, or testid shorthand to limit HTML extraction to a specific container. Omit to get entire page HTML. Example: 'testid:main-content' or '#app'
1004
1004
  - elementIndex (number, optional): When selector matches multiple elements, use this 1-based index to select a specific one (e.g., 2 = second element). Default: first visible element.
1005
1005
  - clean (boolean, optional): Remove noise from HTML: false (default) = remove scripts only, true = remove scripts + styles + comments + meta tags for minimal markup
1006
1006
  - maxLength (number, optional): Maximum number of characters to return (default: 20000)
1007
- - confirmToken (string, optional): Confirmation token from preview response (required to retrieve large HTML). Get this token by calling without confirmToken first - the preview will include the token to use.
1008
1007
 
1009
1008
  #### `get_text`
1010
1009
  ⚠️ RARELY NEEDED: Get ALL visible text content from the entire page (no structure, just raw text). Most tasks need structured inspection instead. ONLY use get_text for: (1) extracting text for content analysis (word count, language detection), (2) searching for text when location is completely unknown, (3) text-only snapshots for comparison. For structured tasks, use: inspect_dom() to understand page structure, find_by_text() to locate specific text with context, query_selector() to find elements. Returns plain text up to 20000 chars (truncated if longer). Supports testid shortcuts.
@@ -1032,8 +1031,6 @@ Upload a file to an input[type='file'] element on the page
1032
1031
 
1033
1032
  ⚠️ Token cost: ~1,500 tokens to read. Structural tools: <100 tokens.
1034
1033
 
1035
- Admin control (optional): set env MCP_SCREENSHOT_GUARD=strict to block execution (prevents misuse by default). Unset to allow visuals for human review.
1036
-
1037
1034
  Screenshots saved to ./.mcp-web-inspector/screenshots. Example: { name: "login-page", fullPage: true } or { name: "submit-btn", selector: "testid:submit" }
1038
1035
 
1039
1036
  - Parameters:
@@ -1048,7 +1045,7 @@ Screenshots saved to ./.mcp-web-inspector/screenshots. Example: { name: "login-p
1048
1045
  Clears captured console logs and returns the number of entries cleared.
1049
1046
 
1050
1047
  #### `get_console_logs`
1051
- Retrieve console logs with filtering and token‑efficient output. Defaults: since='last-interaction', limit=20, format='grouped'. Grouped output deduplicates identical lines and shows counts. Use format='raw' for chronological, ungrouped lines. Large outputs return a preview and require confirmToken to fetch the full payload.
1048
+ Retrieve console logs with filtering and token‑efficient output. Defaults: since='last-interaction', limit=20, format='grouped'. Grouped output deduplicates identical lines and shows counts. Use format='raw' for chronological, ungrouped lines. Large outputs return a preview and a one-time token to fetch the full payload.
1052
1049
 
1053
1050
  - Parameters:
1054
1051
  - type (string, optional): Type of logs to retrieve (all, error, warning, log, info, debug, exception)
@@ -1056,16 +1053,28 @@ Retrieve console logs with filtering and token‑efficient output. Defaults: sin
1056
1053
  - limit (number, optional): Maximum entries to return (groups when grouped, lines when raw). Default: 20
1057
1054
  - since (string, optional): Filter logs since a specific event: 'last-call' (since last get_console_logs call), 'last-navigation' (since last page navigation), or 'last-interaction' (since last user interaction like click, fill, etc.). Default: 'last-interaction'
1058
1055
  - format (string, optional): Output format: 'grouped' (default, deduped with counts) or 'raw' (chronological, ungrouped)
1059
- - confirmToken (string, optional): One-time token to return large outputs. Obtain it by calling without confirmToken to receive a preview.
1060
1056
 
1061
1057
  ### Evaluation
1062
1058
 
1063
1059
  #### `evaluate`
1064
- ⚙️ CUSTOM JAVASCRIPT EXECUTION - Execute arbitrary JavaScript in the browser console and return the result (JSON-stringified). ⚠️ NOT for: scroll detection (inspect_dom shows 'scrollable ↕️'), element dimensions (use measure_element), DOM inspection (use inspect_dom), CSS properties (use get_computed_styles), position comparison (use compare_positions). Use ONLY when specialized tools cannot accomplish the task. Essential for: custom page interactions, complex calculations not covered by other tools. Automatically detects common patterns and suggests better alternatives. High flexibility but less efficient than specialized tools.
1060
+ ⚙️ CUSTOM JAVASCRIPT EXECUTION - Execute arbitrary JavaScript in the browser console and return a compact, token-efficient summary of the result. Includes a large-output preview guard with a one-time token. ⚠️ NOT for: scroll detection (inspect_dom shows 'scrollable ↕️'), element dimensions (use measure_element), DOM inspection (use inspect_dom), CSS properties (use get_computed_styles), position comparison (use compare_element_alignment). Use ONLY when specialized tools cannot accomplish the task. Automatically detects common patterns and suggests better alternatives.
1065
1061
 
1066
1062
  - Parameters:
1067
1063
  - script (string, required): JavaScript code to execute
1068
1064
 
1065
+ - Output Format:
1066
+ - Header: '✓ JavaScript execution result:'
1067
+ - Default result: compact summary string (arrays/objects/dom nodes summarized)
1068
+ - Array summary: 'Array(n) [first, second, third…]' (shows first 3 items)
1069
+ - Object summary (large): 'Object(n keys): key1, key2, key3…' (top-level keys only)
1070
+ - DOM node summary: '<tag id=#id class=.a.b> @ (x,y) WxH' (rounded ints)
1071
+ - NodeList/HTMLCollection summary: 'NodeList(n) [<div…>, <span…>, <a…>…]'
1072
+ - Preview guard when result is large (≥ ~2000 chars):
1073
+ - 'Preview (first 500 chars):' followed by excerpt
1074
+ - Counts: 'totalLength: N, shownLength: M, truncated: true'
1075
+ - One-time token string to fetch full output
1076
+ - Suggestions block (conditional): compact tips for specialized tools based on script patterns
1077
+
1069
1078
  ### Network
1070
1079
 
1071
1080
  #### `get_request_details`
@@ -1107,6 +1116,18 @@ Set the browser color scheme that controls CSS prefers-color-scheme. Defaults to
1107
1116
 
1108
1117
  - Parameters:
1109
1118
  - scheme (string, required): Color scheme to emulate: 'system', 'dark', 'light', or 'no-preference'. Example: { scheme: 'dark' }
1119
+
1120
+ ### Other
1121
+
1122
+ #### `confirm_output`
1123
+ Return full output for a previously previewed large result using a one-time token. Use when a tool responded with a preview + token. Safer than resending original parameters.
1124
+
1125
+ - Parameters:
1126
+ - token (string, required): One-time token obtained from a tool's preview response
1127
+
1128
+ - Output Format:
1129
+ - Full original payload if token is valid (one-time)
1130
+ - Error: 'Invalid or expired token'
1110
1131
  ## Selector Shortcuts ⭐ Time-Saver
1111
1132
 
1112
1133
  All browser tools support **convenient test ID shortcuts** that save typing and improve readability:
@@ -12,18 +12,23 @@ export declare abstract class BrowserToolBase implements ToolHandler {
12
12
  */
13
13
  abstract execute(args: any, context: ToolContext): Promise<ToolResponse>;
14
14
  /**
15
- * Normalize selector shortcuts to full Playwright selectors and clean common escaping mistakes
16
- * - "testid:foo" → "[data-testid='foo']"
17
- * - "data-test:bar" → "[data-test='bar']"
18
- * - "data-cy:baz" → "[data-cy='baz']"
19
- * - Removes unnecessary backslash escapes from CSS selectors that LLMs often add
20
- * - ".dark\\:bg-gray-700" → ".dark:bg-gray-700"
21
- * - ".top-\\[36px\\]" ".top-[36px]"
22
- * - ".top-\\\\[36px\\\\]" ".top-[36px]" (double-escaped)
15
+ * Normalize selector shortcuts and fix common escaping mistakes safely.
16
+ * - "testid:foo" → "[data-testid=\"foo\"]"
17
+ * - "data-test:bar" → "[data-test=\"bar\"]"
18
+ * - "data-cy:baz" → "[data-cy=\"baz\"]"
19
+ * - Convert simple ID-only selectors with special chars to Playwright's id engine:
20
+ * "#radix-\:rc\:-content-123" → "id=radix-:rc:-content-123"
21
+ * - Remove unnecessary escapes for bracket characters only (\\[ and \\])
22
+ * DO NOT unescape colons globally — colons in class/ID names must stay escaped in CSS.
23
23
  * @param selector The selector string
24
24
  * @returns Normalized selector
25
25
  */
26
26
  protected normalizeSelector(selector: string): string;
27
+ /**
28
+ * Sanitize verbose Playwright selector engine messages by removing stack traces and
29
+ * keeping only the essential syntax error information.
30
+ */
31
+ protected sanitizeSelectorEngineMessage(msg: string): string;
27
32
  /**
28
33
  * Ensures a page is available and returns it
29
34
  * @param context The tool context containing browser and page
@@ -8,14 +8,14 @@ export class BrowserToolBase {
8
8
  this.server = server;
9
9
  }
10
10
  /**
11
- * Normalize selector shortcuts to full Playwright selectors and clean common escaping mistakes
12
- * - "testid:foo" → "[data-testid='foo']"
13
- * - "data-test:bar" → "[data-test='bar']"
14
- * - "data-cy:baz" → "[data-cy='baz']"
15
- * - Removes unnecessary backslash escapes from CSS selectors that LLMs often add
16
- * - ".dark\\:bg-gray-700" → ".dark:bg-gray-700"
17
- * - ".top-\\[36px\\]" ".top-[36px]"
18
- * - ".top-\\\\[36px\\\\]" ".top-[36px]" (double-escaped)
11
+ * Normalize selector shortcuts and fix common escaping mistakes safely.
12
+ * - "testid:foo" → "[data-testid=\"foo\"]"
13
+ * - "data-test:bar" → "[data-test=\"bar\"]"
14
+ * - "data-cy:baz" → "[data-cy=\"baz\"]"
15
+ * - Convert simple ID-only selectors with special chars to Playwright's id engine:
16
+ * "#radix-\:rc\:-content-123" → "id=radix-:rc:-content-123"
17
+ * - Remove unnecessary escapes for bracket characters only (\\[ and \\])
18
+ * DO NOT unescape colons globally — colons in class/ID names must stay escaped in CSS.
19
19
  * @param selector The selector string
20
20
  * @returns Normalized selector
21
21
  */
@@ -32,16 +32,58 @@ export class BrowserToolBase {
32
32
  return `[${attr}="${value}"]`;
33
33
  }
34
34
  }
35
- // Clean up common escaping mistakes that LLMs make in CSS selectors
36
- // These characters don't need to be escaped in CSS selectors: [ ] :
37
- let cleaned = selector;
38
- // Remove backslash escapes before brackets and colons
39
- // Handle both single escapes (\[) and double escapes (\\[)
40
- cleaned = cleaned.replace(/\\+\[/g, '[');
41
- cleaned = cleaned.replace(/\\+\]/g, ']');
42
- cleaned = cleaned.replace(/\\+:/g, ':');
35
+ const trimmed = selector.trim();
36
+ // Helper: unescape simple backslash-escapes used inside IDs (e.g., \:, \[, \])
37
+ const unescapeCssIdentifier = (s) => {
38
+ // Collapse multiple backslashes before a single char to the char itself
39
+ return s
40
+ .replace(/\\+:/g, ':')
41
+ .replace(/\\+\[/g, '[')
42
+ .replace(/\\+\]/g, ']');
43
+ };
44
+ // If this looks like a simple, standalone ID selector (no combinators or descendants),
45
+ // switch to Playwright's id engine. This avoids CSS escaping pitfalls with colons.
46
+ if (/^#[^\s>+~]+$/.test(trimmed)) {
47
+ const idToken = trimmed.slice(1);
48
+ // Only switch to id= engine if ID contains characters that commonly break CSS (#... with colons or escapes)
49
+ if (idToken.includes('\\') || idToken.includes(':') || idToken.includes('[') || idToken.includes(']')) {
50
+ const idValue = unescapeCssIdentifier(idToken);
51
+ return `id=${idValue}`;
52
+ }
53
+ // Otherwise, keep simple IDs as-is
54
+ return trimmed;
55
+ }
56
+ // For general CSS selectors, preserve required escapes for special chars.
57
+ // Collapse over-escaping (e.g., \\\\[ → \\[, but keep a single backslash before [ ] :)
58
+ let cleaned = trimmed;
59
+ cleaned = cleaned.replace(/\\{2,}(?=\[)/g, '\\');
60
+ cleaned = cleaned.replace(/\\{2,}(?=\])/g, '\\');
61
+ cleaned = cleaned.replace(/\\{2,}(?=:)/g, '\\');
43
62
  return cleaned;
44
63
  }
64
+ /**
65
+ * Sanitize verbose Playwright selector engine messages by removing stack traces and
66
+ * keeping only the essential syntax error information.
67
+ */
68
+ sanitizeSelectorEngineMessage(msg) {
69
+ if (!msg)
70
+ return '';
71
+ // Prefer to cut at the common phrase used by the browser
72
+ const cutoffPhrases = [
73
+ "is not a valid selector.",
74
+ "is not a valid selector",
75
+ ];
76
+ for (const phrase of cutoffPhrases) {
77
+ const idx = msg.indexOf(phrase);
78
+ if (idx !== -1) {
79
+ return msg.slice(0, idx + phrase.length).trim();
80
+ }
81
+ }
82
+ // Otherwise remove stack-like lines (e.g., " at query (…)")
83
+ const lines = msg.split(/\r?\n/);
84
+ const filtered = lines.filter(l => !/^\s*at\b/.test(l) && !/<anonymous>:\d+:\d+/.test(l));
85
+ return filtered.join('\n').trim();
86
+ }
45
87
  /**
46
88
  * Ensures a page is available and returns it
47
89
  * @param context The tool context containing browser and page
@@ -164,20 +206,23 @@ export class BrowserToolBase {
164
206
  errorMsg.includes('Invalid selector') ||
165
207
  errorMsg.includes('SyntaxError') ||
166
208
  errorMsg.includes('selector')) {
209
+ const conciseMsg = this.sanitizeSelectorEngineMessage(errorMsg);
210
+ // Helpful, accurate guidance with Tailwind-style examples
211
+ const tips = [
212
+ '💡 Tips:',
213
+ ' • Tailwind arbitrary values need escaping in class selectors: .min-w-\\[300px\\]',
214
+ ' • Colons in class names must be escaped: .dark\\:bg-gray-700',
215
+ ' • Prefer robust selectors: use testid:name or [data-testid="..."]',
216
+ ' • Attribute selectors avoid escaping issues: [class*="min-w-[300px]"]',
217
+ '',
218
+ 'Examples:',
219
+ ' ✓ .min-w-\\[300px\\] .flex-1',
220
+ ' ✓ testid:submit-button',
221
+ ' ✓ #login-form'
222
+ ].join('\n');
167
223
  throw new Error(`Invalid CSS selector: "${selector}"\n\n` +
168
- `Selector syntax error: ${errorMsg}\n\n` +
169
- `💡 Tips:\n` +
170
- ` • CSS selectors don't need backslash escapes for [ ] or :\n` +
171
- ` • Use .class-name or #id without escaping\n` +
172
- ` • For data attributes, use [data-attr="value"]\n` +
173
- ` • For testid shortcuts, use testid:name\n\n` +
174
- `Examples:\n` +
175
- ` ✓ .dark:bg-gray-700\n` +
176
- ` ✓ .top-[36px]\n` +
177
- ` ✓ testid:submit-button\n` +
178
- ` ✓ #login-form\n` +
179
- ` ✗ .dark\\:bg-gray-700 (unnecessary escape)\n` +
180
- ` ✗ .top-\\[36px\\] (unnecessary escape)`);
224
+ (conciseMsg ? `Selector syntax error: ${conciseMsg}\n\n` : '') +
225
+ tips);
181
226
  }
182
227
  // Re-throw other errors as-is
183
228
  throw error;
@@ -248,12 +293,19 @@ export class BrowserToolBase {
248
293
  * @returns Formatted string or empty if only one element
249
294
  */
250
295
  formatElementSelectionInfo(selector, elementIndex, totalCount, preferredVisible = true) {
296
+ const usesNth = selector.includes('>> nth=');
251
297
  if (totalCount <= 1) {
298
+ // Even when a single element is ultimately targeted, discourage nth usage
299
+ // because it is brittle across layout/content changes.
300
+ if (usesNth) {
301
+ return "💡 Tip: Selector uses '>> nth='. Prefer adding a unique data-testid for robust selection.";
302
+ }
252
303
  return '';
253
304
  }
254
305
  const duplicateWarning = this.getDuplicateTestIdWarning(selector, totalCount).trimEnd();
255
306
  const nthHint = this.buildNthSelectorHint(selector, totalCount).trimEnd();
256
- const extraHints = [duplicateWarning, nthHint].filter(Boolean).join('\n');
307
+ const avoidNth = usesNth ? "💡 Tip: Avoid relying on '>> nth='; add a unique data-testid instead." : '';
308
+ const extraHints = [duplicateWarning, nthHint, avoidNth].filter(Boolean).join('\n');
257
309
  const baseMessage = preferredVisible
258
310
  ? `⚠ Found ${totalCount} elements matching "${selector}", using element ${elementIndex + 1} (first visible)`
259
311
  : `⚠ Found ${totalCount} elements matching "${selector}", using element ${elementIndex + 1}`;
@@ -276,10 +328,14 @@ export class BrowserToolBase {
276
328
  selector.startsWith('data-cy:') ||
277
329
  selector.match(/^\[data-(testid|test|cy)=/);
278
330
  if (isTestIdSelector) {
279
- return `💡 Tip: Test IDs should be unique. Consider making this test ID unique to avoid ambiguity.\n\n`;
331
+ return (`💡 Tip: Test IDs should be unique. Consider making this test ID unique to avoid ambiguity.\n` +
332
+ ` Primary fix: assign a unique data-testid to the intended element.\n` +
333
+ ` Workaround: if you cannot change markup, you may use '>> nth=<index>' temporarily.\n\n`);
280
334
  }
281
335
  // Suggest testid for non-testid selectors
282
- return `💡 Tip: Consider adding a unique data-testid attribute for more reliable selection.\n\n`;
336
+ return (`💡 Tip: Consider adding a unique data-testid attribute for more reliable selection.\n` +
337
+ ` Primary fix: add data-testid and target it (e.g., testid:submit).\n` +
338
+ ` Workaround: use '>> nth=<index>' only when you can't add test IDs.\n\n`);
283
339
  }
284
340
  /**
285
341
  * Provide a hint for using >> nth= when multiple elements match a selector
@@ -295,6 +351,11 @@ export class BrowserToolBase {
295
351
  const firstExample = `${trimmed} >> nth=0`;
296
352
  const lastIndex = Math.max(totalCount - 1, 1);
297
353
  const lastExample = `${trimmed} >> nth=${lastIndex}`;
298
- return `💡 Hint: Append ">> nth=<index>" to target a specific match.\n Example: ${firstExample} (first match)\n Or: ${lastExample} (last match)`;
354
+ return (`Primary fix: add a unique data-testid to the intended element and select it directly.\n` +
355
+ `Workaround: Append ">> nth=<index>" to target a specific match when you cannot change markup.\n` +
356
+ ` Example: ${firstExample} (first match)\n` +
357
+ ` Or: ${lastExample} (last match)\n` +
358
+ `Note: nth selectors are brittle and may break with layout/content changes.\n` +
359
+ `Prefer unique data-testid attributes for long-term stability.`);
299
360
  }
300
361
  }
@@ -195,4 +195,46 @@ describe('GetConsoleLogsTool', () => {
195
195
  // (The truncation happens in toolHandler.ts before calling registerConsoleMessage)
196
196
  expect(logsText).toContain(longStackError);
197
197
  });
198
+ test('raw format preview should include confirm token and confirmation returns full payload', async () => {
199
+ // Generate enough long logs to exceed preview threshold
200
+ for (let i = 0; i < 40; i++) {
201
+ consoleLogsTool.registerConsoleMessage('log', `RAW-LONG-${i} ` + 'X'.repeat(160));
202
+ }
203
+ const previewResult = await consoleLogsTool.execute({ format: 'raw' }, mockContext);
204
+ expect(previewResult.isError).toBe(false);
205
+ const previewText = previewResult.content.map(c => c.text).join('\n');
206
+ // Should include confirm_output token reference
207
+ expect(previewText).toMatch(/confirm_output\(\{ token: \"([\w\d]+)\" \}\)/);
208
+ const token = (previewText.match(/confirm_output\(\{ token: \"([\w\d]+)\" \}\)/) || [])[1];
209
+ // Confirm to retrieve full payload
210
+ const { ConfirmOutputTool } = await import('../../../common/confirm_output.js');
211
+ const confirmTool = new ConfirmOutputTool({});
212
+ const full = await confirmTool.execute({ token }, mockContext);
213
+ expect(full.isError).toBe(false);
214
+ const fullText = full.content.map(c => c.text).join('\n');
215
+ // By default limit=20 → header should reflect 20 lines
216
+ expect(fullText).toContain('Retrieved 20 console log(s):');
217
+ // And include one of our long messages
218
+ expect(fullText).toContain('RAW-LONG-39');
219
+ });
220
+ test('grouped format preview should include confirm token and confirmation returns full payload', async () => {
221
+ // Generate 30 unique messages to form distinct groups and exceed threshold
222
+ for (let i = 0; i < 30; i++) {
223
+ consoleLogsTool.registerConsoleMessage('log', `GROUP-LONG-${i} ` + 'G'.repeat(160));
224
+ }
225
+ const previewResult = await consoleLogsTool.execute({}, mockContext); // grouped is default
226
+ expect(previewResult.isError).toBe(false);
227
+ const previewText = previewResult.content.map(c => c.text).join('\n');
228
+ expect(previewText).toMatch(/confirm_output\(\{ token: \"([\w\d]+)\" \}\)/);
229
+ const token = (previewText.match(/confirm_output\(\{ token: \"([\w\d]+)\" \}\)/) || [])[1];
230
+ const { ConfirmOutputTool } = await import('../../../common/confirm_output.js');
231
+ const confirmTool = new ConfirmOutputTool({});
232
+ const full = await confirmTool.execute({ token }, mockContext);
233
+ expect(full.isError).toBe(false);
234
+ const fullText = full.content.map(c => c.text).join('\n');
235
+ // Header reflects number of groups shown (limit default 20)
236
+ expect(fullText).toContain('Retrieved 20 console log(s):');
237
+ // Should include one of the first 20 grouped messages
238
+ expect(fullText).toContain('GROUP-LONG-0');
239
+ });
198
240
  });
@@ -8,7 +8,6 @@ export declare class GetConsoleLogsTool extends BrowserToolBase {
8
8
  private lastCallTimestamp;
9
9
  private lastNavigationTimestamp;
10
10
  private lastInteractionTimestamp;
11
- private confirmTokens;
12
11
  static latestInstance: GetConsoleLogsTool | null;
13
12
  constructor(server: any);
14
13
  static getMetadata(sessionConfig?: SessionConfig): ToolMetadata;
@@ -1,5 +1,6 @@
1
1
  import { BrowserToolBase } from '../base.js';
2
2
  import { createSuccessResponse } from '../../common/types.js';
3
+ import { makeConfirmPreview } from '../../common/confirm_output.js';
3
4
  /**
4
5
  * Tool for retrieving and filtering console logs from the browser
5
6
  */
@@ -11,13 +12,12 @@ export class GetConsoleLogsTool extends BrowserToolBase {
11
12
  this.lastCallTimestamp = 0;
12
13
  this.lastNavigationTimestamp = 0;
13
14
  this.lastInteractionTimestamp = 0;
14
- this.confirmTokens = new Map();
15
15
  GetConsoleLogsTool.latestInstance = this;
16
16
  }
17
17
  static getMetadata(sessionConfig) {
18
18
  return {
19
19
  name: "get_console_logs",
20
- description: "Retrieve console logs with filtering and token‑efficient output. Defaults: since='last-interaction', limit=20, format='grouped'. Grouped output deduplicates identical lines and shows counts. Use format='raw' for chronological, ungrouped lines. Large outputs return a preview and require confirmToken to fetch the full payload.",
20
+ description: "Retrieve console logs with filtering and token‑efficient output. Defaults: since='last-interaction', limit=20, format='grouped'. Grouped output deduplicates identical lines and shows counts. Use format='raw' for chronological, ungrouped lines. Large outputs return a preview and a one-time token to fetch the full payload.",
21
21
  inputSchema: {
22
22
  type: "object",
23
23
  properties: {
@@ -44,10 +44,6 @@ export class GetConsoleLogsTool extends BrowserToolBase {
44
44
  description: "Output format: 'grouped' (default, deduped with counts) or 'raw' (chronological, ungrouped)",
45
45
  enum: ["grouped", "raw"]
46
46
  },
47
- confirmToken: {
48
- type: "string",
49
- description: "One-time token to return large outputs. Obtain it by calling without confirmToken to receive a preview."
50
- }
51
47
  },
52
48
  required: [],
53
49
  },
@@ -121,27 +117,17 @@ export class GetConsoleLogsTool extends BrowserToolBase {
121
117
  // Guard large outputs by character size
122
118
  const header = `Retrieved ${messages.length} console log(s):`;
123
119
  const textPayload = [header, ...messages].join('\n');
124
- const tokenKey = `raw:${sinceArg}:${args.type || 'all'}:${args.search || ''}:${limit}:${messages.length}`;
125
120
  if (textPayload.length >= PREVIEW_THRESHOLD) {
126
- const expected = this.confirmTokens.get(tokenKey);
127
- const provided = args.confirmToken;
128
- if (!provided || !expected || provided !== expected) {
129
- const newToken = Math.random().toString(36).slice(2, 10);
130
- this.confirmTokens.set(tokenKey, newToken);
131
- const previewLines = messages.slice(0, Math.min(messages.length, 10));
132
- const preview = [
133
- `Matched ${logs.length} log(s). Showing ${previewLines.length} line(s) preview.`,
134
- ...previewLines,
135
- '',
136
- `Output is large (~${Math.round(textPayload.length / 3)} tokens). To fetch full content, call again with confirmToken: "${newToken}"`,
137
- 'Tip: refine with search/type/since/limit or prefer grouped format.'
138
- ].join('\n');
139
- return createSuccessResponse(preview);
140
- }
141
- else {
142
- // One-time token — consume and proceed
143
- this.confirmTokens.delete(tokenKey);
144
- }
121
+ const previewLines = [
122
+ `Matched ${logs.length} log(s). Showing ${Math.min(messages.length, 10)} line(s) preview.`,
123
+ ...messages.slice(0, Math.min(messages.length, 10)),
124
+ ];
125
+ const preview = makeConfirmPreview(() => textPayload, {
126
+ counts: { totalLength: textPayload.length, shownLength: previewLines.join('\n').length, totalMatched: logs.length, shownCount: Math.min(messages.length, 10), truncated: true },
127
+ previewLines,
128
+ extraTips: ['Tip: refine with search/type/since/limit or prefer grouped format.'],
129
+ });
130
+ return createSuccessResponse(preview.lines.join('\n'));
145
131
  }
146
132
  return createSuccessResponse([header, ...messages]);
147
133
  }
@@ -172,25 +158,17 @@ export class GetConsoleLogsTool extends BrowserToolBase {
172
158
  }
173
159
  // Guard large grouped outputs
174
160
  const textPayload = lines.join('\n');
175
- const tokenKey = `grouped:${sinceArg}:${args.type || 'all'}:${args.search || ''}:${limit}:${groups.size}`;
176
161
  if (textPayload.length >= PREVIEW_THRESHOLD) {
177
- const expected = this.confirmTokens.get(tokenKey);
178
- const provided = args.confirmToken;
179
- if (!provided || !expected || provided !== expected) {
180
- const newToken = Math.random().toString(36).slice(2, 10);
181
- this.confirmTokens.set(tokenKey, newToken);
182
- const previewBody = [
183
- `Matched ${groups.size} group(s). Showing ${limitedGroups.length}.`,
184
- ...lines.slice(0, Math.min(lines.length, 12)),
185
- '',
186
- `Output is large (~${Math.round(textPayload.length / 3)} tokens). To fetch full content, call again with confirmToken: "${newToken}"`,
187
- 'Tip: refine with search/type/since/limit.'
188
- ].join('\n');
189
- return createSuccessResponse(previewBody);
190
- }
191
- else {
192
- this.confirmTokens.delete(tokenKey);
193
- }
162
+ const previewLines = [
163
+ `Matched ${groups.size} group(s). Showing ${limitedGroups.length}.`,
164
+ ...lines.slice(0, Math.min(lines.length, 12)),
165
+ ];
166
+ const preview = makeConfirmPreview(() => textPayload, {
167
+ counts: { totalLength: textPayload.length, shownLength: previewLines.join('\n').length, totalMatched: groups.size, shownCount: limitedGroups.length, truncated: true },
168
+ previewLines,
169
+ extraTips: ['Tip: refine with search/type/since/limit.'],
170
+ });
171
+ return createSuccessResponse(preview.lines.join('\n'));
194
172
  }
195
173
  return createSuccessResponse(lines);
196
174
  }