@mario.andreschak/mcp-browser 3.42.0 → 3.43.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/dist/audioTap.d.ts +23 -0
- package/dist/audioTap.js +147 -0
- package/dist/audioTap.js.map +1 -0
- package/dist/capture.d.ts +94 -0
- package/dist/capture.js +302 -0
- package/dist/capture.js.map +1 -0
- package/dist/gateway.d.ts +28 -0
- package/dist/gateway.js +722 -0
- package/dist/gateway.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/recording.d.ts +21 -0
- package/dist/recording.js +391 -0
- package/dist/recording.js.map +1 -0
- package/dist/resources.d.ts +19 -1
- package/dist/resources.js +258 -37
- package/dist/resources.js.map +1 -1
- package/dist/runtime.d.ts +58 -2
- package/dist/runtime.js +653 -79
- package/dist/runtime.js.map +1 -1
- package/dist/tools.js +579 -33
- package/dist/tools.js.map +1 -1
- package/dist/viewHtml.d.ts +11 -0
- package/dist/viewHtml.js +469 -0
- package/dist/viewHtml.js.map +1 -0
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { BrowserMcpError, assertNavigationAllowed, browserDiagnostics, browserExtensions, closeSession, createCaptureContext, defaultViewport, failureCategoryForCode, getSession, openSession, publicPageState, resetNavigationCounter, runCancellable, timeoutMs, writeScreenshotArtifact, } from './runtime.js';
|
|
2
3
|
import { BROWSER_APP_URI } from './resources.js';
|
|
4
|
+
import { captureDeterministicPng, captureRegionPng, evaluateElementMetrics, navigateCaptureSource, resolveCaptureSource, sha256Hex, writeCaptureArtifact, } from './capture.js';
|
|
5
|
+
import { recordingStatus, startRecording, stopRecording } from './recording.js';
|
|
6
|
+
import { prepareBrowserAudioStream } from './gateway.js';
|
|
3
7
|
const MAX_TEXT_CHARS = 50_000;
|
|
4
8
|
const MAX_SELECTOR_CHARS = 2_000;
|
|
5
9
|
const MAX_SCREENSHOT_BYTES = 5_000_000;
|
|
@@ -23,7 +27,7 @@ const APP_META = {
|
|
|
23
27
|
const SESSION_PROPERTY = {
|
|
24
28
|
type: 'string',
|
|
25
29
|
pattern: '^[A-Za-z0-9_-]{1,64}$',
|
|
26
|
-
description: '
|
|
30
|
+
description: 'Optional browser session identifier. When omitted, the most recently used live session is reused.',
|
|
27
31
|
};
|
|
28
32
|
const TIMEOUT_PROPERTY = {
|
|
29
33
|
type: 'number',
|
|
@@ -35,11 +39,11 @@ export function browserToolDefinitions() {
|
|
|
35
39
|
return [
|
|
36
40
|
{
|
|
37
41
|
name: 'browser_open',
|
|
38
|
-
description: 'Open
|
|
42
|
+
description: 'Open or reuse a browser session in the configured mode: isolated incognito sandbox, or trusted persistent Chrome. Optionally navigates to an allowed HTTP(S) URL.',
|
|
39
43
|
inputSchema: {
|
|
40
44
|
type: 'object',
|
|
41
45
|
properties: {
|
|
42
|
-
sessionId: { ...SESSION_PROPERTY, description: 'Optional stable id
|
|
46
|
+
sessionId: { ...SESSION_PROPERTY, description: 'Optional stable id. Omit it to reuse the most recently used live session, or create one when none exists.' },
|
|
43
47
|
url: { type: 'string', description: 'Optional initial HTTP(S) URL.' },
|
|
44
48
|
timeoutMs: TIMEOUT_PROPERTY,
|
|
45
49
|
},
|
|
@@ -50,11 +54,44 @@ export function browserToolDefinitions() {
|
|
|
50
54
|
},
|
|
51
55
|
{
|
|
52
56
|
name: 'browser_navigate',
|
|
53
|
-
description: 'Navigate an existing
|
|
57
|
+
description: 'Navigate an existing browser session to an allowed HTTP(S) URL. The result distinguishes FLUJO policy blocks from destination-site/WAF blocks.',
|
|
54
58
|
inputSchema: {
|
|
55
59
|
type: 'object',
|
|
56
60
|
properties: { sessionId: SESSION_PROPERTY, url: { type: 'string' }, timeoutMs: TIMEOUT_PROPERTY },
|
|
57
|
-
required: ['
|
|
61
|
+
required: ['url'],
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
},
|
|
64
|
+
annotations: INTERACTION_ANNOTATIONS,
|
|
65
|
+
_meta: APP_META,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'browser_back',
|
|
69
|
+
description: 'Navigate an existing browser session backward in its page history.',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: { sessionId: SESSION_PROPERTY, timeoutMs: TIMEOUT_PROPERTY },
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
},
|
|
75
|
+
annotations: INTERACTION_ANNOTATIONS,
|
|
76
|
+
_meta: APP_META,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'browser_forward',
|
|
80
|
+
description: 'Navigate an existing browser session forward in its page history.',
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: { sessionId: SESSION_PROPERTY, timeoutMs: TIMEOUT_PROPERTY },
|
|
84
|
+
additionalProperties: false,
|
|
85
|
+
},
|
|
86
|
+
annotations: INTERACTION_ANNOTATIONS,
|
|
87
|
+
_meta: APP_META,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'browser_reload',
|
|
91
|
+
description: 'Reload the current page in an existing browser session.',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: { sessionId: SESSION_PROPERTY, timeoutMs: TIMEOUT_PROPERTY },
|
|
58
95
|
additionalProperties: false,
|
|
59
96
|
},
|
|
60
97
|
annotations: INTERACTION_ANNOTATIONS,
|
|
@@ -66,7 +103,6 @@ export function browserToolDefinitions() {
|
|
|
66
103
|
inputSchema: {
|
|
67
104
|
type: 'object',
|
|
68
105
|
properties: { sessionId: SESSION_PROPERTY, timeoutMs: TIMEOUT_PROPERTY },
|
|
69
|
-
required: ['sessionId'],
|
|
70
106
|
additionalProperties: false,
|
|
71
107
|
},
|
|
72
108
|
annotations: READ_ANNOTATIONS,
|
|
@@ -74,15 +110,19 @@ export function browserToolDefinitions() {
|
|
|
74
110
|
},
|
|
75
111
|
{
|
|
76
112
|
name: 'browser_click',
|
|
77
|
-
description: 'Click the first element matching
|
|
113
|
+
description: 'Click either the first element matching a selector or viewport coordinates in an existing browser session.',
|
|
78
114
|
inputSchema: {
|
|
79
115
|
type: 'object',
|
|
80
116
|
properties: {
|
|
81
117
|
sessionId: SESSION_PROPERTY,
|
|
82
118
|
selector: { type: 'string', minLength: 1, maxLength: MAX_SELECTOR_CHARS },
|
|
119
|
+
x: { type: 'number', minimum: 0, description: 'Viewport x coordinate in CSS pixels.' },
|
|
120
|
+
y: { type: 'number', minimum: 0, description: 'Viewport y coordinate in CSS pixels.' },
|
|
121
|
+
button: { type: 'string', enum: ['left', 'right', 'middle'], default: 'left' },
|
|
122
|
+
clickCount: { type: 'integer', minimum: 1, maximum: 3, default: 1 },
|
|
83
123
|
timeoutMs: TIMEOUT_PROPERTY,
|
|
84
124
|
},
|
|
85
|
-
required: ['
|
|
125
|
+
anyOf: [{ required: ['selector'] }, { required: ['x', 'y'] }],
|
|
86
126
|
additionalProperties: false,
|
|
87
127
|
},
|
|
88
128
|
annotations: INTERACTION_ANNOTATIONS,
|
|
@@ -90,7 +130,7 @@ export function browserToolDefinitions() {
|
|
|
90
130
|
},
|
|
91
131
|
{
|
|
92
132
|
name: 'browser_type',
|
|
93
|
-
description: 'Fill
|
|
133
|
+
description: 'Fill an element matching a selector, or type into the currently focused page element; optionally press Enter afterward.',
|
|
94
134
|
inputSchema: {
|
|
95
135
|
type: 'object',
|
|
96
136
|
properties: {
|
|
@@ -100,7 +140,40 @@ export function browserToolDefinitions() {
|
|
|
100
140
|
submit: { type: 'boolean', default: false },
|
|
101
141
|
timeoutMs: TIMEOUT_PROPERTY,
|
|
102
142
|
},
|
|
103
|
-
required: ['
|
|
143
|
+
required: ['text'],
|
|
144
|
+
additionalProperties: false,
|
|
145
|
+
},
|
|
146
|
+
annotations: INTERACTION_ANNOTATIONS,
|
|
147
|
+
_meta: APP_META,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'browser_press',
|
|
151
|
+
description: 'Press a keyboard key or shortcut in the currently focused page element.',
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
sessionId: SESSION_PROPERTY,
|
|
156
|
+
key: { type: 'string', minLength: 1, maxLength: 100, description: 'Patchright key name or shortcut, such as Enter, Tab, ArrowDown, or Control+A.' },
|
|
157
|
+
timeoutMs: TIMEOUT_PROPERTY,
|
|
158
|
+
},
|
|
159
|
+
required: ['key'],
|
|
160
|
+
additionalProperties: false,
|
|
161
|
+
},
|
|
162
|
+
annotations: INTERACTION_ANNOTATIONS,
|
|
163
|
+
_meta: APP_META,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'browser_scroll',
|
|
167
|
+
description: 'Scroll the current page by viewport-relative pixel deltas.',
|
|
168
|
+
inputSchema: {
|
|
169
|
+
type: 'object',
|
|
170
|
+
properties: {
|
|
171
|
+
sessionId: SESSION_PROPERTY,
|
|
172
|
+
deltaX: { type: 'number', minimum: -100000, maximum: 100000, default: 0 },
|
|
173
|
+
deltaY: { type: 'number', minimum: -100000, maximum: 100000, default: 0 },
|
|
174
|
+
timeoutMs: TIMEOUT_PROPERTY,
|
|
175
|
+
},
|
|
176
|
+
anyOf: [{ required: ['deltaX'] }, { required: ['deltaY'] }],
|
|
104
177
|
additionalProperties: false,
|
|
105
178
|
},
|
|
106
179
|
annotations: INTERACTION_ANNOTATIONS,
|
|
@@ -108,7 +181,7 @@ export function browserToolDefinitions() {
|
|
|
108
181
|
},
|
|
109
182
|
{
|
|
110
183
|
name: 'browser_screenshot',
|
|
111
|
-
description: 'Capture a PNG screenshot
|
|
184
|
+
description: 'Capture a PNG screenshot, persist it under the FLUJO data directory, and report its full absolute file path.',
|
|
112
185
|
inputSchema: {
|
|
113
186
|
type: 'object',
|
|
114
187
|
properties: {
|
|
@@ -116,7 +189,143 @@ export function browserToolDefinitions() {
|
|
|
116
189
|
fullPage: { type: 'boolean', default: false },
|
|
117
190
|
timeoutMs: TIMEOUT_PROPERTY,
|
|
118
191
|
},
|
|
119
|
-
|
|
192
|
+
additionalProperties: false,
|
|
193
|
+
},
|
|
194
|
+
annotations: READ_ANNOTATIONS,
|
|
195
|
+
_meta: APP_META,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'browser_capture_page',
|
|
199
|
+
description: 'Capture a deterministic PNG screenshot of a page, inline HTML, or a local file with viewport control, disabled animations, and a fonts-ready wait. Returns the PNG as a run-resource image artifact.',
|
|
200
|
+
inputSchema: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: {
|
|
203
|
+
sessionId: { ...SESSION_PROPERTY, description: 'Optional: capture in an existing session\'s page instead of an ephemeral one.' },
|
|
204
|
+
url: { type: 'string', description: 'HTTP/HTTPS/file:// URL or localhost (file:// and localhost require allowLocal=true + FLUJO_BROWSER_ALLOW_LOCAL_CAPTURE).' },
|
|
205
|
+
html: { type: 'string', maxLength: 500000, description: 'Inline HTML to render instead of loading a URL.' },
|
|
206
|
+
filePath: { type: 'string', description: 'Local file path resolved to file:// (requires allowLocal=true + FLUJO_BROWSER_ALLOW_LOCAL_CAPTURE).' },
|
|
207
|
+
width: { type: 'integer', minimum: 320, maximum: 1920, default: 1920, description: 'Viewport width in CSS pixels.' },
|
|
208
|
+
height: { type: 'integer', minimum: 240, maximum: 1080, default: 1080, description: 'Viewport height in CSS pixels.' },
|
|
209
|
+
deviceScaleFactor: { type: 'number', minimum: 1, maximum: 3, default: 1 },
|
|
210
|
+
fullPage: { type: 'boolean', default: false },
|
|
211
|
+
clipSelector: { type: 'string', minLength: 1, maxLength: MAX_SELECTOR_CHARS, description: 'CSS selector to capture only that element, no browser chrome.' },
|
|
212
|
+
waitFor: { type: 'string', description: 'CSS selector or JS predicate to wait for before capture; the predicate result is never returned.' },
|
|
213
|
+
colorScheme: { type: 'string', enum: ['light', 'dark'], default: 'light' },
|
|
214
|
+
allowLocal: { type: 'boolean', default: false },
|
|
215
|
+
outputPath: { type: 'string', description: 'Optional destination path, confined to the FLUJO data directory.' },
|
|
216
|
+
timeoutMs: TIMEOUT_PROPERTY,
|
|
217
|
+
},
|
|
218
|
+
additionalProperties: false,
|
|
219
|
+
},
|
|
220
|
+
annotations: READ_ANNOTATIONS,
|
|
221
|
+
_meta: APP_META,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'browser_capture_element_metrics',
|
|
225
|
+
description: 'Query per-selector layout metrics (bounding box, computed style, overflow/clipping flags, viewport visibility) without taking a screenshot.',
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
selectors: { type: 'array', items: { type: 'string', minLength: 1, maxLength: MAX_SELECTOR_CHARS }, minItems: 1, maxItems: 50 },
|
|
230
|
+
sessionId: SESSION_PROPERTY,
|
|
231
|
+
url: { type: 'string', description: 'Optional URL to navigate to first (in the given session, or an ephemeral one).' },
|
|
232
|
+
filePath: { type: 'string', description: 'Optional local file to navigate to first (requires allowLocal=true + FLUJO_BROWSER_ALLOW_LOCAL_CAPTURE).' },
|
|
233
|
+
allowLocal: { type: 'boolean', default: false },
|
|
234
|
+
timeoutMs: TIMEOUT_PROPERTY,
|
|
235
|
+
},
|
|
236
|
+
required: ['selectors'],
|
|
237
|
+
additionalProperties: false,
|
|
238
|
+
},
|
|
239
|
+
annotations: READ_ANNOTATIONS,
|
|
240
|
+
_meta: APP_META,
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'browser_capture_region',
|
|
244
|
+
description: 'Capture a specific rectangular pixel region of a page. Cheaper than full-page capture when the exact region is already known.',
|
|
245
|
+
inputSchema: {
|
|
246
|
+
type: 'object',
|
|
247
|
+
properties: {
|
|
248
|
+
sessionId: SESSION_PROPERTY,
|
|
249
|
+
url: { type: 'string', description: 'HTTP/HTTPS/file:// URL or localhost (file:// and localhost require allowLocal=true + FLUJO_BROWSER_ALLOW_LOCAL_CAPTURE).' },
|
|
250
|
+
filePath: { type: 'string', description: 'Local file path resolved to file:// (requires allowLocal=true + FLUJO_BROWSER_ALLOW_LOCAL_CAPTURE).' },
|
|
251
|
+
x: { type: 'integer', minimum: 0, default: 0 },
|
|
252
|
+
y: { type: 'integer', minimum: 0, default: 0 },
|
|
253
|
+
width: { type: 'integer', minimum: 1, maximum: 3840 },
|
|
254
|
+
height: { type: 'integer', minimum: 1, maximum: 2160 },
|
|
255
|
+
allowLocal: { type: 'boolean', default: false },
|
|
256
|
+
outputPath: { type: 'string', description: 'Optional destination path, confined to the FLUJO data directory.' },
|
|
257
|
+
timeoutMs: TIMEOUT_PROPERTY,
|
|
258
|
+
},
|
|
259
|
+
anyOf: [{ required: ['url'] }, { required: ['filePath'] }],
|
|
260
|
+
required: ['width', 'height'],
|
|
261
|
+
additionalProperties: false,
|
|
262
|
+
},
|
|
263
|
+
annotations: READ_ANNOTATIONS,
|
|
264
|
+
_meta: APP_META,
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: 'browser_record_start',
|
|
268
|
+
description: 'Start recording a fresh, dedicated browser session as a WebM video with optional audio (Web Audio + <audio>/<video> tapped via CDP). Drive the returned sessionId with the ordinary browser_* tools, then call browser_record_stop. If durationMs is given, the recording auto-stops and this call returns the finished artifact.',
|
|
269
|
+
inputSchema: {
|
|
270
|
+
type: 'object',
|
|
271
|
+
properties: {
|
|
272
|
+
width: { type: 'integer', minimum: 320, maximum: 3840, description: 'Recording viewport width (default matches FLUJO_BROWSER_VIEWPORT_WIDTH).' },
|
|
273
|
+
height: { type: 'integer', minimum: 240, maximum: 2160, description: 'Recording viewport height (default matches FLUJO_BROWSER_VIEWPORT_HEIGHT).' },
|
|
274
|
+
audio: { type: 'boolean', default: true, description: 'Capture page audio into a WAV sidecar (and mux it in if ffmpeg is available).' },
|
|
275
|
+
durationMs: { type: 'number', minimum: 250, description: 'Auto-stop after this many milliseconds and return the finished artifact (clamped to FLUJO_BROWSER_RECORD_MAX_MS).' },
|
|
276
|
+
outputPath: { type: 'string', description: 'Optional destination path for the finished artifact, confined to the FLUJO data directory.' },
|
|
277
|
+
},
|
|
278
|
+
additionalProperties: false,
|
|
279
|
+
},
|
|
280
|
+
annotations: INTERACTION_ANNOTATIONS,
|
|
281
|
+
_meta: APP_META,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: 'browser_record_stop',
|
|
285
|
+
description: 'Stop a running recording and return its artifact metadata (video path, optional audio path, muxed output if ffmpeg was available).',
|
|
286
|
+
inputSchema: {
|
|
287
|
+
type: 'object',
|
|
288
|
+
properties: {
|
|
289
|
+
recordingId: { ...SESSION_PROPERTY, description: 'Recording id returned by browser_record_start (same as its sessionId). Omit when exactly one recording is running.' },
|
|
290
|
+
sessionId: SESSION_PROPERTY,
|
|
291
|
+
outputPath: { type: 'string', description: 'Optional destination path for the finished artifact, confined to the FLUJO data directory.' },
|
|
292
|
+
},
|
|
293
|
+
additionalProperties: false,
|
|
294
|
+
},
|
|
295
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
296
|
+
_meta: APP_META,
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'browser_record_status',
|
|
300
|
+
description: 'Report whether a recording is running and its elapsed time/captured audio bytes, without taking a screenshot.',
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: 'object',
|
|
303
|
+
properties: {
|
|
304
|
+
recordingId: SESSION_PROPERTY,
|
|
305
|
+
sessionId: SESSION_PROPERTY,
|
|
306
|
+
},
|
|
307
|
+
additionalProperties: false,
|
|
308
|
+
},
|
|
309
|
+
annotations: READ_ANNOTATIONS,
|
|
310
|
+
_meta: APP_META,
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: 'browser_diagnostics',
|
|
314
|
+
description: 'Report configured/actual browser mode, channel, headless state, persistence, locale, service-worker policy, and the active page fingerprint without opening a destination site.',
|
|
315
|
+
inputSchema: {
|
|
316
|
+
type: 'object',
|
|
317
|
+
properties: { sessionId: SESSION_PROPERTY },
|
|
318
|
+
additionalProperties: false,
|
|
319
|
+
},
|
|
320
|
+
annotations: READ_ANNOTATIONS,
|
|
321
|
+
_meta: APP_META,
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: 'browser_extensions',
|
|
325
|
+
description: 'List extensions installed in FLUJO\'s dedicated trusted Chrome profile, explicitly configured unpacked-extension directories, and currently active extension targets. Never reads the personal Chrome profile.',
|
|
326
|
+
inputSchema: {
|
|
327
|
+
type: 'object',
|
|
328
|
+
properties: {},
|
|
120
329
|
additionalProperties: false,
|
|
121
330
|
},
|
|
122
331
|
annotations: READ_ANNOTATIONS,
|
|
@@ -124,11 +333,10 @@ export function browserToolDefinitions() {
|
|
|
124
333
|
},
|
|
125
334
|
{
|
|
126
335
|
name: 'browser_close',
|
|
127
|
-
description: 'Close
|
|
336
|
+
description: 'Close the session tab. Sandbox state is discarded; trusted-mode cookies and profile state remain in the dedicated persistent profile.',
|
|
128
337
|
inputSchema: {
|
|
129
338
|
type: 'object',
|
|
130
339
|
properties: { sessionId: SESSION_PROPERTY },
|
|
131
|
-
required: ['sessionId'],
|
|
132
340
|
additionalProperties: false,
|
|
133
341
|
},
|
|
134
342
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
@@ -142,8 +350,8 @@ function success(data, extraContent = []) {
|
|
|
142
350
|
structuredContent: data,
|
|
143
351
|
};
|
|
144
352
|
}
|
|
145
|
-
function failure(code, message) {
|
|
146
|
-
const data = { success: false, error: { code, message } };
|
|
353
|
+
function failure(code, message, category) {
|
|
354
|
+
const data = { success: false, error: { code, category: category ?? failureCategoryForCode(code), message } };
|
|
147
355
|
return {
|
|
148
356
|
isError: true,
|
|
149
357
|
content: [{ type: 'text', text: JSON.stringify(data) }],
|
|
@@ -179,7 +387,188 @@ function stringArg(args, key, maxLength = 100_000) {
|
|
|
179
387
|
}
|
|
180
388
|
return value;
|
|
181
389
|
}
|
|
182
|
-
|
|
390
|
+
function finiteNumberArg(args, key, fallback) {
|
|
391
|
+
const value = args[key];
|
|
392
|
+
if (value === undefined && fallback !== undefined)
|
|
393
|
+
return fallback;
|
|
394
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
395
|
+
throw new BrowserMcpError('INVALID_ARGUMENT', `${key} must be a finite number.`);
|
|
396
|
+
}
|
|
397
|
+
return value;
|
|
398
|
+
}
|
|
399
|
+
/** Resolve the page a capture tool should operate on: an existing session's page, or a fresh ephemeral context. */
|
|
400
|
+
async function acquireCapturePage(args, signal, viewport) {
|
|
401
|
+
if (typeof args.sessionId === 'string' && args.sessionId.length > 0) {
|
|
402
|
+
const session = getSession(args.sessionId);
|
|
403
|
+
return { page: session.page, close: async () => undefined };
|
|
404
|
+
}
|
|
405
|
+
const { context, page } = await createCaptureContext(signal, viewport);
|
|
406
|
+
return { page, close: () => context.close().catch(() => undefined) };
|
|
407
|
+
}
|
|
408
|
+
async function captureRegionOrPage(args, signal, timeout) {
|
|
409
|
+
const width = finiteNumberArg(args, 'width', 1920);
|
|
410
|
+
const height = finiteNumberArg(args, 'height', 1080);
|
|
411
|
+
if (!Number.isInteger(width) || width < 320 || width > 1920 || !Number.isInteger(height) || height < 240 || height > 1080) {
|
|
412
|
+
throw new BrowserMcpError('INVALID_ARGUMENT', 'width must be an integer 320-1920 and height an integer 240-1080.');
|
|
413
|
+
}
|
|
414
|
+
const deviceScaleFactor = finiteNumberArg(args, 'deviceScaleFactor', 1);
|
|
415
|
+
if (deviceScaleFactor < 1 || deviceScaleFactor > 3) {
|
|
416
|
+
throw new BrowserMcpError('INVALID_ARGUMENT', 'deviceScaleFactor must be between 1 and 3.');
|
|
417
|
+
}
|
|
418
|
+
const colorScheme = args.colorScheme === 'dark' ? 'dark' : 'light';
|
|
419
|
+
const fullPage = args.fullPage === true;
|
|
420
|
+
let clipSelector;
|
|
421
|
+
if (typeof args.clipSelector === 'string' && args.clipSelector.length > 0) {
|
|
422
|
+
if (args.clipSelector.length > MAX_SELECTOR_CHARS) {
|
|
423
|
+
throw new BrowserMcpError('INVALID_ARGUMENT', `clipSelector must be no longer than ${MAX_SELECTOR_CHARS} characters.`);
|
|
424
|
+
}
|
|
425
|
+
clipSelector = args.clipSelector;
|
|
426
|
+
}
|
|
427
|
+
const waitFor = typeof args.waitFor === 'string' && args.waitFor.length > 0 ? args.waitFor : undefined;
|
|
428
|
+
const source = await resolveCaptureSource({
|
|
429
|
+
url: typeof args.url === 'string' ? args.url : undefined,
|
|
430
|
+
html: typeof args.html === 'string' ? args.html : undefined,
|
|
431
|
+
filePath: typeof args.filePath === 'string' ? args.filePath : undefined,
|
|
432
|
+
allowLocal: args.allowLocal === true,
|
|
433
|
+
});
|
|
434
|
+
const { page, close } = await acquireCapturePage(args, signal, { width, height, deviceScaleFactor, colorScheme });
|
|
435
|
+
try {
|
|
436
|
+
const { png, colorType } = await captureDeterministicPng(page, source, { fullPage, clipSelector, waitFor, timeoutMs: timeout });
|
|
437
|
+
const filePath = await writeCaptureArtifact(typeof args.outputPath === 'string' ? args.outputPath : undefined, ['captures', `${randomUUID()}.png`], png);
|
|
438
|
+
return {
|
|
439
|
+
data: {
|
|
440
|
+
success: true,
|
|
441
|
+
path: filePath,
|
|
442
|
+
width,
|
|
443
|
+
height,
|
|
444
|
+
deviceScaleFactor,
|
|
445
|
+
colorType,
|
|
446
|
+
fullPage,
|
|
447
|
+
clipSelector: clipSelector ?? null,
|
|
448
|
+
bytes: png.length,
|
|
449
|
+
sha256: sha256Hex(png),
|
|
450
|
+
mimeType: 'image/png',
|
|
451
|
+
},
|
|
452
|
+
image: { data: png.toString('base64'), mimeType: 'image/png' },
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
finally {
|
|
456
|
+
await close();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async function captureRegionTool(args, signal, timeout) {
|
|
460
|
+
const x = finiteNumberArg(args, 'x', 0);
|
|
461
|
+
const y = finiteNumberArg(args, 'y', 0);
|
|
462
|
+
const width = finiteNumberArg(args, 'width');
|
|
463
|
+
const height = finiteNumberArg(args, 'height');
|
|
464
|
+
if (!Number.isInteger(x) || x < 0 || !Number.isInteger(y) || y < 0) {
|
|
465
|
+
throw new BrowserMcpError('INVALID_ARGUMENT', 'x and y must be non-negative integers.');
|
|
466
|
+
}
|
|
467
|
+
if (!Number.isInteger(width) || width < 1 || width > 3840 || !Number.isInteger(height) || height < 1 || height > 2160) {
|
|
468
|
+
throw new BrowserMcpError('INVALID_ARGUMENT', 'width and height must be positive integers up to 3840x2160.');
|
|
469
|
+
}
|
|
470
|
+
const source = await resolveCaptureSource({
|
|
471
|
+
url: typeof args.url === 'string' ? args.url : undefined,
|
|
472
|
+
filePath: typeof args.filePath === 'string' ? args.filePath : undefined,
|
|
473
|
+
allowLocal: args.allowLocal === true,
|
|
474
|
+
});
|
|
475
|
+
const viewport = defaultViewport();
|
|
476
|
+
const { page, close } = await acquireCapturePage(args, signal, {
|
|
477
|
+
width: Math.min(3840, Math.max(viewport.width, x + width)),
|
|
478
|
+
height: Math.min(2160, Math.max(viewport.height, y + height)),
|
|
479
|
+
});
|
|
480
|
+
try {
|
|
481
|
+
const { png, colorType } = await captureRegionPng(page, source, { x, y, width, height }, timeout);
|
|
482
|
+
const filePath = await writeCaptureArtifact(typeof args.outputPath === 'string' ? args.outputPath : undefined, ['regions', `${randomUUID()}.png`], png);
|
|
483
|
+
return {
|
|
484
|
+
data: {
|
|
485
|
+
success: true,
|
|
486
|
+
path: filePath,
|
|
487
|
+
x,
|
|
488
|
+
y,
|
|
489
|
+
width,
|
|
490
|
+
height,
|
|
491
|
+
colorType,
|
|
492
|
+
bytes: png.length,
|
|
493
|
+
sha256: sha256Hex(png),
|
|
494
|
+
mimeType: 'image/png',
|
|
495
|
+
},
|
|
496
|
+
image: { data: png.toString('base64'), mimeType: 'image/png' },
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
finally {
|
|
500
|
+
await close();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async function captureElementMetricsTool(args, signal, timeout) {
|
|
504
|
+
const rawSelectors = args.selectors;
|
|
505
|
+
if (!Array.isArray(rawSelectors) || rawSelectors.length === 0) {
|
|
506
|
+
throw new BrowserMcpError('INVALID_ARGUMENT', 'Provide a non-empty "selectors" array.');
|
|
507
|
+
}
|
|
508
|
+
const selectors = rawSelectors
|
|
509
|
+
.map((value) => String(value))
|
|
510
|
+
.filter((value) => value.length > 0 && value.length <= MAX_SELECTOR_CHARS);
|
|
511
|
+
if (selectors.length === 0) {
|
|
512
|
+
throw new BrowserMcpError('INVALID_ARGUMENT', `Each selector must be 1-${MAX_SELECTOR_CHARS} characters.`);
|
|
513
|
+
}
|
|
514
|
+
const hasSource = typeof args.url === 'string' || typeof args.filePath === 'string';
|
|
515
|
+
const allowLocal = args.allowLocal === true;
|
|
516
|
+
if (typeof args.sessionId === 'string' && args.sessionId.length > 0) {
|
|
517
|
+
const session = getSession(args.sessionId);
|
|
518
|
+
if (hasSource) {
|
|
519
|
+
const source = await resolveCaptureSource({
|
|
520
|
+
url: typeof args.url === 'string' ? args.url : undefined,
|
|
521
|
+
filePath: typeof args.filePath === 'string' ? args.filePath : undefined,
|
|
522
|
+
allowLocal,
|
|
523
|
+
});
|
|
524
|
+
await navigateCaptureSource(session.page, source, timeout);
|
|
525
|
+
}
|
|
526
|
+
return { success: true, metrics: await evaluateElementMetrics(session.page, selectors) };
|
|
527
|
+
}
|
|
528
|
+
if (hasSource) {
|
|
529
|
+
const source = await resolveCaptureSource({
|
|
530
|
+
url: typeof args.url === 'string' ? args.url : undefined,
|
|
531
|
+
filePath: typeof args.filePath === 'string' ? args.filePath : undefined,
|
|
532
|
+
allowLocal,
|
|
533
|
+
});
|
|
534
|
+
const { context, page } = await createCaptureContext(signal, defaultViewport());
|
|
535
|
+
try {
|
|
536
|
+
await navigateCaptureSource(page, source, timeout);
|
|
537
|
+
return { success: true, metrics: await evaluateElementMetrics(page, selectors) };
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
await context.close().catch(() => undefined);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const session = getSession(undefined);
|
|
544
|
+
return { success: true, metrics: await evaluateElementMetrics(session.page, selectors) };
|
|
545
|
+
}
|
|
546
|
+
function siteBlockClassification(status, title, text, url) {
|
|
547
|
+
const challengeText = `${title}\n${text.slice(0, 4_000)}`;
|
|
548
|
+
const challengePattern = /just a moment|verify (?:that )?you are human|unusual traffic|ungewöhnlichen datenverkehr|tr[aá]fico inusual|trafic inhabituel|attention required|access denied|captcha|security check|request unsuccessful/i;
|
|
549
|
+
const challengeUrlPattern = /\/(?:sorry|captcha)(?:\/|$)|\/challenge(?:\/|$)|\/cdn-cgi\/challenge-platform(?:\/|$)/i;
|
|
550
|
+
if (status !== undefined && [401, 403, 407, 429, 451].includes(status)) {
|
|
551
|
+
return {
|
|
552
|
+
classification: 'site',
|
|
553
|
+
blocked: true,
|
|
554
|
+
status,
|
|
555
|
+
reason: `The destination returned HTTP ${status}; this was not blocked by FLUJO policy.`,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
if (challengeUrlPattern.test(url) || challengePattern.test(challengeText)) {
|
|
559
|
+
return {
|
|
560
|
+
classification: 'site',
|
|
561
|
+
blocked: true,
|
|
562
|
+
...(status !== undefined ? { status } : {}),
|
|
563
|
+
reason: 'The destination rendered an anti-bot, CAPTCHA, or access-denied challenge; this was not blocked by FLUJO policy.',
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
if (status !== undefined) {
|
|
567
|
+
return { classification: 'none', blocked: false, status };
|
|
568
|
+
}
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
async function pageState(session, timeout, response) {
|
|
183
572
|
const [title, bodyText] = await Promise.all([
|
|
184
573
|
session.page.title(),
|
|
185
574
|
session.page.locator('body').innerText({ timeout }).catch(() => ''),
|
|
@@ -187,14 +576,26 @@ async function pageState(session, timeout) {
|
|
|
187
576
|
const text = bodyText.length > MAX_TEXT_CHARS
|
|
188
577
|
? `${bodyText.slice(0, MAX_TEXT_CHARS)}\n…[truncated]`
|
|
189
578
|
: bodyText;
|
|
190
|
-
|
|
579
|
+
const navigation = siteBlockClassification(response?.status(), title, text, session.page.url());
|
|
580
|
+
return {
|
|
581
|
+
success: true,
|
|
582
|
+
...publicPageState(session),
|
|
583
|
+
title,
|
|
584
|
+
text,
|
|
585
|
+
...(navigation ? { navigation } : {}),
|
|
586
|
+
};
|
|
191
587
|
}
|
|
192
588
|
async function navigate(session, rawUrl, timeout, signal) {
|
|
193
589
|
const url = await assertNavigationAllowed(rawUrl);
|
|
590
|
+
// Install the main-world audio hook before page.goto: once a page has created
|
|
591
|
+
// its AudioContext or fired a media play event, it cannot be intercepted
|
|
592
|
+
// retroactively.
|
|
593
|
+
await prepareBrowserAudioStream(session.id);
|
|
194
594
|
resetNavigationCounter(session);
|
|
195
595
|
return runCancellable(session, signal, async () => {
|
|
196
596
|
try {
|
|
197
|
-
await session.page.goto(url.href, { waitUntil: 'domcontentloaded', timeout });
|
|
597
|
+
const response = await session.page.goto(url.href, { waitUntil: 'domcontentloaded', timeout });
|
|
598
|
+
return pageState(session, timeout, response);
|
|
198
599
|
}
|
|
199
600
|
catch (error) {
|
|
200
601
|
if (session.navigationBlocked) {
|
|
@@ -202,7 +603,6 @@ async function navigate(session, rawUrl, timeout, signal) {
|
|
|
202
603
|
}
|
|
203
604
|
throw error;
|
|
204
605
|
}
|
|
205
|
-
return pageState(session, timeout);
|
|
206
606
|
});
|
|
207
607
|
}
|
|
208
608
|
export async function browserCallTool(name, rawArgs, signal) {
|
|
@@ -217,23 +617,126 @@ export async function browserCallTool(name, rawArgs, signal) {
|
|
|
217
617
|
return success(data);
|
|
218
618
|
}
|
|
219
619
|
if (name === 'browser_close') {
|
|
220
|
-
|
|
620
|
+
let sessionId;
|
|
621
|
+
if (args.sessionId === undefined || args.sessionId === '') {
|
|
622
|
+
try {
|
|
623
|
+
sessionId = getSession(undefined).id;
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
if (error instanceof BrowserMcpError && error.code === 'NOT_FOUND') {
|
|
627
|
+
return success({ success: true, sessionId: null, closed: false });
|
|
628
|
+
}
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
sessionId = stringArg(args, 'sessionId', 64);
|
|
634
|
+
}
|
|
221
635
|
const closed = await closeSession(sessionId);
|
|
222
636
|
return success({ success: true, sessionId, closed });
|
|
223
637
|
}
|
|
224
|
-
|
|
638
|
+
if (name === 'browser_diagnostics') {
|
|
639
|
+
let session;
|
|
640
|
+
if (typeof args.sessionId === 'string' && args.sessionId.length > 0) {
|
|
641
|
+
session = getSession(args.sessionId);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
try {
|
|
645
|
+
session = getSession(undefined);
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
if (!(error instanceof BrowserMcpError) || error.code !== 'NOT_FOUND')
|
|
649
|
+
throw error;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return success(await browserDiagnostics(session));
|
|
653
|
+
}
|
|
654
|
+
if (name === 'browser_extensions') {
|
|
655
|
+
return success(await browserExtensions());
|
|
656
|
+
}
|
|
225
657
|
const timeout = timeoutMs(args.timeoutMs);
|
|
658
|
+
if (name === 'browser_capture_page') {
|
|
659
|
+
const result = await captureRegionOrPage(args, signal, timeout);
|
|
660
|
+
return success(result.data, [{ type: 'image', data: result.image.data, mimeType: result.image.mimeType }]);
|
|
661
|
+
}
|
|
662
|
+
if (name === 'browser_capture_region') {
|
|
663
|
+
const result = await captureRegionTool(args, signal, timeout);
|
|
664
|
+
return success(result.data, [{ type: 'image', data: result.image.data, mimeType: result.image.mimeType }]);
|
|
665
|
+
}
|
|
666
|
+
if (name === 'browser_capture_element_metrics') {
|
|
667
|
+
return success(await captureElementMetricsTool(args, signal, timeout));
|
|
668
|
+
}
|
|
669
|
+
if (name === 'browser_record_start') {
|
|
670
|
+
return success(await startRecording({
|
|
671
|
+
width: args.width,
|
|
672
|
+
height: args.height,
|
|
673
|
+
audio: args.audio,
|
|
674
|
+
durationMs: args.durationMs,
|
|
675
|
+
outputPath: args.outputPath,
|
|
676
|
+
}, signal));
|
|
677
|
+
}
|
|
678
|
+
if (name === 'browser_record_stop') {
|
|
679
|
+
return success(await stopRecording({
|
|
680
|
+
recordingId: args.recordingId,
|
|
681
|
+
sessionId: args.sessionId,
|
|
682
|
+
outputPath: args.outputPath,
|
|
683
|
+
}));
|
|
684
|
+
}
|
|
685
|
+
if (name === 'browser_record_status') {
|
|
686
|
+
return success(recordingStatus({ recordingId: args.recordingId, sessionId: args.sessionId }));
|
|
687
|
+
}
|
|
688
|
+
const session = getSession(args.sessionId);
|
|
226
689
|
if (name === 'browser_navigate') {
|
|
227
690
|
return success(await navigate(session, stringArg(args, 'url', 8_192), timeout, signal));
|
|
228
691
|
}
|
|
692
|
+
if (name === 'browser_back' || name === 'browser_forward' || name === 'browser_reload') {
|
|
693
|
+
resetNavigationCounter(session);
|
|
694
|
+
const data = await runCancellable(session, signal, async () => {
|
|
695
|
+
if (name === 'browser_back') {
|
|
696
|
+
await session.page.goBack({ waitUntil: 'domcontentloaded', timeout });
|
|
697
|
+
}
|
|
698
|
+
else if (name === 'browser_forward') {
|
|
699
|
+
await session.page.goForward({ waitUntil: 'domcontentloaded', timeout });
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
await session.page.reload({ waitUntil: 'domcontentloaded', timeout });
|
|
703
|
+
}
|
|
704
|
+
if (session.navigationBlocked) {
|
|
705
|
+
throw new BrowserMcpError('NAVIGATION_BLOCKED', 'The history navigation was blocked by browser policy.');
|
|
706
|
+
}
|
|
707
|
+
return pageState(session, timeout);
|
|
708
|
+
});
|
|
709
|
+
return success(data);
|
|
710
|
+
}
|
|
229
711
|
if (name === 'browser_snapshot') {
|
|
230
712
|
return success(await runCancellable(session, signal, () => pageState(session, timeout)));
|
|
231
713
|
}
|
|
232
714
|
if (name === 'browser_click') {
|
|
233
|
-
const selector = stringArg(args, 'selector', MAX_SELECTOR_CHARS);
|
|
234
715
|
resetNavigationCounter(session);
|
|
235
716
|
const data = await runCancellable(session, signal, async () => {
|
|
236
|
-
|
|
717
|
+
if (args.button !== undefined && !['left', 'right', 'middle'].includes(String(args.button))) {
|
|
718
|
+
throw new BrowserMcpError('INVALID_ARGUMENT', 'button must be left, right, or middle.');
|
|
719
|
+
}
|
|
720
|
+
const button = args.button === 'right' || args.button === 'middle' ? args.button : 'left';
|
|
721
|
+
const clickCount = finiteNumberArg(args, 'clickCount', 1);
|
|
722
|
+
if (!Number.isInteger(clickCount) || clickCount < 1 || clickCount > 3) {
|
|
723
|
+
throw new BrowserMcpError('INVALID_ARGUMENT', 'clickCount must be an integer from 1 to 3.');
|
|
724
|
+
}
|
|
725
|
+
if (typeof args.selector === 'string' && args.selector.length > 0) {
|
|
726
|
+
if (args.selector.length > MAX_SELECTOR_CHARS) {
|
|
727
|
+
throw new BrowserMcpError('INVALID_ARGUMENT', `selector must be no longer than ${MAX_SELECTOR_CHARS} characters.`);
|
|
728
|
+
}
|
|
729
|
+
await session.page.locator(args.selector).first().click({ timeout, button, clickCount });
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
const x = finiteNumberArg(args, 'x');
|
|
733
|
+
const y = finiteNumberArg(args, 'y');
|
|
734
|
+
const viewport = session.page.viewportSize();
|
|
735
|
+
if (x < 0 || y < 0 || (viewport && (x >= viewport.width || y >= viewport.height))) {
|
|
736
|
+
throw new BrowserMcpError('INVALID_ARGUMENT', 'Click coordinates must be inside the current viewport.');
|
|
737
|
+
}
|
|
738
|
+
await session.page.mouse.click(x, y, { button, clickCount });
|
|
739
|
+
}
|
|
237
740
|
await session.page.waitForLoadState('domcontentloaded', { timeout }).catch(() => undefined);
|
|
238
741
|
if (session.navigationBlocked) {
|
|
239
742
|
throw new BrowserMcpError('NAVIGATION_BLOCKED', 'The interaction attempted a navigation blocked by browser policy.');
|
|
@@ -243,17 +746,26 @@ export async function browserCallTool(name, rawArgs, signal) {
|
|
|
243
746
|
return success(data);
|
|
244
747
|
}
|
|
245
748
|
if (name === 'browser_type') {
|
|
246
|
-
const selector = stringArg(args, 'selector', MAX_SELECTOR_CHARS);
|
|
247
749
|
const text = args.text;
|
|
248
750
|
if (typeof text !== 'string' || text.length > 100_000) {
|
|
249
751
|
throw new BrowserMcpError('INVALID_ARGUMENT', 'text must be a string no longer than 100000 characters.');
|
|
250
752
|
}
|
|
251
753
|
resetNavigationCounter(session);
|
|
252
754
|
const data = await runCancellable(session, signal, async () => {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
755
|
+
if (typeof args.selector === 'string' && args.selector.length > 0) {
|
|
756
|
+
if (args.selector.length > MAX_SELECTOR_CHARS) {
|
|
757
|
+
throw new BrowserMcpError('INVALID_ARGUMENT', `selector must be no longer than ${MAX_SELECTOR_CHARS} characters.`);
|
|
758
|
+
}
|
|
759
|
+
const locator = session.page.locator(args.selector).first();
|
|
760
|
+
await locator.fill(text, { timeout });
|
|
761
|
+
if (args.submit === true)
|
|
762
|
+
await locator.press('Enter', { timeout });
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
await session.page.keyboard.insertText(text);
|
|
766
|
+
if (args.submit === true)
|
|
767
|
+
await session.page.keyboard.press('Enter');
|
|
768
|
+
}
|
|
257
769
|
if (session.navigationBlocked) {
|
|
258
770
|
throw new BrowserMcpError('NAVIGATION_BLOCKED', 'The interaction attempted a navigation blocked by browser policy.');
|
|
259
771
|
}
|
|
@@ -261,23 +773,57 @@ export async function browserCallTool(name, rawArgs, signal) {
|
|
|
261
773
|
});
|
|
262
774
|
return success(data);
|
|
263
775
|
}
|
|
776
|
+
if (name === 'browser_press') {
|
|
777
|
+
const key = stringArg(args, 'key', 100);
|
|
778
|
+
resetNavigationCounter(session);
|
|
779
|
+
const data = await runCancellable(session, signal, async () => {
|
|
780
|
+
await session.page.keyboard.press(key);
|
|
781
|
+
await session.page.waitForLoadState('domcontentloaded', { timeout }).catch(() => undefined);
|
|
782
|
+
if (session.navigationBlocked) {
|
|
783
|
+
throw new BrowserMcpError('NAVIGATION_BLOCKED', 'The keyboard interaction attempted a navigation blocked by browser policy.');
|
|
784
|
+
}
|
|
785
|
+
return pageState(session, timeout);
|
|
786
|
+
});
|
|
787
|
+
return success(data);
|
|
788
|
+
}
|
|
789
|
+
if (name === 'browser_scroll') {
|
|
790
|
+
const deltaX = finiteNumberArg(args, 'deltaX', 0);
|
|
791
|
+
const deltaY = finiteNumberArg(args, 'deltaY', 0);
|
|
792
|
+
if (Math.abs(deltaX) > 100_000 || Math.abs(deltaY) > 100_000) {
|
|
793
|
+
throw new BrowserMcpError('INVALID_ARGUMENT', 'Scroll deltas must be between -100000 and 100000.');
|
|
794
|
+
}
|
|
795
|
+
const data = await runCancellable(session, signal, async () => {
|
|
796
|
+
await session.page.mouse.wheel(deltaX, deltaY);
|
|
797
|
+
return pageState(session, timeout);
|
|
798
|
+
});
|
|
799
|
+
return success(data);
|
|
800
|
+
}
|
|
264
801
|
if (name === 'browser_screenshot') {
|
|
802
|
+
const fullPage = args.fullPage === true;
|
|
265
803
|
const png = await runCancellable(session, signal, () => session.page.screenshot({
|
|
266
804
|
type: 'png',
|
|
267
|
-
fullPage
|
|
805
|
+
fullPage,
|
|
268
806
|
timeout,
|
|
269
807
|
}));
|
|
270
808
|
if (png.length > MAX_SCREENSHOT_BYTES) {
|
|
271
809
|
throw new BrowserMcpError('INVALID_ARGUMENT', 'The screenshot exceeded the 5 MB artifact limit.');
|
|
272
810
|
}
|
|
273
|
-
const
|
|
811
|
+
const filePath = await writeScreenshotArtifact(session.id, fullPage, png);
|
|
812
|
+
const data = {
|
|
813
|
+
success: true,
|
|
814
|
+
...publicPageState(session),
|
|
815
|
+
path: filePath,
|
|
816
|
+
mimeType: 'image/png',
|
|
817
|
+
bytes: png.length,
|
|
818
|
+
viewport: session.page.viewportSize(),
|
|
819
|
+
};
|
|
274
820
|
return success(data, [{ type: 'image', data: png.toString('base64'), mimeType: 'image/png' }]);
|
|
275
821
|
}
|
|
276
822
|
return failure('NOT_FOUND', `Unknown browser tool: ${name}`);
|
|
277
823
|
}
|
|
278
824
|
catch (error) {
|
|
279
825
|
const normalized = normalizedError(error);
|
|
280
|
-
return failure(normalized.code, normalized.message);
|
|
826
|
+
return failure(normalized.code, normalized.message, normalized.category);
|
|
281
827
|
}
|
|
282
828
|
}
|
|
283
829
|
//# sourceMappingURL=tools.js.map
|