mcp-web-inspector 0.5.0 → 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
@@ -1031,8 +1031,6 @@ Upload a file to an input[type='file'] element on the page
1031
1031
 
1032
1032
  ⚠️ Token cost: ~1,500 tokens to read. Structural tools: <100 tokens.
1033
1033
 
1034
- Admin control (optional): set env MCP_SCREENSHOT_GUARD=strict to block execution (prevents misuse by default). Unset to allow visuals for human review.
1035
-
1036
1034
  Screenshots saved to ./.mcp-web-inspector/screenshots. Example: { name: "login-page", fullPage: true } or { name: "submit-btn", selector: "testid:submit" }
1037
1035
 
1038
1036
  - Parameters:
@@ -12,14 +12,14 @@ 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
  */
@@ -28,7 +28,7 @@ export declare abstract class BrowserToolBase implements ToolHandler {
28
28
  * Sanitize verbose Playwright selector engine messages by removing stack traces and
29
29
  * keeping only the essential syntax error information.
30
30
  */
31
- private sanitizeSelectorEngineMessage;
31
+ protected sanitizeSelectorEngineMessage(msg: string): string;
32
32
  /**
33
33
  * Ensures a page is available and returns it
34
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,14 +32,33 @@ 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 many selector contexts: [ ] :
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
  }
45
64
  /**
@@ -274,12 +293,19 @@ export class BrowserToolBase {
274
293
  * @returns Formatted string or empty if only one element
275
294
  */
276
295
  formatElementSelectionInfo(selector, elementIndex, totalCount, preferredVisible = true) {
296
+ const usesNth = selector.includes('>> nth=');
277
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
+ }
278
303
  return '';
279
304
  }
280
305
  const duplicateWarning = this.getDuplicateTestIdWarning(selector, totalCount).trimEnd();
281
306
  const nthHint = this.buildNthSelectorHint(selector, totalCount).trimEnd();
282
- 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');
283
309
  const baseMessage = preferredVisible
284
310
  ? `⚠ Found ${totalCount} elements matching "${selector}", using element ${elementIndex + 1} (first visible)`
285
311
  : `⚠ Found ${totalCount} elements matching "${selector}", using element ${elementIndex + 1}`;
@@ -302,10 +328,14 @@ export class BrowserToolBase {
302
328
  selector.startsWith('data-cy:') ||
303
329
  selector.match(/^\[data-(testid|test|cy)=/);
304
330
  if (isTestIdSelector) {
305
- 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`);
306
334
  }
307
335
  // Suggest testid for non-testid selectors
308
- 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`);
309
339
  }
310
340
  /**
311
341
  * Provide a hint for using >> nth= when multiple elements match a selector
@@ -321,6 +351,11 @@ export class BrowserToolBase {
321
351
  const firstExample = `${trimmed} >> nth=0`;
322
352
  const lastIndex = Math.max(totalCount - 1, 1);
323
353
  const lastExample = `${trimmed} >> nth=${lastIndex}`;
324
- 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.`);
325
360
  }
326
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
  });
@@ -1,6 +1,6 @@
1
1
  import { BrowserToolBase } from '../base.js';
2
2
  import { createSuccessResponse } from '../../common/types.js';
3
- import { makeConfirmPreview } from '../../common/confirmHelpers.js';
3
+ import { makeConfirmPreview } from '../../common/confirm_output.js';
4
4
  /**
5
5
  * Tool for retrieving and filtering console logs from the browser
6
6
  */
@@ -122,7 +122,7 @@ export class GetConsoleLogsTool extends BrowserToolBase {
122
122
  `Matched ${logs.length} log(s). Showing ${Math.min(messages.length, 10)} line(s) preview.`,
123
123
  ...messages.slice(0, Math.min(messages.length, 10)),
124
124
  ];
125
- const preview = makeConfirmPreview(textPayload, {
125
+ const preview = makeConfirmPreview(() => textPayload, {
126
126
  counts: { totalLength: textPayload.length, shownLength: previewLines.join('\n').length, totalMatched: logs.length, shownCount: Math.min(messages.length, 10), truncated: true },
127
127
  previewLines,
128
128
  extraTips: ['Tip: refine with search/type/since/limit or prefer grouped format.'],
@@ -163,7 +163,7 @@ export class GetConsoleLogsTool extends BrowserToolBase {
163
163
  `Matched ${groups.size} group(s). Showing ${limitedGroups.length}.`,
164
164
  ...lines.slice(0, Math.min(lines.length, 12)),
165
165
  ];
166
- const preview = makeConfirmPreview(textPayload, {
166
+ const preview = makeConfirmPreview(() => textPayload, {
167
167
  counts: { totalLength: textPayload.length, shownLength: previewLines.join('\n').length, totalMatched: groups.size, shownCount: limitedGroups.length, truncated: true },
168
168
  previewLines,
169
169
  extraTips: ['Tip: refine with search/type/since/limit.'],
@@ -52,47 +52,65 @@ describe('ScreenshotTool', () => {
52
52
  afterEach(() => {
53
53
  jest.restoreAllMocks();
54
54
  });
55
- test('should take a full page screenshot', async () => {
55
+ test('should take a full page screenshot (via confirm_output)', async () => {
56
56
  const args = {
57
57
  name: 'test-screenshot',
58
58
  fullPage: true
59
59
  };
60
- // Return a buffer for the screenshot
60
+ // First call returns preview with confirm token; no screenshot yet
61
+ const previewResult = await screenshotTool.execute(args, mockContext);
62
+ expect(previewResult.isError).toBe(false);
63
+ const text = previewResult.content.map(c => c.text).join('\n');
64
+ expect(text).toMatch(/confirm_output\(\{ token: \"[\w\d]+\" \}\)/);
65
+ // Extract token and confirm
66
+ const tokenMatch = text.match(/confirm_output\(\{ token: \"([\w\d]+)\" \}\)/);
67
+ expect(tokenMatch).not.toBeNull();
68
+ const token = tokenMatch[1];
69
+ const { ConfirmOutputTool } = await import('../../../common/confirm_output.js');
70
+ const confirmTool = new ConfirmOutputTool({});
61
71
  const screenshotBuffer = Buffer.from('mock-screenshot');
62
72
  mockScreenshot.mockImplementationOnce(() => Promise.resolve(screenshotBuffer));
63
- const result = await screenshotTool.execute(args, mockContext);
64
- // Check if screenshot was called with correct options
65
- expect(mockScreenshot).toHaveBeenCalledWith(expect.objectContaining({
66
- fullPage: true,
67
- type: 'png'
68
- }));
69
- // Check that the result contains success message
70
- expect(result.isError).toBe(false);
71
- expect(result.content[0].text).toContain('Screenshot saved to');
73
+ const finalResult = await confirmTool.execute({ token }, mockContext);
74
+ expect(finalResult.isError).toBe(false);
75
+ expect(finalResult.content[0].text).toContain('Screenshot saved to');
76
+ expect(mockScreenshot).toHaveBeenCalledWith(expect.objectContaining({ fullPage: true, type: 'png' }));
72
77
  });
73
- test('should handle element screenshot', async () => {
78
+ test('should handle element screenshot (via confirm_output)', async () => {
74
79
  const args = {
75
80
  name: 'test-element-screenshot',
76
81
  selector: '#test-element'
77
82
  };
78
- // Return a buffer for the screenshot
83
+ const previewResult = await screenshotTool.execute(args, mockContext);
84
+ expect(previewResult.isError).toBe(false);
85
+ const text = previewResult.content.map(c => c.text).join('\n');
86
+ const tokenMatch = text.match(/confirm_output\(\{ token: \"([\w\d]+)\" \}\)/);
87
+ expect(tokenMatch).not.toBeNull();
88
+ const token = tokenMatch[1];
89
+ const { ConfirmOutputTool } = await import('../../../common/confirm_output.js');
90
+ const confirmTool = new ConfirmOutputTool({});
79
91
  const screenshotBuffer = Buffer.from('mock-element-screenshot');
80
92
  mockLocatorScreenshot.mockImplementationOnce(() => Promise.resolve(screenshotBuffer));
81
- const result = await screenshotTool.execute(args, mockContext);
82
- // Check that the result contains success message
83
- expect(result.isError).toBe(false);
84
- expect(result.content[0].text).toContain('Screenshot saved to');
93
+ const finalResult = await confirmTool.execute({ token }, mockContext);
94
+ expect(finalResult.isError).toBe(false);
95
+ expect(finalResult.content[0].text).toContain('Screenshot saved to');
85
96
  });
86
- test('should handle screenshot errors', async () => {
97
+ test('should handle screenshot errors (on confirm)', async () => {
87
98
  const args = {
88
99
  name: 'test-screenshot'
89
100
  };
90
- // Mock a screenshot error
101
+ const previewResult = await screenshotTool.execute(args, mockContext);
102
+ expect(previewResult.isError).toBe(false);
103
+ const text = previewResult.content.map(c => c.text).join('\n');
104
+ const tokenMatch = text.match(/confirm_output\(\{ token: \"([\w\d]+)\" \}\)/);
105
+ expect(tokenMatch).not.toBeNull();
106
+ const token = tokenMatch[1];
107
+ const { ConfirmOutputTool } = await import('../../../common/confirm_output.js');
108
+ const confirmTool = new ConfirmOutputTool({});
109
+ // Mock a screenshot error on confirmation
91
110
  mockScreenshot.mockImplementationOnce(() => Promise.reject(new Error('Screenshot failed')));
92
- const result = await screenshotTool.execute(args, mockContext);
93
- expect(mockScreenshot).toHaveBeenCalled();
94
- expect(result.isError).toBe(true);
95
- expect(result.content[0].text).toContain('Operation failed');
111
+ const finalResult = await confirmTool.execute({ token }, mockContext);
112
+ expect(finalResult.isError).toBe(true);
113
+ expect(finalResult.content[0].text).toContain('Screenshot failed');
96
114
  });
97
115
  test('should handle missing page', async () => {
98
116
  const args = {
@@ -108,60 +126,60 @@ describe('ScreenshotTool', () => {
108
126
  expect(result.isError).toBe(true);
109
127
  expect(result.content[0].text).toContain('Browser page not initialized');
110
128
  });
111
- test('should store screenshots in a map', async () => {
129
+ test('should store screenshots in a map (after confirm)', async () => {
112
130
  const args = {
113
131
  name: 'test-screenshot',
114
132
  storeBase64: true
115
133
  };
116
- // Return a buffer for the screenshot
117
- const screenshotBuffer = Buffer.from('mock-screenshot');
118
- mockScreenshot.mockImplementationOnce(() => Promise.resolve(screenshotBuffer));
119
- await screenshotTool.execute(args, mockContext);
134
+ const preview = await screenshotTool.execute(args, mockContext);
135
+ const token = (preview.content.map(c => c.text).join('\n').match(/confirm_output\(\{ token: \"([\w\d]+)\" \}\)/) || [])[1];
136
+ const { ConfirmOutputTool } = await import('../../../common/confirm_output.js');
137
+ const confirmTool = new ConfirmOutputTool({});
138
+ mockScreenshot.mockImplementationOnce(() => Promise.resolve(Buffer.from('mock-screenshot')));
139
+ await confirmTool.execute({ token }, mockContext);
120
140
  // Check that the screenshot was stored in the map
121
141
  const screenshots = screenshotTool.getScreenshots();
122
142
  expect(screenshots.has('test-screenshot')).toBe(true);
123
143
  });
124
- test('should take a screenshot with specific browser type', async () => {
144
+ test('should take a screenshot with specific browser type (via confirm_output)', async () => {
125
145
  const args = {
126
146
  name: 'browser-type-test',
127
147
  browserType: 'firefox'
128
148
  };
129
- // Execute with browser type
130
- const result = await screenshotTool.execute(args, mockContext);
149
+ const preview = await screenshotTool.execute(args, mockContext);
150
+ const text = preview.content.map(c => c.text).join('\n');
151
+ const token = (text.match(/confirm_output\(\{ token: \"([\w\d]+)\" \}\)/) || [])[1];
152
+ const { ConfirmOutputTool } = await import('../../../common/confirm_output.js');
153
+ const confirmTool = new ConfirmOutputTool({});
154
+ mockScreenshot.mockImplementationOnce(() => Promise.resolve(Buffer.from('mock-screenshot')));
155
+ const result = await confirmTool.execute({ token }, mockContext);
131
156
  expect(mockScreenshot).toHaveBeenCalled();
132
157
  expect(result.isError).toBe(false);
133
158
  expect(result.content[0].text).toContain('Screenshot saved to');
134
159
  });
135
- test('should include actionable guidance for full-page screenshots', async () => {
160
+ test('preview should suggest alternative tools', async () => {
136
161
  const args = {
137
162
  name: 'test-screenshot',
138
163
  fullPage: true
139
164
  };
140
- const screenshotBuffer = Buffer.from('mock-screenshot');
141
- mockScreenshot.mockImplementationOnce(() => Promise.resolve(screenshotBuffer));
142
165
  const result = await screenshotTool.execute(args, mockContext);
143
166
  expect(result.isError).toBe(false);
144
- // Concatenate all content items to check the full response
145
167
  const fullResponseText = result.content.map(c => c.text).join('\n');
146
- // Verify guidance is included (updated to match new cross-tool advertising)
147
- expect(fullResponseText).toContain('💡 To debug layout issues');
168
+ expect(fullResponseText).toContain('Screenshot requested');
148
169
  expect(fullResponseText).toContain('inspect_dom');
149
- expect(fullResponseText).toContain('get_test_ids');
170
+ expect(fullResponseText).toContain('compare_element_alignment');
171
+ expect(fullResponseText).toContain('get_computed_styles');
150
172
  expect(fullResponseText).toContain('inspect_ancestors');
151
173
  });
152
- test('should NOT include guidance for element-specific screenshots', async () => {
174
+ test('preview should also suggest alternatives for element screenshots', async () => {
153
175
  const args = {
154
176
  name: 'test-element-screenshot',
155
177
  selector: '#test-element'
156
178
  };
157
- const screenshotBuffer = Buffer.from('mock-element-screenshot');
158
- mockLocatorScreenshot.mockImplementationOnce(() => Promise.resolve(screenshotBuffer));
159
179
  const result = await screenshotTool.execute(args, mockContext);
160
180
  expect(result.isError).toBe(false);
161
- // Concatenate all content items to check the full response
162
181
  const fullResponseText = result.content.map(c => c.text).join('\n');
163
- // Verify guidance is NOT included for element screenshots
164
- expect(fullResponseText).not.toContain('💡 Next steps to analyze this page:');
165
- expect(fullResponseText).not.toContain('inspect_dom');
182
+ expect(fullResponseText).toContain('Screenshot requested');
183
+ expect(fullResponseText).toContain('inspect_dom');
166
184
  });
167
185
  });
@@ -1,6 +1,6 @@
1
1
  import { BrowserToolBase } from '../base.js';
2
2
  import { createSuccessResponse, createErrorResponse, } from '../../common/types.js';
3
- import { makeConfirmPreview } from '../../common/confirmHelpers.js';
3
+ import { makeConfirmPreview } from '../../common/confirm_output.js';
4
4
  /**
5
5
  * Tool for getting HTML from the page
6
6
  */
@@ -105,7 +105,7 @@ export class GetHtmlTool extends BrowserToolBase {
105
105
  // Generate key for this HTML request
106
106
  // Check if HTML is too large => return preview + token for confirm_output
107
107
  if (originalLength >= PREVIEW_THRESHOLD) {
108
- const preview = makeConfirmPreview(processedHtml, {
108
+ const preview = makeConfirmPreview(() => processedHtml, {
109
109
  counts: { totalLength: originalLength, shownLength: Math.min(500, originalLength), truncated: true },
110
110
  previewLines: [
111
111
  'Preview (first 500 chars):',
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import { BrowserToolBase } from '../base.js';
4
4
  import { createSuccessResponse } from '../../common/types.js';
5
+ import { makeConfirmPreview } from '../../common/confirm_output.js';
5
6
  /**
6
7
  * Tool for taking screenshots of pages or elements
7
8
  */
@@ -30,8 +31,6 @@ export class ScreenshotTool extends BrowserToolBase {
30
31
  '',
31
32
  '⚠️ Token cost: ~1,500 tokens to read. Structural tools: <100 tokens.',
32
33
  '',
33
- 'Admin control (optional): set env MCP_SCREENSHOT_GUARD=strict to block execution (prevents misuse by default). Unset to allow visuals for human review.',
34
- '',
35
34
  `Screenshots saved to ${screenshotsDir}. Example: { name: "login-page", fullPage: true } or { name: "submit-btn", selector: "testid:submit" }`
36
35
  ].join('\n');
37
36
  return {
@@ -63,81 +62,64 @@ export class ScreenshotTool extends BrowserToolBase {
63
62
  }
64
63
  async execute(args, context) {
65
64
  return this.safeExecute(context, async (page) => {
66
- // Optional guardrail to reduce unreasonable calls by LLMs.
67
- // If MCP_SCREENSHOT_GUARD is set to 'strict', block execution.
68
- const guard = (process.env.MCP_SCREENSHOT_GUARD || '').toLowerCase();
69
- const strictGuard = guard === '1' || guard === 'true' || guard === 'strict';
70
- if (strictGuard) {
71
- const lines = [];
72
- lines.push('🚫 Screenshot blocked by admin guard (MCP_SCREENSHOT_GUARD=strict).');
73
- lines.push('');
74
- lines.push('Use structural tools for programmatic debugging:');
75
- lines.push(' - inspect_dom() hierarchy, positions, visibility');
76
- lines.push(' - compare_element_alignment() alignment and pixel diffs');
77
- lines.push(' - get_computed_styles() → CSS values');
78
- lines.push(' - inspect_ancestors() → parent constraints and overflow');
79
- lines.push('');
80
- lines.push('If a human needs to review visuals, ask an admin to unset MCP_SCREENSHOT_GUARD.');
81
- return createSuccessResponse(lines.join('\n'));
82
- }
83
- const screenshotOptions = {
84
- type: args.type || "png",
85
- fullPage: !!args.fullPage
86
- };
87
- if (args.selector) {
88
- // Normalize selector (support testid: shorthand)
89
- const selector = this.normalizeSelector(args.selector);
90
- const element = await page.$(selector);
91
- if (!element) {
92
- return {
93
- content: [{
94
- type: "text",
95
- text: `Element not found: ${selector}`,
96
- }],
97
- isError: true
98
- };
65
+ // Defer the screenshot capture until confirmation via confirm_output
66
+ const thunk = async () => {
67
+ const screenshotOptions = {
68
+ type: args.type || "png",
69
+ fullPage: !!args.fullPage
70
+ };
71
+ if (args.selector) {
72
+ const selector = this.normalizeSelector(args.selector);
73
+ const element = await page.$(selector);
74
+ if (!element) {
75
+ throw new Error(`Element not found: ${selector}`);
76
+ }
77
+ screenshotOptions.element = element;
99
78
  }
100
- screenshotOptions.element = element;
101
- }
102
- const { getScreenshotsDir } = await import('../../../toolHandler.js');
103
- // Generate output path
104
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
105
- const filename = `${args.name || 'screenshot'}-${timestamp}.png`;
106
- // Use screenshots directory from config, fall back to downloadsDir arg, then default Downloads
107
- const downloadsDir = args.downloadsDir || getScreenshotsDir();
108
- if (!fs.existsSync(downloadsDir)) {
109
- fs.mkdirSync(downloadsDir, { recursive: true });
110
- }
111
- const outputPath = path.join(downloadsDir, filename);
112
- screenshotOptions.path = outputPath;
113
- const screenshot = await page.screenshot(screenshotOptions);
114
- const base64Screenshot = screenshot.toString('base64');
115
- const messages = [`✓ Screenshot saved to: ${path.relative(process.cwd(), outputPath)}`];
116
- // Handle base64 storage
117
- if (args.storeBase64 !== false) {
118
- this.screenshots.set(args.name || 'screenshot', base64Screenshot);
119
- context.server.notification({
120
- method: "notifications/resources/list_changed",
121
- });
122
- messages.push(`Screenshot also stored in memory with name: '${args.name || 'screenshot'}'`);
123
- }
124
- // Add token cost warning
125
- messages.push('');
126
- messages.push('📸 Open the file in your IDE to view the screenshot');
127
- messages.push('⚠️ Reading the image file consumes ~1,500 tokens — use structural tools for layout debugging');
128
- // Add actionable guidance based on screenshot context
129
- messages.push('');
130
- messages.push('💡 To debug layout issues without reading the screenshot:');
131
- if (args.selector) {
132
- messages.push(` inspect_ancestors({ selector: "${args.selector}" })`);
133
- messages.push(' → See parent constraints (width, margins, overflow, borders)');
134
- }
135
- else {
136
- messages.push(' 1) Find the element: inspect_dom({}) or get_test_ids()');
137
- messages.push(' 2) Check parent constraints: inspect_ancestors({ selector: "..." })');
138
- messages.push(' 3) Compare alignment: compare_element_alignment({ selector1: "...", selector2: "..." })');
139
- }
140
- return createSuccessResponse(messages);
79
+ const { getScreenshotsDir } = await import('../../../toolHandler.js');
80
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
81
+ const filename = `${args.name || 'screenshot'}-${timestamp}.png`;
82
+ const downloadsDir = args.downloadsDir || getScreenshotsDir();
83
+ if (!fs.existsSync(downloadsDir)) {
84
+ fs.mkdirSync(downloadsDir, { recursive: true });
85
+ }
86
+ const outputPath = path.join(downloadsDir, filename);
87
+ screenshotOptions.path = outputPath;
88
+ const screenshot = await page.screenshot(screenshotOptions);
89
+ const base64Screenshot = screenshot.toString('base64');
90
+ const messages = [`✓ Screenshot saved to: ${path.relative(process.cwd(), outputPath)}`];
91
+ if (args.storeBase64 !== false) {
92
+ this.screenshots.set(args.name || 'screenshot', base64Screenshot);
93
+ context.server.notification({ method: "notifications/resources/list_changed" });
94
+ messages.push(`Screenshot also stored in memory with name: '${args.name || 'screenshot'}'`);
95
+ }
96
+ messages.push('');
97
+ messages.push('📸 Open the file in your IDE to view the screenshot');
98
+ messages.push('⚠️ Reading the image file consumes ~1,500 tokens — use structural tools for layout debugging');
99
+ messages.push('');
100
+ messages.push('💡 To debug layout issues without reading the screenshot:');
101
+ if (args.selector) {
102
+ messages.push(` inspect_ancestors({ selector: "${args.selector}" })`);
103
+ messages.push(' → See parent constraints (width, margins, overflow, borders)');
104
+ }
105
+ else {
106
+ messages.push(' 1) Find the element: inspect_dom({}) or get_test_ids()');
107
+ messages.push(' 2) Check parent constraints: inspect_ancestors({ selector: "..." })');
108
+ messages.push(' 3) Compare alignment: compare_element_alignment({ selector1: "...", selector2: "..." })');
109
+ }
110
+ return messages.join('\n');
111
+ };
112
+ // Return a minimal preview that suggests better alternatives
113
+ const preview = makeConfirmPreview(thunk, {
114
+ previewLines: [
115
+ 'Screenshot requested. For debugging, prefer:',
116
+ ' • inspect_dom() - structure, positions, visibility',
117
+ ' compare_element_alignment() - alignment with pixel diffs',
118
+ ' • get_computed_styles() - CSS values',
119
+ ' • inspect_ancestors() - constraints and overflow',
120
+ ],
121
+ });
122
+ return createSuccessResponse(preview.lines.join('\n'));
141
123
  });
142
124
  }
143
125
  /**
@@ -1,6 +1,6 @@
1
1
  import { BrowserToolBase } from '../base.js';
2
2
  import { createSuccessResponse, createErrorResponse, } from '../../common/types.js';
3
- import { makeConfirmPreview } from '../../common/confirmHelpers.js';
3
+ import { makeConfirmPreview } from '../../common/confirm_output.js';
4
4
  /**
5
5
  * Tool for executing JavaScript in the browser
6
6
  */
@@ -274,7 +274,7 @@ export class EvaluateTool extends BrowserToolBase {
274
274
  if (totalLength >= PREVIEW_THRESHOLD) {
275
275
  const previewLen = Math.min(500, totalLength);
276
276
  const preview = resultStr.slice(0, previewLen);
277
- const previewBlock = makeConfirmPreview(resultStr, {
277
+ const previewBlock = makeConfirmPreview(() => resultStr, {
278
278
  headerLine: '✓ JavaScript execution result (preview):',
279
279
  counts: { totalLength, shownLength: previewLen, truncated: true },
280
280
  previewLines: [
@@ -9,7 +9,7 @@ export class CompareElementAlignmentTool extends BrowserToolBase {
9
9
  name: "compare_element_alignment",
10
10
  description: "COMPARE TWO ELEMENTS: Get comprehensive alignment and dimension comparison in one call. Shows edge alignment (top, left, right, bottom), center alignment (horizontal, vertical), and dimensions (width, height). Perfect for debugging 'are these headers aligned?' or 'do these panels match?'. Returns all alignment info with ✓/✗ symbols and pixel differences. For parent-child centering, use inspect_dom() instead (automatically shows if children are centered in parent). More efficient than evaluate() with manual getBoundingClientRect() calculations.",
11
11
  outputs: [
12
- "Optional warnings when a selector matched multiple elements (using first).",
12
+ "Optional warnings when a selector matched multiple elements (uses first visible; suggests adding unique data-testid).",
13
13
  "Header: Alignment: <elem1> vs <elem2>",
14
14
  "Two lines with each element's position and size: @ (x,y) w×h px",
15
15
  "Edges block: Top/Left/Right/Bottom with ✓/✗ and diffs",
@@ -52,10 +52,10 @@ export class CompareElementAlignmentTool extends BrowserToolBase {
52
52
  const selector1 = this.normalizeSelector(args.selector1);
53
53
  const selector2 = this.normalizeSelector(args.selector2);
54
54
  try {
55
- // Get locators for both elements
55
+ // Build locators for both elements
56
56
  const locator1 = page.locator(selector1);
57
57
  const locator2 = page.locator(selector2);
58
- // Check if both elements exist
58
+ // Check existence first to preserve legacy error messages expected by tests
59
59
  const count1 = await locator1.count();
60
60
  const count2 = await locator2.count();
61
61
  if (count1 === 0) {
@@ -64,19 +64,43 @@ export class CompareElementAlignmentTool extends BrowserToolBase {
64
64
  if (count2 === 0) {
65
65
  return createErrorResponse(`Second element not found: ${args.selector2}`);
66
66
  }
67
- // Handle multiple matches by using first() - show warning
68
- const targetLocator1 = count1 > 1 ? locator1.first() : locator1;
69
- const targetLocator2 = count2 > 1 ? locator2.first() : locator2;
70
- let warnings = '';
67
+ // Prefer first visible element and produce clear selection info
68
+ let selectionWarning1 = '';
69
+ let selectionWarning2 = '';
70
+ let targetLocator1 = locator1.first();
71
+ let targetLocator2 = locator2.first();
72
+ try {
73
+ const sel1 = await this.selectPreferredLocator(locator1, {
74
+ originalSelector: args.selector1,
75
+ });
76
+ selectionWarning1 = this.formatElementSelectionInfo(args.selector1, sel1.elementIndex, sel1.totalCount);
77
+ targetLocator1 = sel1.element;
78
+ }
79
+ catch {
80
+ // Fallback to first() when visibility checks are unavailable (tests/mocks)
81
+ selectionWarning1 = this.formatElementSelectionInfo(args.selector1, 0, count1);
82
+ }
83
+ try {
84
+ const sel2 = await this.selectPreferredLocator(locator2, {
85
+ originalSelector: args.selector2,
86
+ });
87
+ selectionWarning2 = this.formatElementSelectionInfo(args.selector2, sel2.elementIndex, sel2.totalCount);
88
+ targetLocator2 = sel2.element;
89
+ }
90
+ catch {
91
+ selectionWarning2 = this.formatElementSelectionInfo(args.selector2, 0, count2);
92
+ }
93
+ // Maintain legacy warning format for multiple matches, then append richer hints
94
+ let legacyWarnings = '';
71
95
  if (count1 > 1) {
72
- warnings += `⚠ Warning: First selector matched ${count1} elements, using first\n`;
96
+ legacyWarnings += `⚠ Warning: First selector matched ${count1} elements, using first\n`;
73
97
  }
74
98
  if (count2 > 1) {
75
- warnings += `⚠ Warning: Second selector matched ${count2} elements, using first\n`;
76
- }
77
- if (warnings) {
78
- warnings += '\n';
99
+ legacyWarnings += `⚠ Warning: Second selector matched ${count2} elements, using first\n`;
79
100
  }
101
+ const warnings = [legacyWarnings.trimEnd(), selectionWarning1, selectionWarning2]
102
+ .filter(Boolean)
103
+ .join('\n');
80
104
  // Get bounding boxes
81
105
  const box1 = await targetLocator1.boundingBox();
82
106
  const box2 = await targetLocator2.boundingBox();
@@ -182,7 +206,7 @@ export class CompareElementAlignmentTool extends BrowserToolBase {
182
206
  };
183
207
  // Build output
184
208
  const lines = [
185
- warnings,
209
+ warnings ? `${warnings}\n` : '',
186
210
  `Alignment: ${descriptor1} vs ${descriptor2}`,
187
211
  ` ${name1}: @ (${left1},${top1}) ${width1}×${height1}px`,
188
212
  ` ${name2}: @ (${left2},${top2}) ${width2}×${height2}px`,
@@ -104,11 +104,8 @@ RELATED TOOLS: For comparing TWO elements' alignment (not parent-child), use com
104
104
  const maxDepth = args.maxDepth ?? 5;
105
105
  try {
106
106
  // Use consistent element selection (Playwright's visibility detection)
107
+ // Delegate count + selector error handling to selectPreferredLocator()
107
108
  const locator = page.locator(selector);
108
- const count = await locator.count();
109
- if (count === 0) {
110
- return createErrorResponse(`Element not found: ${args.selector || 'body'}`);
111
- }
112
109
  const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
113
110
  originalSelector: args.selector || 'body',
114
111
  });
@@ -716,7 +713,14 @@ RELATED TOOLS: For comparing TWO elements' alignment (not parent-child), use com
716
713
  return createSuccessResponse(lines.join('\n'));
717
714
  }
718
715
  catch (error) {
719
- return createErrorResponse(`Failed to inspect DOM: ${error.message}`);
716
+ const msg = error.message || '';
717
+ // Map common not-found message to a user-friendly line
718
+ if (msg === 'No elements found') {
719
+ return createErrorResponse(`Element not found: ${args.selector || 'body'}`);
720
+ }
721
+ // Sanitize verbose selector engine traces and return concise message
722
+ const concise = this.sanitizeSelectorEngineMessage(msg) || msg;
723
+ return createErrorResponse(`Failed to inspect DOM: ${concise}`);
720
724
  }
721
725
  });
722
726
  }
@@ -243,7 +243,9 @@ export class QuerySelectorTool extends BrowserToolBase {
243
243
  return createSuccessResponse(lines.join('\n'));
244
244
  }
245
245
  catch (error) {
246
- return createErrorResponse(`Failed to query selector: ${error.message}`);
246
+ const msg = error.message || '';
247
+ const concise = this.sanitizeSelectorEngineMessage(msg) || msg;
248
+ return createErrorResponse(`Failed to query selector: ${concise}`);
247
249
  }
248
250
  });
249
251
  }
@@ -1,7 +1,33 @@
1
1
  import type { ToolHandler, ToolContext, ToolResponse, ToolMetadata, SessionConfig } from './types.js';
2
+ type PayloadThunk = () => Promise<string> | string;
3
+ export declare function registerPayloadThunk(thunk: PayloadThunk, ttlMs?: number): string;
4
+ export declare function consumeThunk(token: string): {
5
+ ok: true;
6
+ thunk: PayloadThunk;
7
+ } | {
8
+ ok: false;
9
+ error: string;
10
+ };
11
+ export interface PreviewCounts {
12
+ totalLength?: number;
13
+ shownLength?: number;
14
+ totalMatched?: number;
15
+ shownCount?: number;
16
+ truncated?: boolean;
17
+ }
18
+ export declare function makeConfirmPreview(payloadOrThunk: string | PayloadThunk, options: {
19
+ headerLine?: string;
20
+ previewLines: string[];
21
+ counts?: PreviewCounts;
22
+ extraTips?: string[];
23
+ }): {
24
+ token: string;
25
+ lines: string[];
26
+ };
2
27
  export declare class ConfirmOutputTool implements ToolHandler {
3
28
  private server;
4
29
  constructor(server: any);
5
30
  static getMetadata(sessionConfig?: SessionConfig): ToolMetadata;
6
31
  execute(args: any, context: ToolContext): Promise<ToolResponse>;
7
32
  }
33
+ export {};
@@ -1,5 +1,73 @@
1
1
  import { createSuccessResponse, createErrorResponse } from './types.js';
2
- import { consumePayload } from './confirmStore.js';
2
+ const STORE = new Map();
3
+ function now() {
4
+ return Date.now();
5
+ }
6
+ function purgeExpired() {
7
+ const t = now();
8
+ for (const [k, v] of STORE.entries()) {
9
+ if (v.expiresAt <= t)
10
+ STORE.delete(k);
11
+ }
12
+ }
13
+ function genToken(len = 12) {
14
+ return Math.random().toString(36).slice(2, 2 + len);
15
+ }
16
+ export function registerPayloadThunk(thunk, ttlMs = 120000) {
17
+ purgeExpired();
18
+ const token = genToken();
19
+ const expiresAt = now() + Math.max(1000, ttlMs);
20
+ STORE.set(token, { thunk, expiresAt });
21
+ return token;
22
+ }
23
+ export function consumeThunk(token) {
24
+ purgeExpired();
25
+ const entry = STORE.get(token);
26
+ if (!entry)
27
+ return { ok: false, error: 'Invalid or expired token' };
28
+ STORE.delete(token);
29
+ if (entry.expiresAt <= now())
30
+ return { ok: false, error: 'Invalid or expired token' };
31
+ return { ok: true, thunk: entry.thunk };
32
+ }
33
+ export function makeConfirmPreview(payloadOrThunk, options) {
34
+ const thunk = typeof payloadOrThunk === 'function' ? payloadOrThunk : () => payloadOrThunk;
35
+ const token = registerPayloadThunk(thunk);
36
+ const lines = [];
37
+ if (options.headerLine) {
38
+ lines.push(options.headerLine);
39
+ }
40
+ const parts = [];
41
+ const c = options.counts || {};
42
+ if (typeof c.totalLength === 'number')
43
+ parts.push(`totalLength=${c.totalLength}`);
44
+ if (typeof c.shownLength === 'number')
45
+ parts.push(`shownLength=${c.shownLength}`);
46
+ if (typeof c.totalMatched === 'number')
47
+ parts.push(`totalMatched=${c.totalMatched}`);
48
+ if (typeof c.shownCount === 'number')
49
+ parts.push(`shownCount=${c.shownCount}`);
50
+ if (typeof c.truncated === 'boolean')
51
+ parts.push(`truncated=${c.truncated}`);
52
+ if (parts.length) {
53
+ lines.push(`counts: ${parts.join(', ')}`);
54
+ }
55
+ if (lines.length)
56
+ lines.push('');
57
+ // Caller provides preview content lines (e.g., label + excerpt or list)
58
+ for (const l of options.previewLines)
59
+ lines.push(l);
60
+ if (options.previewLines.length)
61
+ lines.push('');
62
+ const baseLen = typeof c.totalLength === 'number' ? c.totalLength : (typeof payloadOrThunk === 'string' ? payloadOrThunk.length : 0);
63
+ const estTokens = Math.round(baseLen / 3);
64
+ lines.push(`Output is large (~${estTokens} tokens). To fetch full content without resending parameters, call confirm_output({ token: "${token}" }).`);
65
+ if (options.extraTips && options.extraTips.length) {
66
+ for (const tip of options.extraTips)
67
+ lines.push(tip);
68
+ }
69
+ return { token, lines };
70
+ }
3
71
  export class ConfirmOutputTool {
4
72
  constructor(server) {
5
73
  this.server = server;
@@ -30,11 +98,17 @@ export class ConfirmOutputTool {
30
98
  const token = typeof args.token === 'string' ? args.token : '';
31
99
  if (!token)
32
100
  return createErrorResponse('Token is required');
33
- const res = consumePayload(token);
101
+ const res = consumeThunk(token);
34
102
  if (!res.ok) {
35
103
  const err = res.error;
36
104
  return createErrorResponse(err);
37
105
  }
38
- return createSuccessResponse(res.payload);
106
+ try {
107
+ const out = await res.thunk();
108
+ return createSuccessResponse(out);
109
+ }
110
+ catch (e) {
111
+ return createErrorResponse(e.message || 'Failed to produce output');
112
+ }
39
113
  }
40
114
  }
@@ -1,9 +1,34 @@
1
1
  // Helper functions for creating responses
2
+ // Global error message sanitizer: removes stack traces and noisy engine frames
3
+ function sanitizeErrorMessage(message) {
4
+ if (!message)
5
+ return '';
6
+ // If message already contains helpful guidance (e.g., Tips), avoid truncation.
7
+ const hasGuidance = /\bTips?:\b|💡/.test(message);
8
+ // Trim to concise selector phrase only when there is no guidance to preserve.
9
+ if (!hasGuidance) {
10
+ const cutoffPhrases = [
11
+ 'is not a valid selector.',
12
+ 'is not a valid selector',
13
+ ];
14
+ for (const phrase of cutoffPhrases) {
15
+ const idx = message.indexOf(phrase);
16
+ if (idx !== -1) {
17
+ return message.slice(0, idx + phrase.length).trim();
18
+ }
19
+ }
20
+ }
21
+ // Remove typical stack lines (e.g., " at query (...)" or " at ... (<anonymous>:x:y)")
22
+ const lines = message.split(/\r?\n/);
23
+ const filtered = lines.filter(l => !/^\s*at\b/.test(l) && !/<anonymous>:\d+:\d+/.test(l));
24
+ return filtered.join('\n').trim();
25
+ }
2
26
  export function createErrorResponse(message) {
27
+ const sanitized = sanitizeErrorMessage(message);
3
28
  return {
4
29
  content: [{
5
30
  type: "text",
6
- text: message
31
+ text: sanitized
7
32
  }],
8
33
  isError: true
9
34
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-web-inspector",
3
- "version": "0.5.0",
3
+ "version": "0.5.3",
4
4
  "description": "Web Inspector MCP: Give LLMs visual superpowers to see, debug, and test any web page.",
5
5
  "license": "MIT",
6
6
  "author": "Anton",