mcp-web-inspector 0.4.4 → 0.5.0
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 +34 -7
- package/dist/tools/browser/base.d.ts +5 -0
- package/dist/tools/browser/base.js +40 -14
- package/dist/tools/browser/console/__tests__/console.test.js +6 -8
- package/dist/tools/browser/console/get_console_logs.d.ts +9 -0
- package/dist/tools/browser/console/get_console_logs.js +108 -27
- package/dist/tools/browser/console/index.d.ts +1 -1
- package/dist/tools/browser/console/index.js +1 -1
- package/dist/tools/browser/content/__tests__/visiblePage.test.js +28 -35
- package/dist/tools/browser/content/get_html.d.ts +0 -1
- package/dist/tools/browser/content/get_html.js +22 -40
- package/dist/tools/browser/evaluation/evaluate.js +191 -16
- package/dist/tools/browser/inspection/__tests__/measureElement.test.js +23 -0
- package/dist/tools/browser/interaction/__tests__/interaction.test.js +11 -7
- package/dist/tools/browser/register.js +3 -2
- package/dist/tools/common/confirmHelpers.d.ts +16 -0
- package/dist/tools/common/confirmHelpers.js +37 -0
- package/dist/tools/common/confirmStore.d.ts +8 -0
- package/dist/tools/common/confirmStore.js +31 -0
- package/dist/tools/common/confirm_output.d.ts +7 -0
- package/dist/tools/common/confirm_output.js +40 -0
- package/dist/tools/common/registry.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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),
|
|
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.
|
|
@@ -1044,24 +1043,40 @@ Screenshots saved to ./.mcp-web-inspector/screenshots. Example: { name: "login-p
|
|
|
1044
1043
|
|
|
1045
1044
|
### Console
|
|
1046
1045
|
|
|
1046
|
+
#### `clear_console_logs`
|
|
1047
|
+
Clears captured console logs and returns the number of entries cleared.
|
|
1048
|
+
|
|
1047
1049
|
#### `get_console_logs`
|
|
1048
|
-
Retrieve console logs
|
|
1050
|
+
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.
|
|
1049
1051
|
|
|
1050
1052
|
- Parameters:
|
|
1051
1053
|
- type (string, optional): Type of logs to retrieve (all, error, warning, log, info, debug, exception)
|
|
1052
1054
|
- search (string, optional): Text to search for in logs (handles text with square brackets)
|
|
1053
|
-
- limit (number, optional): Maximum
|
|
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.)
|
|
1055
|
-
-
|
|
1055
|
+
- limit (number, optional): Maximum entries to return (groups when grouped, lines when raw). Default: 20
|
|
1056
|
+
- 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'
|
|
1057
|
+
- format (string, optional): Output format: 'grouped' (default, deduped with counts) or 'raw' (chronological, ungrouped)
|
|
1056
1058
|
|
|
1057
1059
|
### Evaluation
|
|
1058
1060
|
|
|
1059
1061
|
#### `evaluate`
|
|
1060
|
-
⚙️ CUSTOM JAVASCRIPT EXECUTION - Execute arbitrary JavaScript in the browser console and return the result
|
|
1062
|
+
⚙️ 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.
|
|
1061
1063
|
|
|
1062
1064
|
- Parameters:
|
|
1063
1065
|
- script (string, required): JavaScript code to execute
|
|
1064
1066
|
|
|
1067
|
+
- Output Format:
|
|
1068
|
+
- Header: '✓ JavaScript execution result:'
|
|
1069
|
+
- Default result: compact summary string (arrays/objects/dom nodes summarized)
|
|
1070
|
+
- Array summary: 'Array(n) [first, second, third…]' (shows first 3 items)
|
|
1071
|
+
- Object summary (large): 'Object(n keys): key1, key2, key3…' (top-level keys only)
|
|
1072
|
+
- DOM node summary: '<tag id=#id class=.a.b> @ (x,y) WxH' (rounded ints)
|
|
1073
|
+
- NodeList/HTMLCollection summary: 'NodeList(n) [<div…>, <span…>, <a…>…]'
|
|
1074
|
+
- Preview guard when result is large (≥ ~2000 chars):
|
|
1075
|
+
- 'Preview (first 500 chars):' followed by excerpt
|
|
1076
|
+
- Counts: 'totalLength: N, shownLength: M, truncated: true'
|
|
1077
|
+
- One-time token string to fetch full output
|
|
1078
|
+
- Suggestions block (conditional): compact tips for specialized tools based on script patterns
|
|
1079
|
+
|
|
1065
1080
|
### Network
|
|
1066
1081
|
|
|
1067
1082
|
#### `get_request_details`
|
|
@@ -1103,6 +1118,18 @@ Set the browser color scheme that controls CSS prefers-color-scheme. Defaults to
|
|
|
1103
1118
|
|
|
1104
1119
|
- Parameters:
|
|
1105
1120
|
- scheme (string, required): Color scheme to emulate: 'system', 'dark', 'light', or 'no-preference'. Example: { scheme: 'dark' }
|
|
1121
|
+
|
|
1122
|
+
### Other
|
|
1123
|
+
|
|
1124
|
+
#### `confirm_output`
|
|
1125
|
+
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.
|
|
1126
|
+
|
|
1127
|
+
- Parameters:
|
|
1128
|
+
- token (string, required): One-time token obtained from a tool's preview response
|
|
1129
|
+
|
|
1130
|
+
- Output Format:
|
|
1131
|
+
- Full original payload if token is valid (one-time)
|
|
1132
|
+
- Error: 'Invalid or expired token'
|
|
1106
1133
|
## Selector Shortcuts ⭐ Time-Saver
|
|
1107
1134
|
|
|
1108
1135
|
All browser tools support **convenient test ID shortcuts** that save typing and improve readability:
|
|
@@ -24,6 +24,11 @@ export declare abstract class BrowserToolBase implements ToolHandler {
|
|
|
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
|
+
private sanitizeSelectorEngineMessage;
|
|
27
32
|
/**
|
|
28
33
|
* Ensures a page is available and returns it
|
|
29
34
|
* @param context The tool context containing browser and page
|
|
@@ -33,7 +33,7 @@ export class BrowserToolBase {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
// Clean up common escaping mistakes that LLMs make in CSS selectors
|
|
36
|
-
// These characters don't need to be escaped in
|
|
36
|
+
// These characters don't need to be escaped in many selector contexts: [ ] :
|
|
37
37
|
let cleaned = selector;
|
|
38
38
|
// Remove backslash escapes before brackets and colons
|
|
39
39
|
// Handle both single escapes (\[) and double escapes (\\[)
|
|
@@ -42,6 +42,29 @@ export class BrowserToolBase {
|
|
|
42
42
|
cleaned = cleaned.replace(/\\+:/g, ':');
|
|
43
43
|
return cleaned;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Sanitize verbose Playwright selector engine messages by removing stack traces and
|
|
47
|
+
* keeping only the essential syntax error information.
|
|
48
|
+
*/
|
|
49
|
+
sanitizeSelectorEngineMessage(msg) {
|
|
50
|
+
if (!msg)
|
|
51
|
+
return '';
|
|
52
|
+
// Prefer to cut at the common phrase used by the browser
|
|
53
|
+
const cutoffPhrases = [
|
|
54
|
+
"is not a valid selector.",
|
|
55
|
+
"is not a valid selector",
|
|
56
|
+
];
|
|
57
|
+
for (const phrase of cutoffPhrases) {
|
|
58
|
+
const idx = msg.indexOf(phrase);
|
|
59
|
+
if (idx !== -1) {
|
|
60
|
+
return msg.slice(0, idx + phrase.length).trim();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Otherwise remove stack-like lines (e.g., " at query (…)")
|
|
64
|
+
const lines = msg.split(/\r?\n/);
|
|
65
|
+
const filtered = lines.filter(l => !/^\s*at\b/.test(l) && !/<anonymous>:\d+:\d+/.test(l));
|
|
66
|
+
return filtered.join('\n').trim();
|
|
67
|
+
}
|
|
45
68
|
/**
|
|
46
69
|
* Ensures a page is available and returns it
|
|
47
70
|
* @param context The tool context containing browser and page
|
|
@@ -164,20 +187,23 @@ export class BrowserToolBase {
|
|
|
164
187
|
errorMsg.includes('Invalid selector') ||
|
|
165
188
|
errorMsg.includes('SyntaxError') ||
|
|
166
189
|
errorMsg.includes('selector')) {
|
|
190
|
+
const conciseMsg = this.sanitizeSelectorEngineMessage(errorMsg);
|
|
191
|
+
// Helpful, accurate guidance with Tailwind-style examples
|
|
192
|
+
const tips = [
|
|
193
|
+
'💡 Tips:',
|
|
194
|
+
' • Tailwind arbitrary values need escaping in class selectors: .min-w-\\[300px\\]',
|
|
195
|
+
' • Colons in class names must be escaped: .dark\\:bg-gray-700',
|
|
196
|
+
' • Prefer robust selectors: use testid:name or [data-testid="..."]',
|
|
197
|
+
' • Attribute selectors avoid escaping issues: [class*="min-w-[300px]"]',
|
|
198
|
+
'',
|
|
199
|
+
'Examples:',
|
|
200
|
+
' ✓ .min-w-\\[300px\\] .flex-1',
|
|
201
|
+
' ✓ testid:submit-button',
|
|
202
|
+
' ✓ #login-form'
|
|
203
|
+
].join('\n');
|
|
167
204
|
throw new Error(`Invalid CSS selector: "${selector}"\n\n` +
|
|
168
|
-
`Selector syntax error: ${
|
|
169
|
-
|
|
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)`);
|
|
205
|
+
(conciseMsg ? `Selector syntax error: ${conciseMsg}\n\n` : '') +
|
|
206
|
+
tips);
|
|
181
207
|
}
|
|
182
208
|
// Re-throw other errors as-is
|
|
183
209
|
throw error;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GetConsoleLogsTool } from '../get_console_logs.js';
|
|
1
|
+
import { GetConsoleLogsTool, ClearConsoleLogsTool } from '../get_console_logs.js';
|
|
2
2
|
import { jest } from '@jest/globals';
|
|
3
3
|
// Mock the server
|
|
4
4
|
const mockServer = {
|
|
@@ -67,16 +67,14 @@ describe('GetConsoleLogsTool', () => {
|
|
|
67
67
|
const logText = result.content[1].text;
|
|
68
68
|
expect(logText).toContain('Test log message');
|
|
69
69
|
});
|
|
70
|
-
test('should clear console logs
|
|
70
|
+
test('should clear console logs using clear_console_logs tool', async () => {
|
|
71
71
|
consoleLogsTool.registerConsoleMessage('log', 'Test log message');
|
|
72
72
|
consoleLogsTool.registerConsoleMessage('error', 'Test error message');
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
};
|
|
76
|
-
const result = await consoleLogsTool.execute(args, mockContext);
|
|
73
|
+
const clearer = new ClearConsoleLogsTool(mockServer);
|
|
74
|
+
const result = await clearer.execute({}, mockContext);
|
|
77
75
|
expect(result.isError).toBe(false);
|
|
78
|
-
expect(result.content[0].text).toContain('
|
|
79
|
-
// Logs should be cleared
|
|
76
|
+
expect(result.content[0].text).toContain('Cleared 2 console log(s)');
|
|
77
|
+
// Logs should be cleared
|
|
80
78
|
const logs = consoleLogsTool.getConsoleLogs();
|
|
81
79
|
expect(logs.length).toBe(0);
|
|
82
80
|
});
|
|
@@ -8,6 +8,8 @@ export declare class GetConsoleLogsTool extends BrowserToolBase {
|
|
|
8
8
|
private lastCallTimestamp;
|
|
9
9
|
private lastNavigationTimestamp;
|
|
10
10
|
private lastInteractionTimestamp;
|
|
11
|
+
static latestInstance: GetConsoleLogsTool | null;
|
|
12
|
+
constructor(server: any);
|
|
11
13
|
static getMetadata(sessionConfig?: SessionConfig): ToolMetadata;
|
|
12
14
|
/**
|
|
13
15
|
* Register a console message
|
|
@@ -35,3 +37,10 @@ export declare class GetConsoleLogsTool extends BrowserToolBase {
|
|
|
35
37
|
*/
|
|
36
38
|
getLogsSinceLastNavigation(): string[];
|
|
37
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Tool for clearing console logs (atomic operation)
|
|
42
|
+
*/
|
|
43
|
+
export declare class ClearConsoleLogsTool extends BrowserToolBase {
|
|
44
|
+
static getMetadata(sessionConfig?: SessionConfig): ToolMetadata;
|
|
45
|
+
execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
46
|
+
}
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { BrowserToolBase } from '../base.js';
|
|
2
2
|
import { createSuccessResponse } from '../../common/types.js';
|
|
3
|
+
import { makeConfirmPreview } from '../../common/confirmHelpers.js';
|
|
3
4
|
/**
|
|
4
5
|
* Tool for retrieving and filtering console logs from the browser
|
|
5
6
|
*/
|
|
6
7
|
export class GetConsoleLogsTool extends BrowserToolBase {
|
|
7
|
-
constructor() {
|
|
8
|
-
super(
|
|
8
|
+
constructor(server) {
|
|
9
|
+
super(server);
|
|
10
|
+
// Stored logs and timestamps
|
|
9
11
|
this.consoleLogs = [];
|
|
10
12
|
this.lastCallTimestamp = 0;
|
|
11
13
|
this.lastNavigationTimestamp = 0;
|
|
12
14
|
this.lastInteractionTimestamp = 0;
|
|
15
|
+
GetConsoleLogsTool.latestInstance = this;
|
|
13
16
|
}
|
|
14
17
|
static getMetadata(sessionConfig) {
|
|
15
18
|
return {
|
|
16
19
|
name: "get_console_logs",
|
|
17
|
-
description: "Retrieve 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 a one-time token to fetch the full payload.",
|
|
18
21
|
inputSchema: {
|
|
19
22
|
type: "object",
|
|
20
23
|
properties: {
|
|
@@ -29,17 +32,18 @@ export class GetConsoleLogsTool extends BrowserToolBase {
|
|
|
29
32
|
},
|
|
30
33
|
limit: {
|
|
31
34
|
type: "number",
|
|
32
|
-
description: "Maximum
|
|
35
|
+
description: "Maximum entries to return (groups when grouped, lines when raw). Default: 20"
|
|
33
36
|
},
|
|
34
37
|
since: {
|
|
35
38
|
type: "string",
|
|
36
|
-
description: "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.)",
|
|
39
|
+
description: "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'",
|
|
37
40
|
enum: ["last-call", "last-navigation", "last-interaction"]
|
|
38
41
|
},
|
|
39
|
-
|
|
40
|
-
type: "
|
|
41
|
-
description: "
|
|
42
|
-
|
|
42
|
+
format: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "Output format: 'grouped' (default, deduped with counts) or 'raw' (chronological, ungrouped)",
|
|
45
|
+
enum: ["grouped", "raw"]
|
|
46
|
+
},
|
|
43
47
|
},
|
|
44
48
|
required: [],
|
|
45
49
|
},
|
|
@@ -68,11 +72,16 @@ export class GetConsoleLogsTool extends BrowserToolBase {
|
|
|
68
72
|
this.lastInteractionTimestamp = Date.now();
|
|
69
73
|
}
|
|
70
74
|
async execute(args, context) {
|
|
75
|
+
// Defaults
|
|
76
|
+
const format = args.format === 'raw' ? 'raw' : 'grouped';
|
|
77
|
+
const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.floor(args.limit) : 20;
|
|
78
|
+
const sinceArg = args.since || 'last-interaction';
|
|
79
|
+
const PREVIEW_THRESHOLD = 2000; // chars
|
|
71
80
|
let logs = [...this.consoleLogs];
|
|
72
81
|
// Filter by timestamp if 'since' parameter is specified
|
|
73
|
-
if (
|
|
82
|
+
if (sinceArg) {
|
|
74
83
|
let sinceTimestamp;
|
|
75
|
-
switch (
|
|
84
|
+
switch (sinceArg) {
|
|
76
85
|
case 'last-call':
|
|
77
86
|
sinceTimestamp = this.lastCallTimestamp;
|
|
78
87
|
break;
|
|
@@ -83,7 +92,7 @@ export class GetConsoleLogsTool extends BrowserToolBase {
|
|
|
83
92
|
sinceTimestamp = this.lastInteractionTimestamp;
|
|
84
93
|
break;
|
|
85
94
|
default:
|
|
86
|
-
return createSuccessResponse(`Invalid 'since' value: ${
|
|
95
|
+
return createSuccessResponse(`Invalid 'since' value: ${sinceArg}. Must be one of: last-call, last-navigation, last-interaction`);
|
|
87
96
|
}
|
|
88
97
|
logs = logs.filter(log => log.timestamp > sinceTimestamp);
|
|
89
98
|
}
|
|
@@ -97,26 +106,71 @@ export class GetConsoleLogsTool extends BrowserToolBase {
|
|
|
97
106
|
if (args.search) {
|
|
98
107
|
logs = logs.filter(log => log.message.includes(args.search));
|
|
99
108
|
}
|
|
100
|
-
//
|
|
101
|
-
if (
|
|
102
|
-
|
|
109
|
+
// Build output according to format
|
|
110
|
+
if (format === 'raw') {
|
|
111
|
+
// Chronological lines, limit applied to last N entries
|
|
112
|
+
const limited = limit > 0 ? logs.slice(-limit) : logs;
|
|
113
|
+
const messages = limited.map(l => l.message);
|
|
114
|
+
if (messages.length === 0) {
|
|
115
|
+
return createSuccessResponse("No console logs matching the criteria");
|
|
116
|
+
}
|
|
117
|
+
// Guard large outputs by character size
|
|
118
|
+
const header = `Retrieved ${messages.length} console log(s):`;
|
|
119
|
+
const textPayload = [header, ...messages].join('\n');
|
|
120
|
+
if (textPayload.length >= PREVIEW_THRESHOLD) {
|
|
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'));
|
|
131
|
+
}
|
|
132
|
+
return createSuccessResponse([header, ...messages]);
|
|
103
133
|
}
|
|
104
|
-
//
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
134
|
+
// Grouped format (default)
|
|
135
|
+
const groups = new Map();
|
|
136
|
+
for (const l of logs) {
|
|
137
|
+
const key = l.message; // includes [type] prefix per registerConsoleMessage
|
|
138
|
+
const g = groups.get(key);
|
|
139
|
+
if (g) {
|
|
140
|
+
g.count += 1;
|
|
141
|
+
g.lastTs = l.timestamp;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
groups.set(key, { count: 1, firstTs: l.timestamp, lastTs: l.timestamp, example: l.message });
|
|
145
|
+
}
|
|
109
146
|
}
|
|
110
|
-
|
|
111
|
-
if (messages.length === 0) {
|
|
147
|
+
if (groups.size === 0) {
|
|
112
148
|
return createSuccessResponse("No console logs matching the criteria");
|
|
113
149
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
150
|
+
// Order groups by first occurrence time
|
|
151
|
+
const ordered = Array.from(groups.entries()).sort((a, b) => a[1].firstTs - b[1].firstTs);
|
|
152
|
+
const limitedGroups = limit > 0 ? ordered.slice(0, limit) : ordered;
|
|
153
|
+
const lines = [];
|
|
154
|
+
lines.push(`Retrieved ${limitedGroups.length} console log(s):`);
|
|
155
|
+
for (const [msg, info] of limitedGroups) {
|
|
156
|
+
const line = `${msg} (× ${info.count})`;
|
|
157
|
+
lines.push(line);
|
|
119
158
|
}
|
|
159
|
+
// Guard large grouped outputs
|
|
160
|
+
const textPayload = lines.join('\n');
|
|
161
|
+
if (textPayload.length >= PREVIEW_THRESHOLD) {
|
|
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'));
|
|
172
|
+
}
|
|
173
|
+
return createSuccessResponse(lines);
|
|
120
174
|
}
|
|
121
175
|
/**
|
|
122
176
|
* Get all console logs
|
|
@@ -140,3 +194,30 @@ export class GetConsoleLogsTool extends BrowserToolBase {
|
|
|
140
194
|
.map(log => log.message);
|
|
141
195
|
}
|
|
142
196
|
}
|
|
197
|
+
// Track latest instance for sibling tool access (module-level singleton pattern)
|
|
198
|
+
GetConsoleLogsTool.latestInstance = null;
|
|
199
|
+
/**
|
|
200
|
+
* Tool for clearing console logs (atomic operation)
|
|
201
|
+
*/
|
|
202
|
+
export class ClearConsoleLogsTool extends BrowserToolBase {
|
|
203
|
+
static getMetadata(sessionConfig) {
|
|
204
|
+
return {
|
|
205
|
+
name: "clear_console_logs",
|
|
206
|
+
description: "Clears captured console logs and returns the number of entries cleared.",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: "object",
|
|
209
|
+
properties: {},
|
|
210
|
+
required: [],
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
async execute(args, context) {
|
|
215
|
+
const inst = GetConsoleLogsTool.latestInstance;
|
|
216
|
+
if (!inst) {
|
|
217
|
+
return createSuccessResponse('Cleared 0 console log(s)');
|
|
218
|
+
}
|
|
219
|
+
const count = inst.getConsoleLogs().length;
|
|
220
|
+
inst.clearConsoleLogs();
|
|
221
|
+
return createSuccessResponse(`Cleared ${count} console log(s)`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { GetConsoleLogsTool } from './get_console_logs.js';
|
|
1
|
+
export { GetConsoleLogsTool, ClearConsoleLogsTool } from './get_console_logs.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { GetConsoleLogsTool } from './get_console_logs.js';
|
|
1
|
+
export { GetConsoleLogsTool, ClearConsoleLogsTool } from './get_console_logs.js';
|
|
@@ -392,9 +392,9 @@ describe('GetHtmlTool', () => {
|
|
|
392
392
|
expect(result.content[0].text).toContain('⚠️ Full HTML not returned to save tokens');
|
|
393
393
|
expect(result.content[0].text).toContain('💡 RECOMMENDED: Use token-efficient alternatives');
|
|
394
394
|
expect(result.content[0].text).toContain('inspect_dom()');
|
|
395
|
-
expect(result.content[0].text).toMatch(/
|
|
395
|
+
expect(result.content[0].text).toMatch(/confirm_output\(\{ token: \"[\w\d]+\" \}\)/);
|
|
396
396
|
});
|
|
397
|
-
test('should return full HTML with valid
|
|
397
|
+
test('should return full HTML with valid token via confirm_output', async () => {
|
|
398
398
|
const args = {};
|
|
399
399
|
// Large HTML
|
|
400
400
|
const largeHtml = '<div>' + 'Y'.repeat(3000) + '</div>';
|
|
@@ -402,29 +402,22 @@ describe('GetHtmlTool', () => {
|
|
|
402
402
|
// First call - get preview and token
|
|
403
403
|
const previewResult = await visibleHtmlTool.execute(args, mockContext);
|
|
404
404
|
expect(previewResult.isError).toBe(false);
|
|
405
|
-
// Extract token from preview
|
|
406
|
-
const tokenMatch = previewResult.content[0].text.match(/
|
|
405
|
+
// Extract token from preview and confirm using confirm_output tool
|
|
406
|
+
const tokenMatch = previewResult.content[0].text.match(/confirm_output\(\{ token: \"([\w\d]+)\" \}\)/);
|
|
407
407
|
expect(tokenMatch).toBeTruthy();
|
|
408
408
|
const token = tokenMatch[1];
|
|
409
|
-
|
|
410
|
-
const
|
|
409
|
+
const { ConfirmOutputTool } = await import('../../../common/confirm_output.js');
|
|
410
|
+
const confirmTool = new ConfirmOutputTool({});
|
|
411
|
+
const fullResult = await confirmTool.execute({ token }, {});
|
|
411
412
|
expect(fullResult.isError).toBe(false);
|
|
412
413
|
expect(fullResult.content[0].text).toContain(largeHtml);
|
|
413
|
-
expect(fullResult.content[0].text).not.toContain('Preview (first 500 chars)');
|
|
414
|
-
expect(fullResult.content[0].text).toContain('💡 TIP: If you need structured inspection');
|
|
415
414
|
});
|
|
416
|
-
test('should
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
expect(result.isError).toBe(false);
|
|
423
|
-
expect(result.content[0].text).toContain('HTML size:');
|
|
424
|
-
expect(result.content[0].text).toContain('Preview (first 500 chars)');
|
|
425
|
-
expect(result.content[0].text).toMatch(/confirmToken: "[\w\d]{8}"/);
|
|
426
|
-
// Should not return full HTML
|
|
427
|
-
expect(result.content[0].text).not.toContain('Z'.repeat(3000));
|
|
415
|
+
test('confirm_output should error on invalid token', async () => {
|
|
416
|
+
const { ConfirmOutputTool } = await import('../../../common/confirm_output.js');
|
|
417
|
+
const confirmTool = new ConfirmOutputTool({});
|
|
418
|
+
const res = await confirmTool.execute({ token: 'invalid123' }, {});
|
|
419
|
+
expect(res.isError).toBe(true);
|
|
420
|
+
expect(res.content[0].text).toContain('Invalid or expired token');
|
|
428
421
|
});
|
|
429
422
|
test('should return small HTML directly without token requirement', async () => {
|
|
430
423
|
const args = {};
|
|
@@ -445,22 +438,20 @@ describe('GetHtmlTool', () => {
|
|
|
445
438
|
mockEvaluate.mockImplementation(() => Promise.resolve(largeHtml));
|
|
446
439
|
// First call - get token
|
|
447
440
|
const previewResult = await visibleHtmlTool.execute(args, mockContext);
|
|
448
|
-
const tokenMatch = previewResult.content[0].text.match(/
|
|
441
|
+
const tokenMatch = previewResult.content[0].text.match(/confirm_output\(\{ token: \"([\w\d]+)\" \}\)/);
|
|
449
442
|
const token = tokenMatch[1];
|
|
443
|
+
const { ConfirmOutputTool } = await import('../../../common/confirm_output.js');
|
|
444
|
+
const confirmTool = new ConfirmOutputTool({});
|
|
450
445
|
// Second call - use token (should work)
|
|
451
|
-
const fullResult = await
|
|
446
|
+
const fullResult = await confirmTool.execute({ token }, {});
|
|
452
447
|
expect(fullResult.isError).toBe(false);
|
|
453
448
|
expect(fullResult.content[0].text).toContain(largeHtml);
|
|
454
|
-
// Third call - try to reuse same token (should fail
|
|
455
|
-
const retryResult = await
|
|
456
|
-
expect(retryResult.isError).toBe(
|
|
457
|
-
expect(retryResult.content[0].text).toContain('
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const newTokenMatch = retryResult.content[0].text.match(/confirmToken: "([\w\d]{8})"/);
|
|
461
|
-
expect(newTokenMatch[1]).not.toBe(token);
|
|
462
|
-
});
|
|
463
|
-
test('should handle large HTML with selector and confirmToken', async () => {
|
|
449
|
+
// Third call - try to reuse same token (should fail)
|
|
450
|
+
const retryResult = await confirmTool.execute({ token }, {});
|
|
451
|
+
expect(retryResult.isError).toBe(true);
|
|
452
|
+
expect(retryResult.content[0].text).toContain('Invalid or expired token');
|
|
453
|
+
});
|
|
454
|
+
test('should handle large HTML with selector and confirm_output', async () => {
|
|
464
455
|
const largeHtml = '<section>' + 'S'.repeat(3000) + '</section>';
|
|
465
456
|
const args = { selector: 'testid:large-section' };
|
|
466
457
|
mockLocator.mockImplementation(() => ({}));
|
|
@@ -474,10 +465,12 @@ describe('GetHtmlTool', () => {
|
|
|
474
465
|
// First call - get preview
|
|
475
466
|
const previewResult = await visibleHtmlTool.execute(args, mockContext);
|
|
476
467
|
expect(previewResult.content[0].text).toContain('(from "testid:large-section")');
|
|
477
|
-
const tokenMatch = previewResult.content[0].text.match(/
|
|
468
|
+
const tokenMatch = previewResult.content[0].text.match(/confirm_output\(\{ token: \"([\w\d]+)\" \}\)/);
|
|
478
469
|
const token = tokenMatch[1];
|
|
479
|
-
// Second call - with token
|
|
480
|
-
const
|
|
470
|
+
// Second call - with token via confirm tool
|
|
471
|
+
const { ConfirmOutputTool } = await import('../../../common/confirm_output.js');
|
|
472
|
+
const confirmTool = new ConfirmOutputTool({});
|
|
473
|
+
const fullResult = await confirmTool.execute({ token }, {});
|
|
481
474
|
expect(fullResult.isError).toBe(false);
|
|
482
475
|
expect(fullResult.content[0].text).toContain(largeHtml);
|
|
483
476
|
selectSpy.mockRestore();
|
|
@@ -4,7 +4,6 @@ import { ToolContext, ToolResponse, ToolMetadata, SessionConfig } from '../../co
|
|
|
4
4
|
* Tool for getting HTML from the page
|
|
5
5
|
*/
|
|
6
6
|
export declare class GetHtmlTool extends BrowserToolBase {
|
|
7
|
-
private confirmTokens;
|
|
8
7
|
static getMetadata(sessionConfig?: SessionConfig): ToolMetadata;
|
|
9
8
|
execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
10
9
|
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { BrowserToolBase } from '../base.js';
|
|
2
2
|
import { createSuccessResponse, createErrorResponse, } from '../../common/types.js';
|
|
3
|
+
import { makeConfirmPreview } from '../../common/confirmHelpers.js';
|
|
3
4
|
/**
|
|
4
5
|
* Tool for getting HTML from the page
|
|
5
6
|
*/
|
|
6
7
|
export class GetHtmlTool extends BrowserToolBase {
|
|
7
|
-
constructor() {
|
|
8
|
-
super(...arguments);
|
|
9
|
-
this.confirmTokens = new Map(); // Stores tokens for two-step confirmation
|
|
10
|
-
}
|
|
11
8
|
static getMetadata(sessionConfig) {
|
|
12
9
|
return {
|
|
13
10
|
name: "get_html",
|
|
14
|
-
description: "⚠️ 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),
|
|
11
|
+
description: "⚠️ 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.",
|
|
15
12
|
inputSchema: {
|
|
16
13
|
type: "object",
|
|
17
14
|
properties: {
|
|
@@ -30,10 +27,6 @@ export class GetHtmlTool extends BrowserToolBase {
|
|
|
30
27
|
maxLength: {
|
|
31
28
|
type: "number",
|
|
32
29
|
description: "Maximum number of characters to return (default: 20000)"
|
|
33
|
-
},
|
|
34
|
-
confirmToken: {
|
|
35
|
-
type: "string",
|
|
36
|
-
description: "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."
|
|
37
30
|
}
|
|
38
31
|
},
|
|
39
32
|
required: [],
|
|
@@ -45,7 +38,6 @@ export class GetHtmlTool extends BrowserToolBase {
|
|
|
45
38
|
? Math.floor(args.maxLength)
|
|
46
39
|
: 20000;
|
|
47
40
|
const clean = args.clean ?? false;
|
|
48
|
-
const confirmToken = args.confirmToken;
|
|
49
41
|
const PREVIEW_THRESHOLD = 2000;
|
|
50
42
|
if (!context.page) {
|
|
51
43
|
return createErrorResponse('Page is not available');
|
|
@@ -111,37 +103,27 @@ export class GetHtmlTool extends BrowserToolBase {
|
|
|
111
103
|
const processedHtml = sanitizedHtml ?? '';
|
|
112
104
|
const originalLength = processedHtml.length;
|
|
113
105
|
// Generate key for this HTML request
|
|
114
|
-
|
|
115
|
-
// Check if HTML is too large
|
|
106
|
+
// Check if HTML is too large => return preview + token for confirm_output
|
|
116
107
|
if (originalLength >= PREVIEW_THRESHOLD) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
lines.push('');
|
|
137
|
-
lines.push('💡 RECOMMENDED: Use token-efficient alternatives:');
|
|
138
|
-
lines.push(' • inspect_dom() - structured view with positions and layout');
|
|
139
|
-
lines.push(' • query_selector_all() - find specific elements');
|
|
140
|
-
lines.push(' • get_computed_styles() - CSS values for debugging');
|
|
141
|
-
lines.push('');
|
|
142
|
-
lines.push(`To get full HTML anyway, call again with: confirmToken: "${newToken}"`);
|
|
143
|
-
return createSuccessResponse(lines.join('\n'));
|
|
144
|
-
}
|
|
108
|
+
const preview = makeConfirmPreview(processedHtml, {
|
|
109
|
+
counts: { totalLength: originalLength, shownLength: Math.min(500, originalLength), truncated: true },
|
|
110
|
+
previewLines: [
|
|
111
|
+
'Preview (first 500 chars):',
|
|
112
|
+
processedHtml.slice(0, 500),
|
|
113
|
+
...(originalLength > 500 ? ['...'] : []),
|
|
114
|
+
'',
|
|
115
|
+
'⚠️ Full HTML not returned to save tokens',
|
|
116
|
+
'',
|
|
117
|
+
'💡 RECOMMENDED: Use token-efficient alternatives:',
|
|
118
|
+
' • inspect_dom() - structured view with positions and layout',
|
|
119
|
+
' • query_selector_all() - find specific elements',
|
|
120
|
+
' • get_computed_styles() - CSS values for debugging',
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
lines.push(`HTML size: ${originalLength.toLocaleString()} characters (exceeds ${PREVIEW_THRESHOLD} char threshold)`);
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push(...preview.lines);
|
|
126
|
+
return createSuccessResponse(lines.join('\n'));
|
|
145
127
|
}
|
|
146
128
|
// Return full HTML (either small or explicitly requested)
|
|
147
129
|
let displayHtml = processedHtml;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BrowserToolBase } from '../base.js';
|
|
2
|
-
import { createSuccessResponse } from '../../common/types.js';
|
|
2
|
+
import { createSuccessResponse, createErrorResponse, } from '../../common/types.js';
|
|
3
|
+
import { makeConfirmPreview } from '../../common/confirmHelpers.js';
|
|
3
4
|
/**
|
|
4
5
|
* Tool for executing JavaScript in the browser
|
|
5
6
|
*/
|
|
@@ -7,7 +8,20 @@ export class EvaluateTool extends BrowserToolBase {
|
|
|
7
8
|
static getMetadata(sessionConfig) {
|
|
8
9
|
return {
|
|
9
10
|
name: "evaluate",
|
|
10
|
-
description: "⚙️ CUSTOM JAVASCRIPT EXECUTION - Execute arbitrary JavaScript in the browser console and return the result
|
|
11
|
+
description: "⚙️ 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.",
|
|
12
|
+
outputs: [
|
|
13
|
+
"Header: '✓ JavaScript execution result:'",
|
|
14
|
+
"Default result: compact summary string (arrays/objects/dom nodes summarized)",
|
|
15
|
+
"Array summary: 'Array(n) [first, second, third…]' (shows first 3 items)",
|
|
16
|
+
"Object summary (large): 'Object(n keys): key1, key2, key3…' (top-level keys only)",
|
|
17
|
+
"DOM node summary: '<tag id=#id class=.a.b> @ (x,y) WxH' (rounded ints)",
|
|
18
|
+
"NodeList/HTMLCollection summary: 'NodeList(n) [<div…>, <span…>, <a…>…]'",
|
|
19
|
+
"Preview guard when result is large (≥ ~2000 chars):",
|
|
20
|
+
" - 'Preview (first 500 chars):' followed by excerpt",
|
|
21
|
+
" - Counts: 'totalLength: N, shownLength: M, truncated: true'",
|
|
22
|
+
" - One-time token string to fetch full output",
|
|
23
|
+
"Suggestions block (conditional): compact tips for specialized tools based on script patterns",
|
|
24
|
+
],
|
|
11
25
|
inputSchema: {
|
|
12
26
|
type: "object",
|
|
13
27
|
properties: {
|
|
@@ -32,7 +46,7 @@ export class EvaluateTool extends BrowserToolBase {
|
|
|
32
46
|
// Pattern: Getting text content
|
|
33
47
|
if (scriptLower.match(/textcontent|innertext/)) {
|
|
34
48
|
suggestions.push('📝 Text Content\n' +
|
|
35
|
-
' •
|
|
49
|
+
' • get_text - Extract all visible text\n' +
|
|
36
50
|
' • find_by_text({ text: "..." }) - Locate elements by content');
|
|
37
51
|
}
|
|
38
52
|
// Pattern: Checking if element is scrollable (scrollHeight > clientHeight)
|
|
@@ -60,7 +74,7 @@ export class EvaluateTool extends BrowserToolBase {
|
|
|
60
74
|
}
|
|
61
75
|
// Pattern: Checking visibility
|
|
62
76
|
if (scriptLower.match(/offsetparent|visibility|display.*none|opacity/)) {
|
|
63
|
-
suggestions.push('👁️ Visibility Check - Use
|
|
77
|
+
suggestions.push('👁️ Visibility Check - Use check_visibility({ selector: "..." })\n' +
|
|
64
78
|
' Returns: isVisible, inViewport, opacity, display, visibility properties\n' +
|
|
65
79
|
' More reliable: Handles edge cases (opacity:0, visibility:hidden, etc.)');
|
|
66
80
|
}
|
|
@@ -85,7 +99,7 @@ export class EvaluateTool extends BrowserToolBase {
|
|
|
85
99
|
// Pattern: Comparing positions/alignment
|
|
86
100
|
if (scriptLower.match(/getboundingclientrect.*getboundingclientrect/) ||
|
|
87
101
|
(scriptLower.match(/\.left|\.top|\.right|\.bottom/) && scriptLower.match(/===|==|!==|!=/))) {
|
|
88
|
-
suggestions.push('⚖️ Position Comparison - Use
|
|
102
|
+
suggestions.push('⚖️ Position Comparison - Use compare_element_alignment({ selector1: "...", selector2: "..." })\n' +
|
|
89
103
|
' Returns: Alignment status (left/right/top/bottom/center), pixel gaps\n' +
|
|
90
104
|
' Perfect for: Checking if elements are aligned or overlapping');
|
|
91
105
|
}
|
|
@@ -104,21 +118,182 @@ export class EvaluateTool extends BrowserToolBase {
|
|
|
104
118
|
async execute(args, context) {
|
|
105
119
|
this.recordInteraction();
|
|
106
120
|
return this.safeExecute(context, async (page) => {
|
|
107
|
-
const
|
|
108
|
-
//
|
|
121
|
+
const PREVIEW_THRESHOLD = 2000; // chars
|
|
122
|
+
// Execute the script and produce a compact textual summary entirely in the page context
|
|
123
|
+
// to safely handle DOM nodes and browser-specific objects.
|
|
124
|
+
const evalReturn = await page.evaluate(async (userScript) => {
|
|
125
|
+
const toInt = (n) => Math.max(0, Math.round(n || 0));
|
|
126
|
+
// Summarize a DOM element
|
|
127
|
+
const summarizeElement = (el) => {
|
|
128
|
+
try {
|
|
129
|
+
const tag = (el.tagName || '').toLowerCase();
|
|
130
|
+
const id = el.id ? ` #${el.id}` : '';
|
|
131
|
+
const cls = el.classList?.length
|
|
132
|
+
? ' ' + Array.from(el.classList)
|
|
133
|
+
.map(c => `.${c}`)
|
|
134
|
+
.join('')
|
|
135
|
+
: '';
|
|
136
|
+
const rect = el.getBoundingClientRect?.();
|
|
137
|
+
const x = toInt(rect?.left ?? 0);
|
|
138
|
+
const y = toInt(rect?.top ?? 0);
|
|
139
|
+
const w = toInt(rect?.width ?? 0);
|
|
140
|
+
const h = toInt(rect?.height ?? 0);
|
|
141
|
+
return `<${tag}${id}${cls}> @ (${x},${y}) ${w}x${h}`;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
const tag = (el.tagName || '').toLowerCase();
|
|
145
|
+
return `<${tag}>`;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
// Render values compactly
|
|
149
|
+
const render = (val, depth, seen) => {
|
|
150
|
+
const MAX_DEPTH = 3;
|
|
151
|
+
const ARRAY_PREVIEW = 3;
|
|
152
|
+
const LARGE_ARRAY_THRESHOLD = 10;
|
|
153
|
+
const LARGE_OBJECT_THRESHOLD = 15;
|
|
154
|
+
const t = Object.prototype.toString.call(val);
|
|
155
|
+
if (val === null)
|
|
156
|
+
return 'null';
|
|
157
|
+
if (val === undefined)
|
|
158
|
+
return 'undefined';
|
|
159
|
+
if (typeof val === 'string')
|
|
160
|
+
return JSON.stringify(val);
|
|
161
|
+
if (typeof val === 'number' || typeof val === 'boolean')
|
|
162
|
+
return String(val);
|
|
163
|
+
if (typeof val === 'bigint')
|
|
164
|
+
return `${String(val)}n`;
|
|
165
|
+
if (typeof val === 'function')
|
|
166
|
+
return `[Function ${val.name || 'anonymous'}]`;
|
|
167
|
+
if (t === '[object Date]')
|
|
168
|
+
return `Date(${val.toISOString?.() || String(val)})`;
|
|
169
|
+
if (t === '[object RegExp]')
|
|
170
|
+
return String(val);
|
|
171
|
+
if (t === '[object Error]')
|
|
172
|
+
return `${val.name || 'Error'}: ${val.message || String(val)}`;
|
|
173
|
+
// DOM element
|
|
174
|
+
if (typeof Element !== 'undefined' && val instanceof Element) {
|
|
175
|
+
return summarizeElement(val);
|
|
176
|
+
}
|
|
177
|
+
// NodeList / HTMLCollection
|
|
178
|
+
if ((typeof NodeList !== 'undefined' && val instanceof NodeList) ||
|
|
179
|
+
(typeof HTMLCollection !== 'undefined' && val instanceof HTMLCollection)) {
|
|
180
|
+
const arr = Array.from(val);
|
|
181
|
+
const head = arr.slice(0, ARRAY_PREVIEW).map((e) => typeof Element !== 'undefined' && e instanceof Element ? summarizeElement(e) : render(e, depth + 1, seen));
|
|
182
|
+
const more = arr.length > ARRAY_PREVIEW ? '…' : '';
|
|
183
|
+
return `NodeList(${arr.length}) [${head.join(', ')}${more}]`;
|
|
184
|
+
}
|
|
185
|
+
if (depth >= MAX_DEPTH) {
|
|
186
|
+
if (Array.isArray(val))
|
|
187
|
+
return `Array(${val.length}) […]`;
|
|
188
|
+
if (val && typeof val === 'object')
|
|
189
|
+
return `Object(${Object.keys(val).length} keys) …`;
|
|
190
|
+
return String(val);
|
|
191
|
+
}
|
|
192
|
+
// Avoid circular structures
|
|
193
|
+
if (val && typeof val === 'object') {
|
|
194
|
+
if (seen.has(val))
|
|
195
|
+
return '[Circular]';
|
|
196
|
+
seen.add(val);
|
|
197
|
+
}
|
|
198
|
+
if (Array.isArray(val)) {
|
|
199
|
+
if (val.length > LARGE_ARRAY_THRESHOLD) {
|
|
200
|
+
const head = val.slice(0, ARRAY_PREVIEW).map((v) => render(v, depth + 1, seen));
|
|
201
|
+
const more = val.length > ARRAY_PREVIEW ? '…' : '';
|
|
202
|
+
return `Array(${val.length}) [${head.join(', ')}${more}]`;
|
|
203
|
+
}
|
|
204
|
+
return `[${val.map((v) => render(v, depth + 1, seen)).join(', ')}]`;
|
|
205
|
+
}
|
|
206
|
+
// Map / Set
|
|
207
|
+
if (t === '[object Map]') {
|
|
208
|
+
const m = val;
|
|
209
|
+
const entries = Array.from(m.entries()).slice(0, ARRAY_PREVIEW).map(([k, v]) => `${render(k, depth + 1, seen)} => ${render(v, depth + 1, seen)}`);
|
|
210
|
+
const more = m.size > ARRAY_PREVIEW ? '…' : '';
|
|
211
|
+
return `Map(${m.size}) {${entries.join(', ')}${more}}`;
|
|
212
|
+
}
|
|
213
|
+
if (t === '[object Set]') {
|
|
214
|
+
const s = val;
|
|
215
|
+
const entries = Array.from(s.values()).slice(0, ARRAY_PREVIEW).map((v) => render(v, depth + 1, seen));
|
|
216
|
+
const more = s.size > ARRAY_PREVIEW ? '…' : '';
|
|
217
|
+
return `Set(${s.size}) {${entries.join(', ')}${more}}`;
|
|
218
|
+
}
|
|
219
|
+
if (val && typeof val === 'object') {
|
|
220
|
+
const keys = Object.keys(val);
|
|
221
|
+
if (keys.length > LARGE_OBJECT_THRESHOLD) {
|
|
222
|
+
const head = keys.slice(0, ARRAY_PREVIEW).join(', ');
|
|
223
|
+
const more = keys.length > ARRAY_PREVIEW ? '…' : '';
|
|
224
|
+
return `Object(${keys.length} keys): ${head}${more}`;
|
|
225
|
+
}
|
|
226
|
+
// Render small object inline key: value
|
|
227
|
+
const parts = [];
|
|
228
|
+
for (const k of keys) {
|
|
229
|
+
try {
|
|
230
|
+
parts.push(`${k}: ${render(val[k], depth + 1, seen)}`);
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
parts.push(`${k}: [Unserializable]`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return `{ ${parts.join(', ')} }`;
|
|
237
|
+
}
|
|
238
|
+
return String(val);
|
|
239
|
+
};
|
|
240
|
+
try {
|
|
241
|
+
// Build an async function so both sync and async scripts are supported
|
|
242
|
+
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
243
|
+
const fn = new AsyncFunction(userScript);
|
|
244
|
+
const result = await fn();
|
|
245
|
+
const text = render(result, 0, new WeakSet());
|
|
246
|
+
return { ok: true, text };
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
return { ok: false, error: e?.message || String(e) };
|
|
250
|
+
}
|
|
251
|
+
}, args.script);
|
|
252
|
+
// Backward compatibility: if the page evaluation returns a raw value (string/any)
|
|
253
|
+
// instead of the { ok, text } envelope, treat it as the final result string.
|
|
109
254
|
let resultStr;
|
|
110
|
-
|
|
111
|
-
|
|
255
|
+
if (evalReturn && typeof evalReturn === 'object' && 'ok' in evalReturn) {
|
|
256
|
+
const { ok, text, error: execError } = evalReturn;
|
|
257
|
+
if (!ok) {
|
|
258
|
+
return createErrorResponse(`JavaScript execution failed: ${execError}`);
|
|
259
|
+
}
|
|
260
|
+
resultStr = text || '';
|
|
112
261
|
}
|
|
113
|
-
|
|
114
|
-
|
|
262
|
+
else {
|
|
263
|
+
try {
|
|
264
|
+
resultStr = typeof evalReturn === 'string' ? evalReturn : JSON.stringify(evalReturn, null, 2);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
resultStr = String(evalReturn);
|
|
268
|
+
}
|
|
115
269
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
];
|
|
120
|
-
// Detect if specialized tools would be better
|
|
270
|
+
// Guard for large outputs: preview + confirm
|
|
271
|
+
const totalLength = resultStr.length;
|
|
272
|
+
const lines = [];
|
|
121
273
|
const suggestions = this.detectBetterToolSuggestions(args.script);
|
|
274
|
+
if (totalLength >= PREVIEW_THRESHOLD) {
|
|
275
|
+
const previewLen = Math.min(500, totalLength);
|
|
276
|
+
const preview = resultStr.slice(0, previewLen);
|
|
277
|
+
const previewBlock = makeConfirmPreview(resultStr, {
|
|
278
|
+
headerLine: '✓ JavaScript execution result (preview):',
|
|
279
|
+
counts: { totalLength, shownLength: previewLen, truncated: true },
|
|
280
|
+
previewLines: [
|
|
281
|
+
'Preview (first 500 chars):',
|
|
282
|
+
preview,
|
|
283
|
+
...(totalLength > previewLen ? ['...'] : []),
|
|
284
|
+
],
|
|
285
|
+
extraTips: ['Tip: Prefer specialized tools or narrow the script when possible.'],
|
|
286
|
+
});
|
|
287
|
+
lines.push(...previewBlock.lines);
|
|
288
|
+
if (suggestions.length > 0) {
|
|
289
|
+
lines.push('');
|
|
290
|
+
lines.push('💡 Consider specialized tools:');
|
|
291
|
+
suggestions.forEach(s => lines.push(` ${s}`));
|
|
292
|
+
}
|
|
293
|
+
return createSuccessResponse(lines);
|
|
294
|
+
}
|
|
295
|
+
const messages = [`✓ JavaScript execution result:`, resultStr];
|
|
296
|
+
// Detect if specialized tools would be better
|
|
122
297
|
if (suggestions.length > 0) {
|
|
123
298
|
messages.push('');
|
|
124
299
|
messages.push('💡 Consider using specialized tools instead:');
|
|
@@ -330,4 +330,27 @@ describe('MeasureElementTool', () => {
|
|
|
330
330
|
expect(text).toContain('Margin:');
|
|
331
331
|
expect(text).toMatch(/↑15px|↓15px|←15px|→15px/); // Should show margin with arrows
|
|
332
332
|
});
|
|
333
|
+
it('returns clean error for invalid CSS selector without stack trace', async () => {
|
|
334
|
+
await page.setContent(`
|
|
335
|
+
<html>
|
|
336
|
+
<body>
|
|
337
|
+
<div class="flex min-w-[300px] flex-1">Hello</div>
|
|
338
|
+
</body>
|
|
339
|
+
</html>
|
|
340
|
+
`);
|
|
341
|
+
// Intentionally pass an invalid selector (missing escapes for [ ])
|
|
342
|
+
const result = await tool.execute({ selector: '.flex.min-w-[300px].flex-1' }, { page, browser });
|
|
343
|
+
expect(result.isError).toBe(true);
|
|
344
|
+
const text = result.content[0].text;
|
|
345
|
+
// Should present a concise, user-friendly error without stack traces
|
|
346
|
+
expect(text).toContain('Invalid CSS selector:');
|
|
347
|
+
expect(text).toContain('Selector syntax error:');
|
|
348
|
+
expect(text).toContain('not a valid selector');
|
|
349
|
+
// No internal stack frames from the selector engine
|
|
350
|
+
expect(text).not.toMatch(/\n\s*at\b/);
|
|
351
|
+
expect(text).not.toContain('<anonymous>:');
|
|
352
|
+
// Provide helpful guidance for Tailwind-style class names
|
|
353
|
+
expect(text).toContain('Tailwind arbitrary values need escaping');
|
|
354
|
+
expect(text).toContain('.min-w-\\[300px\\]');
|
|
355
|
+
});
|
|
333
356
|
});
|
|
@@ -219,7 +219,11 @@ describe('Browser Interaction Tools', () => {
|
|
|
219
219
|
script: 'return document.title'
|
|
220
220
|
};
|
|
221
221
|
const result = await evaluateTool.execute(args, mockContext);
|
|
222
|
-
|
|
222
|
+
// evaluate() wraps script inside a function passed to page.evaluate
|
|
223
|
+
expect(mockEvaluate).toHaveBeenCalled();
|
|
224
|
+
const argsPassed = mockEvaluate.mock.calls[0];
|
|
225
|
+
expect(typeof argsPassed[0]).toBe('function');
|
|
226
|
+
expect(argsPassed[1]).toBe('return document.title');
|
|
223
227
|
expect(result.isError).toBe(false);
|
|
224
228
|
expect(result.content[0].text).toContain('JavaScript execution result');
|
|
225
229
|
});
|
|
@@ -233,7 +237,7 @@ describe('Browser Interaction Tools', () => {
|
|
|
233
237
|
expect(fullResponse).toContain('💡 Consider using specialized tools instead');
|
|
234
238
|
expect(fullResponse).toContain('inspect_dom');
|
|
235
239
|
});
|
|
236
|
-
test('should suggest
|
|
240
|
+
test('should suggest get_text for textContent usage', async () => {
|
|
237
241
|
const args = {
|
|
238
242
|
script: 'document.body.textContent'
|
|
239
243
|
};
|
|
@@ -241,7 +245,7 @@ describe('Browser Interaction Tools', () => {
|
|
|
241
245
|
const fullResponse = result.content.map(c => c.text).join('\n');
|
|
242
246
|
expect(result.isError).toBe(false);
|
|
243
247
|
expect(fullResponse).toContain('💡 Consider using specialized tools instead');
|
|
244
|
-
expect(fullResponse).toContain('
|
|
248
|
+
expect(fullResponse).toContain('get_text');
|
|
245
249
|
});
|
|
246
250
|
test('should suggest measure_element for getBoundingClientRect usage', async () => {
|
|
247
251
|
const args = {
|
|
@@ -253,7 +257,7 @@ describe('Browser Interaction Tools', () => {
|
|
|
253
257
|
expect(fullResponse).toContain('💡 Consider using specialized tools instead');
|
|
254
258
|
expect(fullResponse).toContain('measure_element');
|
|
255
259
|
});
|
|
256
|
-
test('should suggest
|
|
260
|
+
test('should suggest check_visibility for visibility checks', async () => {
|
|
257
261
|
const args = {
|
|
258
262
|
script: 'window.getComputedStyle(document.querySelector("#el")).visibility'
|
|
259
263
|
};
|
|
@@ -261,7 +265,7 @@ describe('Browser Interaction Tools', () => {
|
|
|
261
265
|
const fullResponse = result.content.map(c => c.text).join('\n');
|
|
262
266
|
expect(result.isError).toBe(false);
|
|
263
267
|
expect(fullResponse).toContain('💡 Consider using specialized tools instead');
|
|
264
|
-
expect(fullResponse).toContain('
|
|
268
|
+
expect(fullResponse).toContain('check_visibility');
|
|
265
269
|
});
|
|
266
270
|
test('should suggest get_computed_styles for getComputedStyle usage', async () => {
|
|
267
271
|
const args = {
|
|
@@ -293,7 +297,7 @@ describe('Browser Interaction Tools', () => {
|
|
|
293
297
|
expect(fullResponse).toContain('💡 Consider using specialized tools instead');
|
|
294
298
|
expect(fullResponse).toContain('get_test_ids');
|
|
295
299
|
});
|
|
296
|
-
test('should suggest
|
|
300
|
+
test('should suggest compare_element_alignment for position comparison', async () => {
|
|
297
301
|
const args = {
|
|
298
302
|
script: 'document.querySelector("#a").getBoundingClientRect().top === document.querySelector("#b").getBoundingClientRect().top'
|
|
299
303
|
};
|
|
@@ -301,7 +305,7 @@ describe('Browser Interaction Tools', () => {
|
|
|
301
305
|
const fullResponse = result.content.map(c => c.text).join('\n');
|
|
302
306
|
expect(result.isError).toBe(false);
|
|
303
307
|
expect(fullResponse).toContain('💡 Consider using specialized tools instead');
|
|
304
|
-
expect(fullResponse).toContain('
|
|
308
|
+
expect(fullResponse).toContain('compare_element_alignment');
|
|
305
309
|
});
|
|
306
310
|
test('should suggest scroll tools for scrolling operations', async () => {
|
|
307
311
|
const args = {
|
|
@@ -33,7 +33,7 @@ import { GetComputedStylesTool } from './inspection/get_computed_styles.js';
|
|
|
33
33
|
// Evaluation
|
|
34
34
|
import { EvaluateTool } from './evaluation/evaluate.js';
|
|
35
35
|
// Console
|
|
36
|
-
import { GetConsoleLogsTool } from './console/get_console_logs.js';
|
|
36
|
+
import { GetConsoleLogsTool, ClearConsoleLogsTool } from './console/get_console_logs.js';
|
|
37
37
|
// Network
|
|
38
38
|
import { ListNetworkRequestsTool } from './network/list_network_requests.js';
|
|
39
39
|
import { GetRequestDetailsTool } from './network/get_request_details.js';
|
|
@@ -75,8 +75,9 @@ export const BROWSER_TOOL_CLASSES = [
|
|
|
75
75
|
GetComputedStylesTool,
|
|
76
76
|
// Evaluation (1)
|
|
77
77
|
EvaluateTool,
|
|
78
|
-
// Console (
|
|
78
|
+
// Console (2)
|
|
79
79
|
GetConsoleLogsTool,
|
|
80
|
+
ClearConsoleLogsTool,
|
|
80
81
|
// Network (2)
|
|
81
82
|
ListNetworkRequestsTool,
|
|
82
83
|
GetRequestDetailsTool,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface PreviewCounts {
|
|
2
|
+
totalLength?: number;
|
|
3
|
+
shownLength?: number;
|
|
4
|
+
totalMatched?: number;
|
|
5
|
+
shownCount?: number;
|
|
6
|
+
truncated?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function makeConfirmPreview(payload: string, options: {
|
|
9
|
+
headerLine?: string;
|
|
10
|
+
previewLines: string[];
|
|
11
|
+
counts?: PreviewCounts;
|
|
12
|
+
extraTips?: string[];
|
|
13
|
+
}): {
|
|
14
|
+
token: string;
|
|
15
|
+
lines: string[];
|
|
16
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { registerPayload } from './confirmStore.js';
|
|
2
|
+
export function makeConfirmPreview(payload, options) {
|
|
3
|
+
const token = registerPayload(payload);
|
|
4
|
+
const lines = [];
|
|
5
|
+
if (options.headerLine) {
|
|
6
|
+
lines.push(options.headerLine);
|
|
7
|
+
}
|
|
8
|
+
const parts = [];
|
|
9
|
+
const c = options.counts || {};
|
|
10
|
+
if (typeof c.totalLength === 'number')
|
|
11
|
+
parts.push(`totalLength=${c.totalLength}`);
|
|
12
|
+
if (typeof c.shownLength === 'number')
|
|
13
|
+
parts.push(`shownLength=${c.shownLength}`);
|
|
14
|
+
if (typeof c.totalMatched === 'number')
|
|
15
|
+
parts.push(`totalMatched=${c.totalMatched}`);
|
|
16
|
+
if (typeof c.shownCount === 'number')
|
|
17
|
+
parts.push(`shownCount=${c.shownCount}`);
|
|
18
|
+
if (typeof c.truncated === 'boolean')
|
|
19
|
+
parts.push(`truncated=${c.truncated}`);
|
|
20
|
+
if (parts.length) {
|
|
21
|
+
lines.push(`counts: ${parts.join(', ')}`);
|
|
22
|
+
}
|
|
23
|
+
if (lines.length)
|
|
24
|
+
lines.push('');
|
|
25
|
+
// Caller provides preview content lines (e.g., label + excerpt or list)
|
|
26
|
+
for (const l of options.previewLines)
|
|
27
|
+
lines.push(l);
|
|
28
|
+
if (options.previewLines.length)
|
|
29
|
+
lines.push('');
|
|
30
|
+
const estTokens = Math.round((typeof c.totalLength === 'number' ? c.totalLength : payload.length) / 3);
|
|
31
|
+
lines.push(`Output is large (~${estTokens} tokens). To fetch full content without resending parameters, call confirm_output({ token: "${token}" }).`);
|
|
32
|
+
if (options.extraTips && options.extraTips.length) {
|
|
33
|
+
for (const tip of options.extraTips)
|
|
34
|
+
lines.push(tip);
|
|
35
|
+
}
|
|
36
|
+
return { token, lines };
|
|
37
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const STORE = new Map();
|
|
2
|
+
function now() {
|
|
3
|
+
return Date.now();
|
|
4
|
+
}
|
|
5
|
+
function purgeExpired() {
|
|
6
|
+
const t = now();
|
|
7
|
+
for (const [k, v] of STORE.entries()) {
|
|
8
|
+
if (v.expiresAt <= t)
|
|
9
|
+
STORE.delete(k);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function genToken(len = 12) {
|
|
13
|
+
return Math.random().toString(36).slice(2, 2 + len);
|
|
14
|
+
}
|
|
15
|
+
export function registerPayload(payload, ttlMs = 120000) {
|
|
16
|
+
purgeExpired();
|
|
17
|
+
const token = genToken();
|
|
18
|
+
const expiresAt = now() + Math.max(1000, ttlMs);
|
|
19
|
+
STORE.set(token, { payload, expiresAt });
|
|
20
|
+
return token;
|
|
21
|
+
}
|
|
22
|
+
export function consumePayload(token) {
|
|
23
|
+
purgeExpired();
|
|
24
|
+
const entry = STORE.get(token);
|
|
25
|
+
if (!entry)
|
|
26
|
+
return { ok: false, error: 'Invalid or expired token' };
|
|
27
|
+
STORE.delete(token);
|
|
28
|
+
if (entry.expiresAt <= now())
|
|
29
|
+
return { ok: false, error: 'Invalid or expired token' };
|
|
30
|
+
return { ok: true, payload: entry.payload };
|
|
31
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ToolHandler, ToolContext, ToolResponse, ToolMetadata, SessionConfig } from './types.js';
|
|
2
|
+
export declare class ConfirmOutputTool implements ToolHandler {
|
|
3
|
+
private server;
|
|
4
|
+
constructor(server: any);
|
|
5
|
+
static getMetadata(sessionConfig?: SessionConfig): ToolMetadata;
|
|
6
|
+
execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createSuccessResponse, createErrorResponse } from './types.js';
|
|
2
|
+
import { consumePayload } from './confirmStore.js';
|
|
3
|
+
export class ConfirmOutputTool {
|
|
4
|
+
constructor(server) {
|
|
5
|
+
this.server = server;
|
|
6
|
+
}
|
|
7
|
+
static getMetadata(sessionConfig) {
|
|
8
|
+
return {
|
|
9
|
+
name: 'confirm_output',
|
|
10
|
+
description: "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.",
|
|
11
|
+
outputs: [
|
|
12
|
+
"Full original payload if token is valid (one-time)",
|
|
13
|
+
"Error: 'Invalid or expired token'",
|
|
14
|
+
],
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
token: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: "One-time token obtained from a tool's preview response",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ['token'],
|
|
24
|
+
},
|
|
25
|
+
priority: 2,
|
|
26
|
+
category: 'Other',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async execute(args, context) {
|
|
30
|
+
const token = typeof args.token === 'string' ? args.token : '';
|
|
31
|
+
if (!token)
|
|
32
|
+
return createErrorResponse('Token is required');
|
|
33
|
+
const res = consumePayload(token);
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const err = res.error;
|
|
36
|
+
return createErrorResponse(err);
|
|
37
|
+
}
|
|
38
|
+
return createSuccessResponse(res.payload);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createErrorResponse } from './types.js';
|
|
2
2
|
import { BrowserToolBase } from '../browser/base.js';
|
|
3
3
|
import { BROWSER_TOOL_CLASSES } from '../browser/register.js';
|
|
4
|
+
import { ConfirmOutputTool } from './confirm_output.js';
|
|
4
5
|
const toolClasses = new Map();
|
|
5
6
|
const toolInstances = new Map();
|
|
6
7
|
const browserToolNames = new Set();
|
|
@@ -46,3 +47,4 @@ export function clearToolInstances() {
|
|
|
46
47
|
toolInstances.clear();
|
|
47
48
|
}
|
|
48
49
|
registerTools(BROWSER_TOOL_CLASSES);
|
|
50
|
+
registerTools([ConfirmOutputTool]);
|