@wordbricks/playwright-mcp 0.1.20 → 0.1.22
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/cli-wrapper.js +15 -14
- package/cli.js +1 -1
- package/config.d.ts +11 -6
- package/index.d.ts +7 -5
- package/index.js +1 -1
- package/lib/browserContextFactory.js +131 -58
- package/lib/browserServerBackend.js +14 -12
- package/lib/config.js +60 -46
- package/lib/context.js +41 -39
- package/lib/extension/cdpRelay.js +67 -61
- package/lib/extension/extensionContextFactory.js +10 -10
- package/lib/frameworkPatterns.js +21 -21
- package/lib/hooks/antiBotDetectionHook.js +59 -52
- package/lib/hooks/core.js +11 -10
- package/lib/hooks/eventConsumer.js +21 -21
- package/lib/hooks/events.js +3 -3
- package/lib/hooks/formatToolCallEvent.js +3 -7
- package/lib/hooks/frameworkStateHook.js +40 -40
- package/lib/hooks/grouping.js +3 -3
- package/lib/hooks/jsonLdDetectionHook.js +44 -37
- package/lib/hooks/networkFilters.js +17 -17
- package/lib/hooks/networkSetup.js +9 -7
- package/lib/hooks/networkTrackingHook.js +21 -21
- package/lib/hooks/pageHeightHook.js +9 -9
- package/lib/hooks/registry.js +15 -16
- package/lib/hooks/requireTabHook.js +3 -3
- package/lib/hooks/schema.js +38 -38
- package/lib/hooks/waitHook.js +7 -7
- package/lib/index.js +12 -10
- package/lib/mcp/inProcessTransport.js +3 -4
- package/lib/mcp/proxyBackend.js +43 -28
- package/lib/mcp/server.js +24 -19
- package/lib/mcp/tool.js +14 -8
- package/lib/mcp/transport.js +60 -53
- package/lib/playwrightTransformer.js +129 -106
- package/lib/program.js +54 -52
- package/lib/response.js +36 -30
- package/lib/sessionLog.js +19 -17
- package/lib/tab.js +41 -39
- package/lib/tools/common.js +19 -19
- package/lib/tools/console.js +11 -11
- package/lib/tools/dialogs.js +18 -15
- package/lib/tools/evaluate.js +26 -17
- package/lib/tools/extractFrameworkState.js +48 -37
- package/lib/tools/files.js +17 -14
- package/lib/tools/form.js +32 -23
- package/lib/tools/getSnapshot.js +14 -15
- package/lib/tools/getVisibleHtml.js +33 -17
- package/lib/tools/install.js +20 -20
- package/lib/tools/keyboard.js +29 -24
- package/lib/tools/mouse.js +29 -31
- package/lib/tools/navigate.js +19 -23
- package/lib/tools/network.js +12 -14
- package/lib/tools/networkDetail.js +58 -49
- package/lib/tools/networkSearch/bodySearch.js +46 -32
- package/lib/tools/networkSearch/grouping.js +15 -6
- package/lib/tools/networkSearch/helpers.js +4 -4
- package/lib/tools/networkSearch/searchHtml.js +25 -16
- package/lib/tools/networkSearch/urlSearch.js +56 -14
- package/lib/tools/networkSearch.js +46 -36
- package/lib/tools/pdf.js +13 -12
- package/lib/tools/repl.js +66 -54
- package/lib/tools/screenshot.js +57 -33
- package/lib/tools/scroll.js +29 -24
- package/lib/tools/snapshot.js +66 -49
- package/lib/tools/tabs.js +22 -19
- package/lib/tools/tool.js +5 -3
- package/lib/tools/utils.js +17 -13
- package/lib/tools/wait.js +24 -19
- package/lib/tools.js +21 -20
- package/lib/utils/adBlockFilter.js +29 -26
- package/lib/utils/codegen.js +20 -16
- package/lib/utils/extensionPath.js +4 -4
- package/lib/utils/fileUtils.js +17 -13
- package/lib/utils/graphql.js +69 -58
- package/lib/utils/guid.js +3 -3
- package/lib/utils/httpServer.js +9 -9
- package/lib/utils/log.js +3 -3
- package/lib/utils/manualPromise.js +7 -7
- package/lib/utils/networkFormat.js +7 -5
- package/lib/utils/package.js +4 -4
- package/lib/utils/sanitizeHtml.js +66 -34
- package/lib/utils/truncate.js +25 -25
- package/lib/utils/withTimeout.js +1 -1
- package/package.json +34 -57
- package/src/index.ts +27 -17
- package/LICENSE +0 -202
package/lib/tools/pdf.js
CHANGED
|
@@ -13,20 +13,23 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import { z } from
|
|
17
|
-
import
|
|
18
|
-
import
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import * as javascript from "../utils/codegen.js";
|
|
18
|
+
import { defineTabTool } from "./tool.js";
|
|
19
19
|
const pdfSchema = z.object({
|
|
20
|
-
filename: z
|
|
20
|
+
filename: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified."),
|
|
21
24
|
});
|
|
22
25
|
const pdf = defineTabTool({
|
|
23
|
-
capability:
|
|
26
|
+
capability: "pdf",
|
|
24
27
|
schema: {
|
|
25
|
-
name:
|
|
26
|
-
title:
|
|
27
|
-
description:
|
|
28
|
+
name: "browser_pdf_save",
|
|
29
|
+
title: "Save as PDF",
|
|
30
|
+
description: "Save page as PDF",
|
|
28
31
|
inputSchema: pdfSchema,
|
|
29
|
-
type:
|
|
32
|
+
type: "readOnly",
|
|
30
33
|
},
|
|
31
34
|
handle: async (tab, params, response) => {
|
|
32
35
|
const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.pdf`);
|
|
@@ -35,6 +38,4 @@ const pdf = defineTabTool({
|
|
|
35
38
|
await tab.page.pdf({ path: fileName });
|
|
36
39
|
},
|
|
37
40
|
});
|
|
38
|
-
export default [
|
|
39
|
-
pdf,
|
|
40
|
-
];
|
|
41
|
+
export default [pdf];
|
package/lib/tools/repl.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import * as javascript from
|
|
8
|
-
import { truncate } from
|
|
9
|
-
import {
|
|
10
|
-
const EVALUATE_TIMEOUT_MS = ms(
|
|
11
|
-
const debugLog = debug(
|
|
1
|
+
import debug from "debug";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import ms from "ms";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { transformScript } from "../playwrightTransformer.js";
|
|
7
|
+
import * as javascript from "../utils/codegen.js";
|
|
8
|
+
import { truncate } from "../utils/truncate.js";
|
|
9
|
+
import { defineTabTool } from "./tool.js";
|
|
10
|
+
const EVALUATE_TIMEOUT_MS = ms("30s");
|
|
11
|
+
const debugLog = debug("pw:mcp:repl");
|
|
12
12
|
const replSchema = z.object({
|
|
13
13
|
script: z
|
|
14
14
|
.string()
|
|
15
|
-
.describe(
|
|
15
|
+
.describe("JavaScript code to execute in the browser console."),
|
|
16
16
|
});
|
|
17
17
|
const contextStates = new WeakMap();
|
|
18
18
|
function getPageState(page) {
|
|
@@ -49,17 +49,17 @@ async function setupConsoleCapture(page) {
|
|
|
49
49
|
window.__consoleOutputs = [];
|
|
50
50
|
// Override console methods to capture output
|
|
51
51
|
const captureConsole = (method) => {
|
|
52
|
-
const handler =
|
|
52
|
+
const handler = (...args) => {
|
|
53
53
|
// Call original method
|
|
54
54
|
window.__originalConsole?.[method](...args);
|
|
55
55
|
// Capture output
|
|
56
56
|
const output = args
|
|
57
|
-
.map(arg => {
|
|
57
|
+
.map((arg) => {
|
|
58
58
|
if (arg === undefined)
|
|
59
|
-
return
|
|
59
|
+
return "undefined";
|
|
60
60
|
if (arg === null)
|
|
61
|
-
return
|
|
62
|
-
if (typeof arg ===
|
|
61
|
+
return "null";
|
|
62
|
+
if (typeof arg === "object") {
|
|
63
63
|
try {
|
|
64
64
|
return JSON.stringify(arg);
|
|
65
65
|
}
|
|
@@ -69,26 +69,30 @@ async function setupConsoleCapture(page) {
|
|
|
69
69
|
}
|
|
70
70
|
return String(arg);
|
|
71
71
|
})
|
|
72
|
-
.join(
|
|
72
|
+
.join(" ");
|
|
73
73
|
window.__consoleOutputs?.push(output);
|
|
74
74
|
};
|
|
75
|
-
Object.defineProperty(console, method, {
|
|
75
|
+
Object.defineProperty(console, method, {
|
|
76
|
+
value: handler,
|
|
77
|
+
configurable: true,
|
|
78
|
+
writable: true,
|
|
79
|
+
});
|
|
76
80
|
};
|
|
77
|
-
captureConsole(
|
|
78
|
-
captureConsole(
|
|
79
|
-
captureConsole(
|
|
80
|
-
captureConsole(
|
|
81
|
+
captureConsole("log");
|
|
82
|
+
captureConsole("error");
|
|
83
|
+
captureConsole("warn");
|
|
84
|
+
captureConsole("info");
|
|
81
85
|
});
|
|
82
86
|
}
|
|
83
87
|
async function injectLibraries(cdpSession) {
|
|
84
88
|
try {
|
|
85
|
-
const nodeModulesPath = join(process.cwd(),
|
|
89
|
+
const nodeModulesPath = join(process.cwd(), "node_modules");
|
|
86
90
|
// Inject utility libraries into the page context
|
|
87
|
-
const json5Code = readFileSync(join(nodeModulesPath,
|
|
88
|
-
const jsonpathCode = readFileSync(join(nodeModulesPath,
|
|
89
|
-
const lodashCode = readFileSync(join(nodeModulesPath,
|
|
91
|
+
const json5Code = readFileSync(join(nodeModulesPath, "json5", "dist", "index.min.js"), "utf8");
|
|
92
|
+
const jsonpathCode = readFileSync(join(nodeModulesPath, "jsonpath-plus", "dist", "index-browser-umd.min.cjs"), "utf8");
|
|
93
|
+
const lodashCode = readFileSync(join(nodeModulesPath, "lodash", "lodash.min.js"), "utf8");
|
|
90
94
|
// Inject libraries using CDP
|
|
91
|
-
await cdpSession.send(
|
|
95
|
+
await cdpSession.send("Runtime.evaluate", {
|
|
92
96
|
expression: `
|
|
93
97
|
(function(){
|
|
94
98
|
try {
|
|
@@ -209,18 +213,18 @@ async function injectLibraries(cdpSession) {
|
|
|
209
213
|
return true;
|
|
210
214
|
}
|
|
211
215
|
catch (error) {
|
|
212
|
-
debugLog(
|
|
216
|
+
debugLog("Failed to inject libraries:", error);
|
|
213
217
|
return false;
|
|
214
218
|
}
|
|
215
219
|
}
|
|
216
220
|
const repl = defineTabTool({
|
|
217
|
-
capability:
|
|
221
|
+
capability: "core",
|
|
218
222
|
schema: {
|
|
219
|
-
name:
|
|
220
|
-
title:
|
|
221
|
-
description:
|
|
223
|
+
name: "browser_repl",
|
|
224
|
+
title: "Browser REPL",
|
|
225
|
+
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
226
|
inputSchema: replSchema,
|
|
223
|
-
type:
|
|
227
|
+
type: "action",
|
|
224
228
|
},
|
|
225
229
|
handle: async (tab, params, response) => {
|
|
226
230
|
const page = tab.page;
|
|
@@ -229,7 +233,7 @@ const repl = defineTabTool({
|
|
|
229
233
|
async function getOrCreateSession() {
|
|
230
234
|
if (!state.cdpSession) {
|
|
231
235
|
state.cdpSession = await page.context().newCDPSession(page);
|
|
232
|
-
await state.cdpSession.send(
|
|
236
|
+
await state.cdpSession.send("Runtime.enable");
|
|
233
237
|
}
|
|
234
238
|
return state.cdpSession;
|
|
235
239
|
}
|
|
@@ -268,7 +272,7 @@ const repl = defineTabTool({
|
|
|
268
272
|
// Execute the script with REPL mode for console-like behavior
|
|
269
273
|
let evaluationResponse;
|
|
270
274
|
try {
|
|
271
|
-
evaluationResponse = await session.send(
|
|
275
|
+
evaluationResponse = await session.send("Runtime.evaluate", {
|
|
272
276
|
expression: transformedScript,
|
|
273
277
|
replMode: true, // Chrome DevTools console behavior (non-standard, tolerated by CDP)
|
|
274
278
|
includeCommandLineAPI: true, // Includes console utilities like $, $$, etc.
|
|
@@ -280,7 +284,7 @@ const repl = defineTabTool({
|
|
|
280
284
|
catch (error) {
|
|
281
285
|
// If REPL mode fails due to wrapped functions, try without REPL mode
|
|
282
286
|
if (hasReturnOutsideFunction || hasTopLevelAwait) {
|
|
283
|
-
evaluationResponse = await session.send(
|
|
287
|
+
evaluationResponse = await session.send("Runtime.evaluate", {
|
|
284
288
|
expression: transformedScript,
|
|
285
289
|
replMode: false, // Disable REPL mode for wrapped functions
|
|
286
290
|
includeCommandLineAPI: true,
|
|
@@ -296,7 +300,7 @@ const repl = defineTabTool({
|
|
|
296
300
|
if (evaluationResponse.exceptionDetails) {
|
|
297
301
|
const details = evaluationResponse.exceptionDetails;
|
|
298
302
|
// Try to get the actual error message from multiple sources
|
|
299
|
-
let errorMessage = details.text ||
|
|
303
|
+
let errorMessage = details.text || "Evaluation failed";
|
|
300
304
|
// If we have exception details, try to extract more info
|
|
301
305
|
if (details.exception) {
|
|
302
306
|
if (details.exception.description)
|
|
@@ -309,11 +313,11 @@ const repl = defineTabTool({
|
|
|
309
313
|
errorMessage += ` (at line ${details.lineNumber}`;
|
|
310
314
|
if (details.columnNumber !== undefined)
|
|
311
315
|
errorMessage += `:${details.columnNumber}`;
|
|
312
|
-
errorMessage +=
|
|
316
|
+
errorMessage += ")";
|
|
313
317
|
}
|
|
314
318
|
// Check if it's a timeout error
|
|
315
|
-
if (errorMessage.includes(
|
|
316
|
-
errorMessage.includes(
|
|
319
|
+
if (errorMessage.includes("Timeout") ||
|
|
320
|
+
errorMessage.includes("timeout"))
|
|
317
321
|
throw new Error(`Script execution timed out after ${EVALUATE_TIMEOUT_MS / 1000} seconds`);
|
|
318
322
|
throw new Error(errorMessage);
|
|
319
323
|
}
|
|
@@ -325,14 +329,14 @@ const repl = defineTabTool({
|
|
|
325
329
|
const formatOutput = () => {
|
|
326
330
|
const MAX_OUTPUT_LENGTH = 10_000;
|
|
327
331
|
const MAX_CONSOLE_OUTPUTS = 100;
|
|
328
|
-
let output =
|
|
332
|
+
let output = "";
|
|
329
333
|
// Add console output if any
|
|
330
334
|
if (consoleOutputs && consoleOutputs.length > 0) {
|
|
331
335
|
// Limit number of console outputs and truncate each one
|
|
332
336
|
const truncatedOutputs = consoleOutputs
|
|
333
337
|
.slice(0, MAX_CONSOLE_OUTPUTS)
|
|
334
|
-
.map(o => String(truncate(o, { maxStringLength: 1000 })));
|
|
335
|
-
output = truncatedOutputs.join(
|
|
338
|
+
.map((o) => String(truncate(o, { maxStringLength: 1000 })));
|
|
339
|
+
output = truncatedOutputs.join("\n");
|
|
336
340
|
if (consoleOutputs.length > MAX_CONSOLE_OUTPUTS)
|
|
337
341
|
output += `\n... (${consoleOutputs.length - MAX_CONSOLE_OUTPUTS} more console outputs omitted)`;
|
|
338
342
|
}
|
|
@@ -344,7 +348,7 @@ const repl = defineTabTool({
|
|
|
344
348
|
let resultString;
|
|
345
349
|
try {
|
|
346
350
|
resultString =
|
|
347
|
-
typeof result ===
|
|
351
|
+
typeof result === "string"
|
|
348
352
|
? result
|
|
349
353
|
: JSON.stringify(result, null, 2);
|
|
350
354
|
}
|
|
@@ -353,29 +357,37 @@ const repl = defineTabTool({
|
|
|
353
357
|
resultString = String(result);
|
|
354
358
|
}
|
|
355
359
|
// Truncate result if too long (first structure-limit, then clamp string)
|
|
356
|
-
const truncatedValue = truncate(result, {
|
|
357
|
-
|
|
360
|
+
const truncatedValue = truncate(result, {
|
|
361
|
+
maxStringLength: 200,
|
|
362
|
+
maxItems: 20,
|
|
363
|
+
});
|
|
364
|
+
let limited = "";
|
|
358
365
|
try {
|
|
359
|
-
limited =
|
|
366
|
+
limited =
|
|
367
|
+
typeof truncatedValue === "string"
|
|
368
|
+
? truncatedValue
|
|
369
|
+
: JSON.stringify(truncatedValue, null, 2);
|
|
360
370
|
}
|
|
361
371
|
catch {
|
|
362
372
|
limited = resultString;
|
|
363
373
|
}
|
|
364
|
-
const truncatedResult = limited.length > 5000
|
|
374
|
+
const truncatedResult = limited.length > 5000
|
|
375
|
+
? limited.substring(0, 5000) + "\n... (truncated)"
|
|
376
|
+
: limited;
|
|
365
377
|
if (output)
|
|
366
|
-
output +=
|
|
378
|
+
output += "\n" + truncatedResult;
|
|
367
379
|
else
|
|
368
380
|
output = truncatedResult;
|
|
369
381
|
}
|
|
370
382
|
else if (!consoleOutputs || consoleOutputs.length === 0) {
|
|
371
383
|
// Only show "undefined" if there's no console output
|
|
372
|
-
output =
|
|
384
|
+
output = "undefined";
|
|
373
385
|
}
|
|
374
386
|
// Final truncation of the entire output
|
|
375
387
|
if (output.length > MAX_OUTPUT_LENGTH) {
|
|
376
388
|
output =
|
|
377
389
|
output.substring(0, MAX_OUTPUT_LENGTH) +
|
|
378
|
-
|
|
390
|
+
"\n... (output truncated)";
|
|
379
391
|
}
|
|
380
392
|
return output;
|
|
381
393
|
};
|
|
@@ -385,13 +397,13 @@ const repl = defineTabTool({
|
|
|
385
397
|
const errorToMessage = (e) => {
|
|
386
398
|
if (e instanceof Error)
|
|
387
399
|
return e.message;
|
|
388
|
-
if (typeof e ===
|
|
400
|
+
if (typeof e === "string")
|
|
389
401
|
return e;
|
|
390
402
|
try {
|
|
391
403
|
return JSON.stringify(e);
|
|
392
404
|
}
|
|
393
405
|
catch {
|
|
394
|
-
return
|
|
406
|
+
return "Unknown error occurred";
|
|
395
407
|
}
|
|
396
408
|
};
|
|
397
409
|
response.addError(errorToMessage(error));
|
package/lib/tools/screenshot.js
CHANGED
|
@@ -13,67 +13,91 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import { z } from
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import { generateLocator } from
|
|
20
|
-
const screenshotSchema = z
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import * as javascript from "../utils/codegen.js";
|
|
18
|
+
import { defineTabTool } from "./tool.js";
|
|
19
|
+
import { generateLocator } from "./utils.js";
|
|
20
|
+
const screenshotSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
type: z
|
|
23
|
+
.enum(["png", "jpeg"])
|
|
24
|
+
.default("png")
|
|
25
|
+
.describe("Image format for the screenshot. Default is png."),
|
|
26
|
+
filename: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified."),
|
|
30
|
+
element: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.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."),
|
|
34
|
+
ref: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.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."),
|
|
38
|
+
fullPage: z
|
|
39
|
+
.boolean()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots."),
|
|
42
|
+
})
|
|
43
|
+
.refine((data) => {
|
|
27
44
|
return !!data.element === !!data.ref;
|
|
28
45
|
}, {
|
|
29
|
-
message:
|
|
30
|
-
path: [
|
|
31
|
-
})
|
|
46
|
+
message: "Both element and ref must be provided or neither.",
|
|
47
|
+
path: ["ref", "element"],
|
|
48
|
+
})
|
|
49
|
+
.refine((data) => {
|
|
32
50
|
return !(data.fullPage && (data.element || data.ref));
|
|
33
51
|
}, {
|
|
34
|
-
message:
|
|
35
|
-
path: [
|
|
52
|
+
message: "fullPage cannot be used with element screenshots.",
|
|
53
|
+
path: ["fullPage"],
|
|
36
54
|
});
|
|
37
55
|
const screenshot = defineTabTool({
|
|
38
|
-
capability:
|
|
56
|
+
capability: "core",
|
|
39
57
|
schema: {
|
|
40
|
-
name:
|
|
41
|
-
title:
|
|
58
|
+
name: "browser_take_screenshot",
|
|
59
|
+
title: "Take a screenshot",
|
|
42
60
|
description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
|
|
43
61
|
inputSchema: screenshotSchema,
|
|
44
|
-
type:
|
|
62
|
+
type: "readOnly",
|
|
45
63
|
},
|
|
46
64
|
handle: async (tab, params, response) => {
|
|
47
|
-
const fileType = params.type ||
|
|
65
|
+
const fileType = params.type || "png";
|
|
48
66
|
const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
|
|
49
67
|
const options = {
|
|
50
68
|
type: fileType,
|
|
51
|
-
quality: fileType ===
|
|
52
|
-
scale:
|
|
69
|
+
quality: fileType === "png" ? undefined : 90,
|
|
70
|
+
scale: "css",
|
|
53
71
|
path: fileName,
|
|
54
|
-
...(params.fullPage !== undefined && { fullPage: params.fullPage })
|
|
72
|
+
...(params.fullPage !== undefined && { fullPage: params.fullPage }),
|
|
55
73
|
};
|
|
56
74
|
const isElementScreenshot = params.element && params.ref;
|
|
57
|
-
const screenshotTarget = isElementScreenshot
|
|
75
|
+
const screenshotTarget = isElementScreenshot
|
|
76
|
+
? params.element
|
|
77
|
+
: params.fullPage
|
|
78
|
+
? "full page"
|
|
79
|
+
: "viewport";
|
|
58
80
|
response.addCode(`// Screenshot ${screenshotTarget} and save it as ${fileName}`);
|
|
59
81
|
// Only get snapshot when element screenshot is needed
|
|
60
|
-
const locator = params.ref
|
|
82
|
+
const locator = params.ref
|
|
83
|
+
? await tab.refLocator({ element: params.element || "", ref: params.ref })
|
|
84
|
+
: null;
|
|
61
85
|
if (locator)
|
|
62
86
|
response.addCode(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`);
|
|
63
87
|
else
|
|
64
88
|
response.addCode(`await page.screenshot(${javascript.formatObject(options)});`);
|
|
65
|
-
const buffer = locator
|
|
89
|
+
const buffer = locator
|
|
90
|
+
? await locator.screenshot(options)
|
|
91
|
+
: await tab.page.screenshot(options);
|
|
66
92
|
response.addResult(`Took the ${screenshotTarget} screenshot and saved it as ${fileName}`);
|
|
67
93
|
// https://github.com/microsoft/playwright-mcp/issues/817
|
|
68
94
|
// Never return large images to LLM, saving them to the file system is enough.
|
|
69
95
|
if (!params.fullPage) {
|
|
70
96
|
response.addImage({
|
|
71
|
-
contentType: fileType ===
|
|
72
|
-
data: buffer
|
|
97
|
+
contentType: fileType === "png" ? "image/png" : "image/jpeg",
|
|
98
|
+
data: buffer,
|
|
73
99
|
});
|
|
74
100
|
}
|
|
75
|
-
}
|
|
101
|
+
},
|
|
76
102
|
});
|
|
77
|
-
export default [
|
|
78
|
-
screenshot,
|
|
79
|
-
];
|
|
103
|
+
export default [screenshot];
|
package/lib/tools/scroll.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { defineTabTool } from
|
|
1
|
+
import ms from "ms";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { defineTabTool } from "./tool.js";
|
|
4
4
|
/**
|
|
5
5
|
* Generate random number between min and max
|
|
6
6
|
*/
|
|
@@ -15,8 +15,10 @@ function generateScrollPattern(totalDelta) {
|
|
|
15
15
|
const abs = Math.abs(totalDelta);
|
|
16
16
|
const direction = totalDelta > 0 ? 1 : -1;
|
|
17
17
|
// Faster profile: fewer steps overall, still eased
|
|
18
|
-
const steps = abs < 250
|
|
19
|
-
|
|
18
|
+
const steps = abs < 250
|
|
19
|
+
? randomBetween(3, 6)
|
|
20
|
+
: abs < 1200
|
|
21
|
+
? randomBetween(4, 10)
|
|
20
22
|
: randomBetween(8, 16);
|
|
21
23
|
const base = abs / steps;
|
|
22
24
|
const easeInOut = (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t); // 0..1
|
|
@@ -28,42 +30,44 @@ function generateScrollPattern(totalDelta) {
|
|
|
28
30
|
// Larger per-event wheel to reduce total time: clamp 10..180px
|
|
29
31
|
const clamped = Math.max(10, Math.min(180, Math.round(scrollAmount)));
|
|
30
32
|
// Short delays: 15–60ms + small size factor
|
|
31
|
-
const delay = randomBetween(ms(
|
|
33
|
+
const delay = randomBetween(ms("15ms"), ms("60ms")) + Math.floor(clamped * 0.08);
|
|
32
34
|
pattern.push({
|
|
33
35
|
delta: clamped * direction,
|
|
34
36
|
delay,
|
|
35
37
|
});
|
|
36
38
|
// Rare micro-pause (short) to keep a bit of human feel
|
|
37
39
|
if (Math.random() < 0.05 && i < steps - 1)
|
|
38
|
-
pattern.push({ delta: 0, delay: randomBetween(ms(
|
|
40
|
+
pattern.push({ delta: 0, delay: randomBetween(ms("30ms"), ms("80ms")) });
|
|
39
41
|
}
|
|
40
42
|
// Smaller, rarer overshoot for long moves only
|
|
41
43
|
if (abs > 600 && Math.random() < 0.1) {
|
|
42
44
|
pattern.push({
|
|
43
45
|
delta: -direction * randomBetween(6, 18),
|
|
44
|
-
delay: randomBetween(ms(
|
|
46
|
+
delay: randomBetween(ms("60ms"), ms("120ms")),
|
|
45
47
|
});
|
|
46
48
|
}
|
|
47
49
|
return pattern;
|
|
48
50
|
}
|
|
49
51
|
const scrollSchema = z.object({
|
|
50
|
-
amount: z
|
|
52
|
+
amount: z
|
|
53
|
+
.number()
|
|
54
|
+
.describe("Vertical scroll amount in pixels (positive scrolls down, negative up)"),
|
|
51
55
|
});
|
|
52
56
|
const scrollWheel = defineTabTool({
|
|
53
|
-
capability:
|
|
57
|
+
capability: "core",
|
|
54
58
|
schema: {
|
|
55
|
-
name:
|
|
56
|
-
title:
|
|
57
|
-
description:
|
|
59
|
+
name: "browser_scroll",
|
|
60
|
+
title: "Scroll page",
|
|
61
|
+
description: "Scroll the page using mouse wheel with human-like behavior",
|
|
58
62
|
inputSchema: scrollSchema,
|
|
59
|
-
type:
|
|
63
|
+
type: "readOnly",
|
|
60
64
|
},
|
|
61
65
|
handle: async (tab, params, response) => {
|
|
62
66
|
const requestedDeltaY = params.amount || 0;
|
|
63
67
|
// Capture initial scroll position
|
|
64
68
|
const initialScrollPosition = await tab.page.evaluate(() => ({
|
|
65
69
|
x: window.scrollX,
|
|
66
|
-
y: window.scrollY
|
|
70
|
+
y: window.scrollY,
|
|
67
71
|
}));
|
|
68
72
|
response.addCode(`// Scroll page by ${requestedDeltaY}px (vertical)`);
|
|
69
73
|
await tab.waitForCompletion(async () => {
|
|
@@ -76,17 +80,20 @@ const scrollWheel = defineTabTool({
|
|
|
76
80
|
response.addCode(`await page.mouse.move(${x}, ${y});`);
|
|
77
81
|
}
|
|
78
82
|
// Add initial "hover" delay (short)
|
|
79
|
-
await tab.page.waitForTimeout(randomBetween(ms(
|
|
83
|
+
await tab.page.waitForTimeout(randomBetween(ms("40ms"), ms("120ms")));
|
|
80
84
|
// Generate and execute human-like scroll pattern
|
|
81
|
-
const scrollPatternY = requestedDeltaY
|
|
85
|
+
const scrollPatternY = requestedDeltaY
|
|
86
|
+
? generateScrollPattern(requestedDeltaY)
|
|
87
|
+
: [];
|
|
82
88
|
const maxSteps = scrollPatternY.length;
|
|
83
89
|
for (let i = 0; i < maxSteps; i++) {
|
|
84
90
|
let deltaY = scrollPatternY[i]?.delta || 0;
|
|
85
91
|
const delay = scrollPatternY[i]?.delay || 0;
|
|
86
92
|
// Clamp vertical delta to avoid scrolling past page edges
|
|
87
|
-
const allowedY = await tab.page.evaluate(dy => {
|
|
93
|
+
const allowedY = await tab.page.evaluate((dy) => {
|
|
88
94
|
const doc = document.scrollingElement || document.documentElement;
|
|
89
|
-
const maxY = Math.max(0, (doc.scrollHeight || document.documentElement.scrollHeight) -
|
|
95
|
+
const maxY = Math.max(0, (doc.scrollHeight || document.documentElement.scrollHeight) -
|
|
96
|
+
window.innerHeight);
|
|
90
97
|
const curY = window.scrollY || doc.scrollTop || 0;
|
|
91
98
|
let a = dy;
|
|
92
99
|
if (curY + dy < 0)
|
|
@@ -108,19 +115,17 @@ const scrollWheel = defineTabTool({
|
|
|
108
115
|
}
|
|
109
116
|
}
|
|
110
117
|
// Short final pause
|
|
111
|
-
const finalDelay = randomBetween(ms(
|
|
118
|
+
const finalDelay = randomBetween(ms("80ms"), ms("200ms"));
|
|
112
119
|
await tab.page.waitForTimeout(finalDelay);
|
|
113
120
|
response.addCode(`await page.waitForTimeout(${finalDelay});`);
|
|
114
121
|
});
|
|
115
122
|
// Capture final scroll position
|
|
116
123
|
const finalScrollPosition = await tab.page.evaluate(() => ({
|
|
117
124
|
x: window.scrollX,
|
|
118
|
-
y: window.scrollY
|
|
125
|
+
y: window.scrollY,
|
|
119
126
|
}));
|
|
120
127
|
// Always show scroll position for scroll tool
|
|
121
128
|
response.addResult(`Scroll position:\nBefore: x=${initialScrollPosition.x}, y=${initialScrollPosition.y}\nAfter: x=${finalScrollPosition.x}, y=${finalScrollPosition.y}`);
|
|
122
129
|
},
|
|
123
130
|
});
|
|
124
|
-
export default [
|
|
125
|
-
scrollWheel,
|
|
126
|
-
];
|
|
131
|
+
export default [scrollWheel];
|