@wordbricks/playwright-mcp 0.1.20 → 0.1.23

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.
Files changed (89) hide show
  1. package/cli-wrapper.js +15 -14
  2. package/cli.js +1 -1
  3. package/config.d.ts +11 -6
  4. package/index.d.ts +7 -5
  5. package/index.js +1 -1
  6. package/package.json +34 -57
  7. package/LICENSE +0 -202
  8. package/lib/browserContextFactory.js +0 -326
  9. package/lib/browserServerBackend.js +0 -84
  10. package/lib/config.js +0 -286
  11. package/lib/context.js +0 -309
  12. package/lib/extension/cdpRelay.js +0 -346
  13. package/lib/extension/extensionContextFactory.js +0 -56
  14. package/lib/frameworkPatterns.js +0 -35
  15. package/lib/hooks/antiBotDetectionHook.js +0 -171
  16. package/lib/hooks/core.js +0 -144
  17. package/lib/hooks/eventConsumer.js +0 -52
  18. package/lib/hooks/events.js +0 -42
  19. package/lib/hooks/formatToolCallEvent.js +0 -16
  20. package/lib/hooks/frameworkStateHook.js +0 -182
  21. package/lib/hooks/grouping.js +0 -72
  22. package/lib/hooks/jsonLdDetectionHook.js +0 -175
  23. package/lib/hooks/networkFilters.js +0 -82
  24. package/lib/hooks/networkSetup.js +0 -59
  25. package/lib/hooks/networkTrackingHook.js +0 -67
  26. package/lib/hooks/pageHeightHook.js +0 -75
  27. package/lib/hooks/registry.js +0 -42
  28. package/lib/hooks/requireTabHook.js +0 -26
  29. package/lib/hooks/schema.js +0 -89
  30. package/lib/hooks/waitHook.js +0 -33
  31. package/lib/index.js +0 -39
  32. package/lib/mcp/inProcessTransport.js +0 -72
  33. package/lib/mcp/proxyBackend.js +0 -115
  34. package/lib/mcp/server.js +0 -86
  35. package/lib/mcp/tool.js +0 -38
  36. package/lib/mcp/transport.js +0 -181
  37. package/lib/playwrightTransformer.js +0 -497
  38. package/lib/program.js +0 -110
  39. package/lib/response.js +0 -186
  40. package/lib/sessionLog.js +0 -121
  41. package/lib/tab.js +0 -249
  42. package/lib/tools/common.js +0 -55
  43. package/lib/tools/console.js +0 -33
  44. package/lib/tools/dialogs.js +0 -47
  45. package/lib/tools/evaluate.js +0 -53
  46. package/lib/tools/extractFrameworkState.js +0 -214
  47. package/lib/tools/files.js +0 -45
  48. package/lib/tools/form.js +0 -57
  49. package/lib/tools/getSnapshot.js +0 -37
  50. package/lib/tools/getVisibleHtml.js +0 -52
  51. package/lib/tools/install.js +0 -51
  52. package/lib/tools/keyboard.js +0 -78
  53. package/lib/tools/mouse.js +0 -99
  54. package/lib/tools/navigate.js +0 -70
  55. package/lib/tools/network.js +0 -123
  56. package/lib/tools/networkDetail.js +0 -229
  57. package/lib/tools/networkSearch/bodySearch.js +0 -147
  58. package/lib/tools/networkSearch/grouping.js +0 -28
  59. package/lib/tools/networkSearch/helpers.js +0 -32
  60. package/lib/tools/networkSearch/searchHtml.js +0 -67
  61. package/lib/tools/networkSearch/types.js +0 -1
  62. package/lib/tools/networkSearch/urlSearch.js +0 -82
  63. package/lib/tools/networkSearch.js +0 -268
  64. package/lib/tools/pdf.js +0 -40
  65. package/lib/tools/repl.js +0 -402
  66. package/lib/tools/screenshot.js +0 -79
  67. package/lib/tools/scroll.js +0 -126
  68. package/lib/tools/snapshot.js +0 -144
  69. package/lib/tools/tabs.js +0 -59
  70. package/lib/tools/tool.js +0 -33
  71. package/lib/tools/utils.js +0 -74
  72. package/lib/tools/wait.js +0 -55
  73. package/lib/tools.js +0 -67
  74. package/lib/utils/adBlockFilter.js +0 -87
  75. package/lib/utils/codegen.js +0 -51
  76. package/lib/utils/extensionPath.js +0 -10
  77. package/lib/utils/fileUtils.js +0 -36
  78. package/lib/utils/graphql.js +0 -258
  79. package/lib/utils/guid.js +0 -22
  80. package/lib/utils/httpServer.js +0 -39
  81. package/lib/utils/log.js +0 -21
  82. package/lib/utils/manualPromise.js +0 -111
  83. package/lib/utils/networkFormat.js +0 -12
  84. package/lib/utils/package.js +0 -20
  85. package/lib/utils/result.js +0 -2
  86. package/lib/utils/sanitizeHtml.js +0 -98
  87. package/lib/utils/truncate.js +0 -103
  88. package/lib/utils/withTimeout.js +0 -7
  89. package/src/index.ts +0 -50
package/lib/tools/repl.js DELETED
@@ -1,402 +0,0 @@
1
- import { readFileSync } from 'fs';
2
- import { join } from 'path';
3
- import { z } from 'zod';
4
- import debug from 'debug';
5
- import ms from 'ms';
6
- import { defineTabTool } from './tool.js';
7
- import * as javascript from '../utils/codegen.js';
8
- import { truncate } from '../utils/truncate.js';
9
- import { transformScript } from '../playwrightTransformer.js';
10
- const EVALUATE_TIMEOUT_MS = ms('30s');
11
- const debugLog = debug('pw:mcp:repl');
12
- const replSchema = z.object({
13
- script: z
14
- .string()
15
- .describe('JavaScript code to execute in the browser console.'),
16
- });
17
- const contextStates = new WeakMap();
18
- function getPageState(page) {
19
- let pageStates = contextStates.get(page.context());
20
- if (!pageStates) {
21
- pageStates = new WeakMap();
22
- contextStates.set(page.context(), pageStates);
23
- }
24
- let state = pageStates.get(page);
25
- if (!state) {
26
- state = {
27
- librariesInjected: false,
28
- consoleListenerSetup: false,
29
- };
30
- pageStates.set(page, state);
31
- }
32
- return state;
33
- }
34
- // Helpers
35
- async function setupConsoleCapture(page) {
36
- await page.evaluate(() => {
37
- // Store original console methods
38
- window.__originalConsole = {
39
- // eslint-disable-next-line no-console
40
- log: console.log,
41
- // eslint-disable-next-line no-console
42
- error: console.error,
43
- // eslint-disable-next-line no-console
44
- warn: console.warn,
45
- // eslint-disable-next-line no-console
46
- info: console.info,
47
- };
48
- // Create array to store console outputs
49
- window.__consoleOutputs = [];
50
- // Override console methods to capture output
51
- const captureConsole = (method) => {
52
- const handler = function (...args) {
53
- // Call original method
54
- window.__originalConsole?.[method](...args);
55
- // Capture output
56
- const output = args
57
- .map(arg => {
58
- if (arg === undefined)
59
- return 'undefined';
60
- if (arg === null)
61
- return 'null';
62
- if (typeof arg === 'object') {
63
- try {
64
- return JSON.stringify(arg);
65
- }
66
- catch {
67
- return String(arg);
68
- }
69
- }
70
- return String(arg);
71
- })
72
- .join(' ');
73
- window.__consoleOutputs?.push(output);
74
- };
75
- Object.defineProperty(console, method, { value: handler, configurable: true, writable: true });
76
- };
77
- captureConsole('log');
78
- captureConsole('error');
79
- captureConsole('warn');
80
- captureConsole('info');
81
- });
82
- }
83
- async function injectLibraries(cdpSession) {
84
- try {
85
- const nodeModulesPath = join(process.cwd(), 'node_modules');
86
- // Inject utility libraries into the page context
87
- const json5Code = readFileSync(join(nodeModulesPath, 'json5', 'dist', 'index.min.js'), 'utf8');
88
- const jsonpathCode = readFileSync(join(nodeModulesPath, 'jsonpath-plus', 'dist', 'index-browser-umd.min.cjs'), 'utf8');
89
- const lodashCode = readFileSync(join(nodeModulesPath, 'lodash', 'lodash.min.js'), 'utf8');
90
- // Inject libraries using CDP
91
- await cdpSession.send('Runtime.evaluate', {
92
- expression: `
93
- (function(){
94
- try {
95
- window.mcp = window.mcp || {};
96
- window.mcp.helpers = window.mcp.helpers || {};
97
-
98
- var prev_ = window._;
99
- var prevJSON5 = window.JSON5;
100
- var prevJSONPath = window.JSONPath;
101
-
102
- // Load libs
103
- ${json5Code}
104
- ${jsonpathCode}
105
- ${lodashCode}
106
-
107
- // Capture injected references
108
- var injectedJSON5 = window.JSON5;
109
- var injectedJSONPathFn = (window.JSONPath && window.JSONPath.JSONPath) ? window.JSONPath.JSONPath : window.JSONPath;
110
- var injectedLodash = window._;
111
-
112
- // Restore previous globals to avoid conflicts
113
- if (typeof prevJSON5 !== 'undefined') window.JSON5 = prevJSON5;
114
- if (typeof prevJSONPath !== 'undefined') window.JSONPath = prevJSONPath;
115
- if (injectedLodash && injectedLodash.noConflict) {
116
- injectedLodash = injectedLodash.noConflict();
117
- } else if (typeof prev_ !== 'undefined') {
118
- window._ = prev_;
119
- }
120
-
121
- // Expose helpers under a simple namespace
122
- window.mcp.JSON5 = injectedJSON5;
123
- window.mcp.JSONPath = injectedJSONPathFn;
124
- window.mcp._ = injectedLodash;
125
-
126
- // GraphQL helpers
127
- if (!window.mcp.GraphQLClient) {
128
- class GraphQLClient {
129
- constructor(endpoint, options = {}) {
130
- this.endpoint = endpoint;
131
- this.options = options || {};
132
- }
133
- async request(document, variables, requestHeaders) {
134
- const method = ((this.options.method || 'POST')).toUpperCase();
135
- const headers = Object.assign(
136
- { 'content-type': 'application/json' },
137
- this.options.headers || {},
138
- requestHeaders || {}
139
- );
140
- const bodyObj = {
141
- query: typeof document === 'string' ? document : String(document),
142
- variables: variables || undefined,
143
- };
144
- let url = this.endpoint;
145
- const fetchOptions = { method, headers, credentials: (this.options.credentials || 'same-origin') };
146
- if (method === 'GET') {
147
- const params = new URLSearchParams();
148
- params.set('query', bodyObj.query);
149
- if (variables) params.set('variables', JSON.stringify(variables));
150
- url += (url.includes('?') ? '&' : '?') + params.toString();
151
- } else {
152
- fetchOptions.body = JSON.stringify(bodyObj);
153
- }
154
- const res = await fetch(url, fetchOptions);
155
- let json;
156
- try {
157
- json = await res.json();
158
- } catch (e) {
159
- throw new Error('Invalid JSON response: ' + (e && e.message ? e.message : String(e)));
160
- }
161
- if (!res.ok) {
162
- const errMsg = (json && json.error) ? json.error : (json && json.errors ? (json.errors.map(e => e.message).join('; ')) : res.statusText);
163
- throw new Error('HTTP ' + res.status + ' ' + errMsg);
164
- }
165
- if (json.errors && (!this.options || this.options.errorPolicy !== 'all')) {
166
- const msg = json.errors.map(e => e.message).join('; ');
167
- throw new Error('GraphQL errors: ' + msg);
168
- }
169
- return (this.options && this.options.errorPolicy === 'all') ? json : json.data;
170
- }
171
- setHeaders(headers) {
172
- this.options = this.options || {};
173
- this.options.headers = Object.assign({}, this.options.headers || {}, headers || {});
174
- }
175
- setEndpoint(endpoint) { this.endpoint = endpoint; }
176
- }
177
- function gql(strings, ...values) {
178
- let out = '';
179
- for (let i = 0; i < strings.length; i++) {
180
- out += strings[i];
181
- if (i < values.length) out += values[i];
182
- }
183
- return out;
184
- }
185
- async function graphqlRequest(urlOrConfig, document, variables, requestHeaders) {
186
- if (typeof urlOrConfig === 'string') {
187
- const client = new GraphQLClient(urlOrConfig);
188
- return client.request(document, variables, requestHeaders);
189
- }
190
- const { url, ...rest } = urlOrConfig || {};
191
- const client = new GraphQLClient(url, rest);
192
- return client.request(document, variables, requestHeaders);
193
- }
194
-
195
- window.mcp.GraphQLClient = GraphQLClient;
196
- window.mcp.gql = gql;
197
- window.mcp.graphqlRequest = graphqlRequest;
198
-
199
- // Expose globals only if not already present
200
- if (!window.GraphQLClient) window.GraphQLClient = GraphQLClient;
201
- if (!window.gql) window.gql = gql;
202
- if (!window.graphqlRequest) window.graphqlRequest = graphqlRequest;
203
- }
204
- } catch (e) {}
205
- })();
206
- `,
207
- replMode: true,
208
- });
209
- return true;
210
- }
211
- catch (error) {
212
- debugLog('Failed to inject libraries:', error);
213
- return false;
214
- }
215
- }
216
- const repl = defineTabTool({
217
- capability: 'core',
218
- schema: {
219
- name: 'browser_repl',
220
- title: 'Browser REPL',
221
- description: 'DevTools-like browser REPL. Per-tab state persists across calls (const/let/functions); supports top-level await. Survives SPA nav; resets on full reload or when switching tabs. Helpers available via window.mcp: JSON5, JSONPath, _, GraphQLClient, gql, graphqlRequest. No need for wrapping in IIFE.',
222
- inputSchema: replSchema,
223
- type: 'action',
224
- },
225
- handle: async (tab, params, response) => {
226
- const page = tab.page;
227
- const state = getPageState(page);
228
- // Get or create CDP session for this page
229
- async function getOrCreateSession() {
230
- if (!state.cdpSession) {
231
- state.cdpSession = await page.context().newCDPSession(page);
232
- await state.cdpSession.send('Runtime.enable');
233
- }
234
- return state.cdpSession;
235
- }
236
- const session = await getOrCreateSession();
237
- // Set up console output capture if not already done
238
- if (!state.consoleListenerSetup) {
239
- await setupConsoleCapture(page);
240
- state.consoleListenerSetup = true;
241
- }
242
- // Transform Playwright selectors to DOM-compatible code
243
- let transformedScript = transformScript(params.script);
244
- // Check if script has return statements outside functions
245
- const hasReturnOutsideFunction = /^[^{]*\breturn\b/.test(transformedScript.trim());
246
- const hasTopLevelAwait = /^[^{]*\bawait\b/.test(transformedScript.trim());
247
- // Wrap script appropriately if it has return statements
248
- if (hasReturnOutsideFunction && hasTopLevelAwait) {
249
- // Wrap in async function for both return and await
250
- transformedScript = `(async function() { ${transformedScript} })()`;
251
- }
252
- else if (hasReturnOutsideFunction) {
253
- // Wrap in regular function for return statements
254
- transformedScript = `(function() { ${transformedScript} })()`;
255
- }
256
- // Add code to response - show the original script, not transformed
257
- response.addCode(`// Execute in browser REPL\nawait page.evaluate(${javascript.quote(params.script)});`);
258
- response.setIncludeSnapshot();
259
- await tab.waitForCompletion(async () => {
260
- try {
261
- // Clear console outputs for this evaluation
262
- await page.evaluate(() => {
263
- window.__consoleOutputs = [];
264
- });
265
- // Inject libraries if not already done
266
- if (!state.librariesInjected)
267
- state.librariesInjected = await injectLibraries(session);
268
- // Execute the script with REPL mode for console-like behavior
269
- let evaluationResponse;
270
- try {
271
- evaluationResponse = await session.send('Runtime.evaluate', {
272
- expression: transformedScript,
273
- replMode: true, // Chrome DevTools console behavior (non-standard, tolerated by CDP)
274
- includeCommandLineAPI: true, // Includes console utilities like $, $$, etc.
275
- awaitPromise: true, // Automatically await promises
276
- returnByValue: true, // Return the actual value, not just object reference
277
- timeout: EVALUATE_TIMEOUT_MS,
278
- });
279
- }
280
- catch (error) {
281
- // If REPL mode fails due to wrapped functions, try without REPL mode
282
- if (hasReturnOutsideFunction || hasTopLevelAwait) {
283
- evaluationResponse = await session.send('Runtime.evaluate', {
284
- expression: transformedScript,
285
- replMode: false, // Disable REPL mode for wrapped functions
286
- includeCommandLineAPI: true,
287
- awaitPromise: true,
288
- returnByValue: true,
289
- timeout: EVALUATE_TIMEOUT_MS,
290
- });
291
- }
292
- else {
293
- throw error;
294
- }
295
- }
296
- if (evaluationResponse.exceptionDetails) {
297
- const details = evaluationResponse.exceptionDetails;
298
- // Try to get the actual error message from multiple sources
299
- let errorMessage = details.text || 'Evaluation failed';
300
- // If we have exception details, try to extract more info
301
- if (details.exception) {
302
- if (details.exception.description)
303
- errorMessage = details.exception.description;
304
- else if (details.exception.value !== undefined)
305
- errorMessage = String(details.exception.value);
306
- }
307
- // Add location information if available
308
- if (details.lineNumber !== undefined) {
309
- errorMessage += ` (at line ${details.lineNumber}`;
310
- if (details.columnNumber !== undefined)
311
- errorMessage += `:${details.columnNumber}`;
312
- errorMessage += ')';
313
- }
314
- // Check if it's a timeout error
315
- if (errorMessage.includes('Timeout') ||
316
- errorMessage.includes('timeout'))
317
- throw new Error(`Script execution timed out after ${EVALUATE_TIMEOUT_MS / 1000} seconds`);
318
- throw new Error(errorMessage);
319
- }
320
- // Get console outputs from page context
321
- const consoleOutputs = await page.evaluate(() => {
322
- return window.__consoleOutputs || [];
323
- });
324
- // Format the output
325
- const formatOutput = () => {
326
- const MAX_OUTPUT_LENGTH = 10_000;
327
- const MAX_CONSOLE_OUTPUTS = 100;
328
- let output = '';
329
- // Add console output if any
330
- if (consoleOutputs && consoleOutputs.length > 0) {
331
- // Limit number of console outputs and truncate each one
332
- const truncatedOutputs = consoleOutputs
333
- .slice(0, MAX_CONSOLE_OUTPUTS)
334
- .map(o => String(truncate(o, { maxStringLength: 1000 })));
335
- output = truncatedOutputs.join('\n');
336
- if (consoleOutputs.length > MAX_CONSOLE_OUTPUTS)
337
- output += `\n... (${consoleOutputs.length - MAX_CONSOLE_OUTPUTS} more console outputs omitted)`;
338
- }
339
- // Add evaluation result only if:
340
- // 1. It's not undefined, OR
341
- // 2. There's no console output (so we need to show something)
342
- const result = evaluationResponse.result.value;
343
- if (result !== undefined) {
344
- let resultString;
345
- try {
346
- resultString =
347
- typeof result === 'string'
348
- ? result
349
- : JSON.stringify(result, null, 2);
350
- }
351
- catch {
352
- // Handle circular references or other stringify errors
353
- resultString = String(result);
354
- }
355
- // Truncate result if too long (first structure-limit, then clamp string)
356
- const truncatedValue = truncate(result, { maxStringLength: 200, maxItems: 20 });
357
- let limited = '';
358
- try {
359
- limited = typeof truncatedValue === 'string' ? truncatedValue : JSON.stringify(truncatedValue, null, 2);
360
- }
361
- catch {
362
- limited = resultString;
363
- }
364
- const truncatedResult = limited.length > 5000 ? (limited.substring(0, 5000) + '\n... (truncated)') : limited;
365
- if (output)
366
- output += '\n' + truncatedResult;
367
- else
368
- output = truncatedResult;
369
- }
370
- else if (!consoleOutputs || consoleOutputs.length === 0) {
371
- // Only show "undefined" if there's no console output
372
- output = 'undefined';
373
- }
374
- // Final truncation of the entire output
375
- if (output.length > MAX_OUTPUT_LENGTH) {
376
- output =
377
- output.substring(0, MAX_OUTPUT_LENGTH) +
378
- '\n... (output truncated)';
379
- }
380
- return output;
381
- };
382
- response.addResult(formatOutput());
383
- }
384
- catch (error) {
385
- const errorToMessage = (e) => {
386
- if (e instanceof Error)
387
- return e.message;
388
- if (typeof e === 'string')
389
- return e;
390
- try {
391
- return JSON.stringify(e);
392
- }
393
- catch {
394
- return 'Unknown error occurred';
395
- }
396
- };
397
- response.addError(errorToMessage(error));
398
- }
399
- });
400
- },
401
- });
402
- export default [repl];
@@ -1,79 +0,0 @@
1
- /**
2
- * Copyright (c) Microsoft Corporation.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- import { z } from 'zod';
17
- import { defineTabTool } from './tool.js';
18
- import * as javascript from '../utils/codegen.js';
19
- import { generateLocator } from './utils.js';
20
- const screenshotSchema = z.object({
21
- type: z.enum(['png', 'jpeg']).default('png').describe('Image format for the screenshot. Default is png.'),
22
- filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'),
23
- element: z.string().optional().describe('Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.'),
24
- ref: z.string().optional().describe('Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.'),
25
- fullPage: z.boolean().optional().describe('When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.'),
26
- }).refine(data => {
27
- return !!data.element === !!data.ref;
28
- }, {
29
- message: 'Both element and ref must be provided or neither.',
30
- path: ['ref', 'element']
31
- }).refine(data => {
32
- return !(data.fullPage && (data.element || data.ref));
33
- }, {
34
- message: 'fullPage cannot be used with element screenshots.',
35
- path: ['fullPage']
36
- });
37
- const screenshot = defineTabTool({
38
- capability: 'core',
39
- schema: {
40
- name: 'browser_take_screenshot',
41
- title: 'Take a screenshot',
42
- description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
43
- inputSchema: screenshotSchema,
44
- type: 'readOnly',
45
- },
46
- handle: async (tab, params, response) => {
47
- const fileType = params.type || 'png';
48
- const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
49
- const options = {
50
- type: fileType,
51
- quality: fileType === 'png' ? undefined : 90,
52
- scale: 'css',
53
- path: fileName,
54
- ...(params.fullPage !== undefined && { fullPage: params.fullPage })
55
- };
56
- const isElementScreenshot = params.element && params.ref;
57
- const screenshotTarget = isElementScreenshot ? params.element : (params.fullPage ? 'full page' : 'viewport');
58
- response.addCode(`// Screenshot ${screenshotTarget} and save it as ${fileName}`);
59
- // Only get snapshot when element screenshot is needed
60
- const locator = params.ref ? await tab.refLocator({ element: params.element || '', ref: params.ref }) : null;
61
- if (locator)
62
- response.addCode(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`);
63
- else
64
- response.addCode(`await page.screenshot(${javascript.formatObject(options)});`);
65
- const buffer = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
66
- response.addResult(`Took the ${screenshotTarget} screenshot and saved it as ${fileName}`);
67
- // https://github.com/microsoft/playwright-mcp/issues/817
68
- // Never return large images to LLM, saving them to the file system is enough.
69
- if (!params.fullPage) {
70
- response.addImage({
71
- contentType: fileType === 'png' ? 'image/png' : 'image/jpeg',
72
- data: buffer
73
- });
74
- }
75
- }
76
- });
77
- export default [
78
- screenshot,
79
- ];
@@ -1,126 +0,0 @@
1
- import { z } from 'zod';
2
- import ms from 'ms';
3
- import { defineTabTool } from './tool.js';
4
- /**
5
- * Generate random number between min and max
6
- */
7
- function randomBetween(min, max) {
8
- return Math.floor(Math.random() * (max - min + 1)) + min;
9
- }
10
- /**
11
- * Generate human-like scroll pattern
12
- */
13
- function generateScrollPattern(totalDelta) {
14
- const pattern = [];
15
- const abs = Math.abs(totalDelta);
16
- const direction = totalDelta > 0 ? 1 : -1;
17
- // Faster profile: fewer steps overall, still eased
18
- const steps = abs < 250 ? randomBetween(3, 6)
19
- : abs < 1200 ? randomBetween(4, 10)
20
- : randomBetween(8, 16);
21
- const base = abs / steps;
22
- const easeInOut = (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t); // 0..1
23
- for (let i = 0; i < steps; i++) {
24
- const t = steps > 1 ? i / (steps - 1) : 0;
25
- const momentum = 0.6 + easeInOut(t) * 0.7; // ~0.6 .. 1.3
26
- const variation = 0.1 + Math.random() * 0.2; // +/- ~15%
27
- const scrollAmount = base * momentum * (1 + (Math.random() - 0.5) * variation);
28
- // Larger per-event wheel to reduce total time: clamp 10..180px
29
- const clamped = Math.max(10, Math.min(180, Math.round(scrollAmount)));
30
- // Short delays: 15–60ms + small size factor
31
- const delay = randomBetween(ms('15ms'), ms('60ms')) + Math.floor(clamped * 0.08);
32
- pattern.push({
33
- delta: clamped * direction,
34
- delay,
35
- });
36
- // Rare micro-pause (short) to keep a bit of human feel
37
- if (Math.random() < 0.05 && i < steps - 1)
38
- pattern.push({ delta: 0, delay: randomBetween(ms('30ms'), ms('80ms')) });
39
- }
40
- // Smaller, rarer overshoot for long moves only
41
- if (abs > 600 && Math.random() < 0.1) {
42
- pattern.push({
43
- delta: -direction * randomBetween(6, 18),
44
- delay: randomBetween(ms('60ms'), ms('120ms')),
45
- });
46
- }
47
- return pattern;
48
- }
49
- const scrollSchema = z.object({
50
- amount: z.number().describe('Vertical scroll amount in pixels (positive scrolls down, negative up)')
51
- });
52
- const scrollWheel = defineTabTool({
53
- capability: 'core',
54
- schema: {
55
- name: 'browser_scroll',
56
- title: 'Scroll page',
57
- description: 'Scroll the page using mouse wheel with human-like behavior',
58
- inputSchema: scrollSchema,
59
- type: 'readOnly',
60
- },
61
- handle: async (tab, params, response) => {
62
- const requestedDeltaY = params.amount || 0;
63
- // Capture initial scroll position
64
- const initialScrollPosition = await tab.page.evaluate(() => ({
65
- x: window.scrollX,
66
- y: window.scrollY
67
- }));
68
- response.addCode(`// Scroll page by ${requestedDeltaY}px (vertical)`);
69
- await tab.waitForCompletion(async () => {
70
- // Position mouse at a slightly randomized viewport center
71
- const viewport = tab.page.viewportSize();
72
- if (viewport) {
73
- const x = viewport.width / 2 + randomBetween(-100, 100);
74
- const y = viewport.height / 2 + randomBetween(-100, 100);
75
- await tab.page.mouse.move(x, y);
76
- response.addCode(`await page.mouse.move(${x}, ${y});`);
77
- }
78
- // Add initial "hover" delay (short)
79
- await tab.page.waitForTimeout(randomBetween(ms('40ms'), ms('120ms')));
80
- // Generate and execute human-like scroll pattern
81
- const scrollPatternY = requestedDeltaY ? generateScrollPattern(requestedDeltaY) : [];
82
- const maxSteps = scrollPatternY.length;
83
- for (let i = 0; i < maxSteps; i++) {
84
- let deltaY = scrollPatternY[i]?.delta || 0;
85
- const delay = scrollPatternY[i]?.delay || 0;
86
- // Clamp vertical delta to avoid scrolling past page edges
87
- const allowedY = await tab.page.evaluate(dy => {
88
- const doc = document.scrollingElement || document.documentElement;
89
- const maxY = Math.max(0, (doc.scrollHeight || document.documentElement.scrollHeight) - window.innerHeight);
90
- const curY = window.scrollY || doc.scrollTop || 0;
91
- let a = dy;
92
- if (curY + dy < 0)
93
- a = -curY;
94
- if (curY + dy > maxY)
95
- a = maxY - curY;
96
- return a;
97
- }, deltaY);
98
- deltaY = allowedY;
99
- // Skip if nothing to do (already at edge)
100
- if (deltaY === 0)
101
- continue;
102
- await tab.page.mouse.wheel(0, deltaY);
103
- response.addCode(`await page.mouse.wheel(0, ${deltaY});`);
104
- // Wait between scroll events
105
- if (i < maxSteps - 1 && delay > 0) {
106
- await tab.page.waitForTimeout(delay);
107
- response.addCode(`await page.waitForTimeout(${delay});`);
108
- }
109
- }
110
- // Short final pause
111
- const finalDelay = randomBetween(ms('80ms'), ms('200ms'));
112
- await tab.page.waitForTimeout(finalDelay);
113
- response.addCode(`await page.waitForTimeout(${finalDelay});`);
114
- });
115
- // Capture final scroll position
116
- const finalScrollPosition = await tab.page.evaluate(() => ({
117
- x: window.scrollX,
118
- y: window.scrollY
119
- }));
120
- // Always show scroll position for scroll tool
121
- response.addResult(`Scroll position:\nBefore: x=${initialScrollPosition.x}, y=${initialScrollPosition.y}\nAfter: x=${finalScrollPosition.x}, y=${finalScrollPosition.y}`);
122
- },
123
- });
124
- export default [
125
- scrollWheel,
126
- ];