playwright-checkpoint 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +665 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-F5A6XGLJ.js +104 -0
- package/dist/chunk-F5A6XGLJ.js.map +1 -0
- package/dist/chunk-K5DX32TO.js +214 -0
- package/dist/chunk-K5DX32TO.js.map +1 -0
- package/dist/chunk-KG37WSYS.js +1549 -0
- package/dist/chunk-KG37WSYS.js.map +1 -0
- package/dist/chunk-X5IPL32H.js +1484 -0
- package/dist/chunk-X5IPL32H.js.map +1 -0
- package/dist/cli/bin.cjs +3972 -0
- package/dist/cli/bin.cjs.map +1 -0
- package/dist/cli/bin.d.cts +1 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +43 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/index.cjs +1672 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +31 -0
- package/dist/cli/index.d.ts +31 -0
- package/dist/cli/index.js +17 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/mcp-args.cjs +129 -0
- package/dist/cli/mcp-args.cjs.map +1 -0
- package/dist/cli/mcp-args.d.cts +32 -0
- package/dist/cli/mcp-args.d.ts +32 -0
- package/dist/cli/mcp-args.js +10 -0
- package/dist/cli/mcp-args.js.map +1 -0
- package/dist/components.cjs +53 -0
- package/dist/components.cjs.map +1 -0
- package/dist/components.d.cts +27 -0
- package/dist/components.d.ts +27 -0
- package/dist/components.js +26 -0
- package/dist/components.js.map +1 -0
- package/dist/core-CD4jHGgI.d.cts +51 -0
- package/dist/core-CZvnc0rE.d.ts +51 -0
- package/dist/core.cjs +1576 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +3 -0
- package/dist/core.d.ts +3 -0
- package/dist/core.js +32 -0
- package/dist/core.js.map +1 -0
- package/dist/index-BjYQX_hK.d.ts +8 -0
- package/dist/index-Cabk31qi.d.cts +8 -0
- package/dist/index.cjs +3318 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +94 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +285 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.cjs +3467 -0
- package/dist/mcp/index.cjs.map +1 -0
- package/dist/mcp/index.d.cts +26 -0
- package/dist/mcp/index.d.ts +26 -0
- package/dist/mcp/index.js +586 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/teardown.cjs +1509 -0
- package/dist/teardown.cjs.map +1 -0
- package/dist/teardown.d.cts +5 -0
- package/dist/teardown.d.ts +5 -0
- package/dist/teardown.js +52 -0
- package/dist/teardown.js.map +1 -0
- package/dist/types-G7w4n8kR.d.cts +359 -0
- package/dist/types-G7w4n8kR.d.ts +359 -0
- package/package.json +109 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* playwright-checkpoint MCP proxy server.
|
|
3
|
+
*
|
|
4
|
+
* Proxies all tools from an upstream Playwright MCP server while adding
|
|
5
|
+
* the browser_checkpoint, browser_checkpoint_report, and
|
|
6
|
+
* browser_checkpoint_compare tools handled locally.
|
|
7
|
+
*
|
|
8
|
+
* The StdioServerTransport is inlined locally. The SDK is loaded at runtime
|
|
9
|
+
* (not bundled) so this module works even when the SDK is not installed,
|
|
10
|
+
* with a clear error message when startMcpProxy() is actually called.
|
|
11
|
+
*/
|
|
12
|
+
type McpProxyOptions = {
|
|
13
|
+
/** Explicit upstream package to proxy (e.g. "@playwright/mcp"). */
|
|
14
|
+
upstream?: string;
|
|
15
|
+
/** Run in standalone mode — no upstream proxying. */
|
|
16
|
+
standalone?: boolean;
|
|
17
|
+
/** CDP endpoint to connect to the browser directly (overrides auto-detection). */
|
|
18
|
+
cdpEndpoint?: string;
|
|
19
|
+
/** Directory for checkpoint output files (default: ./checkpoints). */
|
|
20
|
+
outputDir?: string;
|
|
21
|
+
/** Arguments to pass through to the upstream server (everything after --). */
|
|
22
|
+
passthrough?: string[];
|
|
23
|
+
};
|
|
24
|
+
declare function startMcpProxy(options?: McpProxyOptions): Promise<void>;
|
|
25
|
+
|
|
26
|
+
export { type McpProxyOptions, startMcpProxy };
|
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import {
|
|
2
|
+
captureCheckpoint,
|
|
3
|
+
sanitizeSegment
|
|
4
|
+
} from "../chunk-KG37WSYS.js";
|
|
5
|
+
import {
|
|
6
|
+
runReporters
|
|
7
|
+
} from "../chunk-X5IPL32H.js";
|
|
8
|
+
import {
|
|
9
|
+
__require
|
|
10
|
+
} from "../chunk-DGUM43GV.js";
|
|
11
|
+
|
|
12
|
+
// src/mcp/transport.ts
|
|
13
|
+
function serializeMessage(message) {
|
|
14
|
+
return JSON.stringify(message) + "\n";
|
|
15
|
+
}
|
|
16
|
+
var ReadBuffer = class {
|
|
17
|
+
_buffer;
|
|
18
|
+
append(chunk) {
|
|
19
|
+
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
|
|
20
|
+
}
|
|
21
|
+
readMessage() {
|
|
22
|
+
if (!this._buffer) return null;
|
|
23
|
+
const index = this._buffer.indexOf("\n");
|
|
24
|
+
if (index === -1) return null;
|
|
25
|
+
const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, "");
|
|
26
|
+
this._buffer = this._buffer.subarray(index + 1);
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(line);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
clear() {
|
|
34
|
+
this._buffer = void 0;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var StdioServerTransport = class {
|
|
38
|
+
_stdin;
|
|
39
|
+
_stdout;
|
|
40
|
+
_readBuffer = new ReadBuffer();
|
|
41
|
+
_started = false;
|
|
42
|
+
constructor(stdin, stdout) {
|
|
43
|
+
this._stdin = stdin ?? process.stdin;
|
|
44
|
+
this._stdout = stdout ?? process.stdout;
|
|
45
|
+
}
|
|
46
|
+
onclose;
|
|
47
|
+
onerror;
|
|
48
|
+
onmessage;
|
|
49
|
+
async start() {
|
|
50
|
+
if (this._started) {
|
|
51
|
+
throw new Error("StdioServerTransport already started!");
|
|
52
|
+
}
|
|
53
|
+
this._started = true;
|
|
54
|
+
this._stdin.on("data", (chunk) => {
|
|
55
|
+
this._readBuffer.append(chunk);
|
|
56
|
+
this.#processReadBuffer();
|
|
57
|
+
});
|
|
58
|
+
this._stdin.on("error", (error) => {
|
|
59
|
+
this.onerror?.(error);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
#processReadBuffer() {
|
|
63
|
+
while (true) {
|
|
64
|
+
try {
|
|
65
|
+
const message = this._readBuffer.readMessage();
|
|
66
|
+
if (message === null) break;
|
|
67
|
+
this.onmessage?.(message);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
this.onerror?.(error instanceof Error ? error : new Error(String(error)));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async close() {
|
|
74
|
+
this._stdin.pause?.();
|
|
75
|
+
this._readBuffer.clear();
|
|
76
|
+
this.onclose?.();
|
|
77
|
+
}
|
|
78
|
+
async send(message) {
|
|
79
|
+
const json = serializeMessage(message);
|
|
80
|
+
if (this._stdout.write(json)) {
|
|
81
|
+
return Promise.resolve();
|
|
82
|
+
}
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
this._stdout.once("drain", resolve);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// src/mcp/upstream.ts
|
|
90
|
+
import { spawn } from "child_process";
|
|
91
|
+
function getNodeModuleCreateRequire() {
|
|
92
|
+
const { createRequire } = __require("module");
|
|
93
|
+
return createRequire(process.cwd() + "/noop.js");
|
|
94
|
+
}
|
|
95
|
+
function resolvePackageJson(pkg) {
|
|
96
|
+
try {
|
|
97
|
+
const req = getNodeModuleCreateRequire();
|
|
98
|
+
return req.resolve(`${pkg}/package.json`);
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function resolveUpstream(options) {
|
|
104
|
+
if (options.upstream) {
|
|
105
|
+
return options.upstream;
|
|
106
|
+
}
|
|
107
|
+
if (resolvePackageJson("@playwright/mcp")) {
|
|
108
|
+
return "@playwright/mcp";
|
|
109
|
+
}
|
|
110
|
+
if (resolvePackageJson("playwright-mcp-advanced")) {
|
|
111
|
+
return "playwright-mcp-advanced";
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
var ChildProcessTransport = class {
|
|
116
|
+
constructor(stdin, stdout, stderr, onMessage, onClose) {
|
|
117
|
+
this.stdin = stdin;
|
|
118
|
+
this.stdout = stdout;
|
|
119
|
+
this.stderr = stderr;
|
|
120
|
+
this.onMessage = onMessage;
|
|
121
|
+
this.onClose = onClose;
|
|
122
|
+
}
|
|
123
|
+
stdin;
|
|
124
|
+
stdout;
|
|
125
|
+
stderr;
|
|
126
|
+
onMessage;
|
|
127
|
+
onClose;
|
|
128
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
129
|
+
id = 0;
|
|
130
|
+
start() {
|
|
131
|
+
let buffer = "";
|
|
132
|
+
this.stdout.on("data", (chunk) => {
|
|
133
|
+
buffer += chunk.toString();
|
|
134
|
+
const lines = buffer.split("\n");
|
|
135
|
+
buffer = lines.pop() ?? "";
|
|
136
|
+
for (const line of lines) {
|
|
137
|
+
if (line.trim()) {
|
|
138
|
+
try {
|
|
139
|
+
const msg = JSON.parse(line);
|
|
140
|
+
this.handleMessage(msg);
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
this.stdout.on("end", () => {
|
|
147
|
+
this.onClose?.();
|
|
148
|
+
});
|
|
149
|
+
this.stderr?.on("data", (chunk) => {
|
|
150
|
+
process.stderr.write(chunk);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
handleMessage(msg) {
|
|
154
|
+
if (msg.id !== void 0) {
|
|
155
|
+
const pending = this.pendingRequests.get(String(msg.id));
|
|
156
|
+
if (pending) {
|
|
157
|
+
this.pendingRequests.delete(String(msg.id));
|
|
158
|
+
if (msg.error) {
|
|
159
|
+
pending.reject(new Error(String(msg.error)));
|
|
160
|
+
} else {
|
|
161
|
+
pending.resolve(msg.result);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (msg.method) {
|
|
167
|
+
this.onMessage(msg);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
send(message) {
|
|
171
|
+
const json = JSON.stringify(message) + "\n";
|
|
172
|
+
this.stdin.write(json);
|
|
173
|
+
}
|
|
174
|
+
request(method, params) {
|
|
175
|
+
const id = String(++this.id);
|
|
176
|
+
const promise = new Promise((resolve, reject) => {
|
|
177
|
+
this.pendingRequests.set(id, { resolve, reject });
|
|
178
|
+
});
|
|
179
|
+
this.send({ method, params, id });
|
|
180
|
+
return promise;
|
|
181
|
+
}
|
|
182
|
+
close() {
|
|
183
|
+
this.stdin.end();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
function injectDebugPort(args) {
|
|
187
|
+
const hasDebugFlag = args.some(
|
|
188
|
+
(a) => a.startsWith("--remote-debugging-port") || a.startsWith("--cdp-endpoint")
|
|
189
|
+
);
|
|
190
|
+
if (hasDebugFlag) {
|
|
191
|
+
return args;
|
|
192
|
+
}
|
|
193
|
+
return ["--remote-debugging-port=9222", ...args];
|
|
194
|
+
}
|
|
195
|
+
function spawnUpstream(pkg, passthroughArgs) {
|
|
196
|
+
const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
197
|
+
const npxArgs = [pkg, ...passthroughArgs];
|
|
198
|
+
const child = spawn(npxBin, npxArgs, {
|
|
199
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
200
|
+
env: {
|
|
201
|
+
...process.env,
|
|
202
|
+
// Prevent npx from prompting or caching in CI
|
|
203
|
+
NPM_CONFIG_YES: "true",
|
|
204
|
+
NPM_CONFIG_INTERACTIVE: "false"
|
|
205
|
+
},
|
|
206
|
+
shell: false,
|
|
207
|
+
windowsHide: true
|
|
208
|
+
});
|
|
209
|
+
const transport = new ChildProcessTransport(
|
|
210
|
+
child.stdin,
|
|
211
|
+
child.stdout,
|
|
212
|
+
child.stderr,
|
|
213
|
+
(_msg) => {
|
|
214
|
+
},
|
|
215
|
+
() => {
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
transport.start();
|
|
219
|
+
const connection = {
|
|
220
|
+
process: child,
|
|
221
|
+
async listTools() {
|
|
222
|
+
const result = await transport.request("tools/list");
|
|
223
|
+
return result;
|
|
224
|
+
},
|
|
225
|
+
async callTool(name, args) {
|
|
226
|
+
const result = await transport.request("tools/call", { name, arguments: args ?? {} });
|
|
227
|
+
return result;
|
|
228
|
+
},
|
|
229
|
+
close() {
|
|
230
|
+
transport.close();
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
return connection;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/mcp/browser-connect.ts
|
|
237
|
+
var CDP_WS_REGEX = /DevTools listening on (ws:\/\/[^\s]+)/;
|
|
238
|
+
var cachedConnection = null;
|
|
239
|
+
function extractCdpUrlFromStderr(data) {
|
|
240
|
+
const match = CDP_WS_REGEX.exec(data);
|
|
241
|
+
return match ? match[1] ?? null : null;
|
|
242
|
+
}
|
|
243
|
+
async function getUpstreamPage(upstreamProcess, options) {
|
|
244
|
+
if (cachedConnection) {
|
|
245
|
+
return cachedConnection;
|
|
246
|
+
}
|
|
247
|
+
let cdpEndpoint = options.cdpEndpoint;
|
|
248
|
+
if (!cdpEndpoint) {
|
|
249
|
+
if (upstreamProcess?.stderr) {
|
|
250
|
+
const stderrLines = [];
|
|
251
|
+
upstreamProcess.stderr.on("data", (chunk) => {
|
|
252
|
+
stderrLines.push(chunk.toString());
|
|
253
|
+
});
|
|
254
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
255
|
+
for (const line of stderrLines) {
|
|
256
|
+
const url = extractCdpUrlFromStderr(line);
|
|
257
|
+
if (url) {
|
|
258
|
+
cdpEndpoint = url;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (!cdpEndpoint) {
|
|
265
|
+
const port = options.debugPort ?? 9222;
|
|
266
|
+
cdpEndpoint = `http://localhost:${port}`;
|
|
267
|
+
}
|
|
268
|
+
const browser = await chromiumConnect(cdpEndpoint);
|
|
269
|
+
const pages = await browser.contexts()[0]?.pages() ?? [];
|
|
270
|
+
const page = pages[0] ?? await browser.newPage();
|
|
271
|
+
cachedConnection = { browser, page };
|
|
272
|
+
return cachedConnection;
|
|
273
|
+
}
|
|
274
|
+
function getNodeModuleCreateRequire2() {
|
|
275
|
+
const { createRequire } = __require("module");
|
|
276
|
+
return createRequire(import.meta.url);
|
|
277
|
+
}
|
|
278
|
+
async function chromiumConnect(endpoint) {
|
|
279
|
+
const req = getNodeModuleCreateRequire2();
|
|
280
|
+
const pw = req("playwright-core");
|
|
281
|
+
return pw.chromium.connectOverCDP(endpoint);
|
|
282
|
+
}
|
|
283
|
+
function resetCachedConnection() {
|
|
284
|
+
cachedConnection = null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/mcp/tools.ts
|
|
288
|
+
var browserCheckpointSchema = {
|
|
289
|
+
type: "object",
|
|
290
|
+
properties: {
|
|
291
|
+
name: { type: "string", description: 'Unique name for this checkpoint (e.g. "homepage", "after-login").' },
|
|
292
|
+
description: {
|
|
293
|
+
type: "string",
|
|
294
|
+
description: "Long-form description of what this checkpoint captures."
|
|
295
|
+
},
|
|
296
|
+
highlightSelector: {
|
|
297
|
+
type: "string",
|
|
298
|
+
description: "CSS selector for a region to highlight in the annotated screenshot."
|
|
299
|
+
},
|
|
300
|
+
fullPage: {
|
|
301
|
+
type: "boolean",
|
|
302
|
+
description: "Capture the full page (default: true). Set false for viewport-only."
|
|
303
|
+
},
|
|
304
|
+
collectors: {
|
|
305
|
+
type: "object",
|
|
306
|
+
description: "Per-collector overrides. Set to false to disable, or an options object to configure.",
|
|
307
|
+
additionalProperties: true
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
required: ["name"]
|
|
311
|
+
};
|
|
312
|
+
var browserCheckpointReportSchema = {
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: {
|
|
315
|
+
outputDir: {
|
|
316
|
+
type: "string",
|
|
317
|
+
description: "Directory to write report files (default: ./report)."
|
|
318
|
+
},
|
|
319
|
+
format: {
|
|
320
|
+
type: "string",
|
|
321
|
+
enum: ["html", "markdown", "mdx"],
|
|
322
|
+
description: "Report format(s) to generate."
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
var browserCheckpointCompareSchema = {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {
|
|
329
|
+
baseline: { type: "string", description: "Path or name of the baseline checkpoint manifest." },
|
|
330
|
+
current: { type: "string", description: "Path or name of the current checkpoint manifest." }
|
|
331
|
+
},
|
|
332
|
+
required: ["baseline", "current"]
|
|
333
|
+
};
|
|
334
|
+
var CHECKPOINT_TOOL_NAME = "browser_checkpoint";
|
|
335
|
+
var REPORT_TOOL_NAME = "browser_checkpoint_report";
|
|
336
|
+
var COMPARE_TOOL_NAME = "browser_checkpoint_compare";
|
|
337
|
+
var CHECKPOINT_TOOLS = [
|
|
338
|
+
{
|
|
339
|
+
name: CHECKPOINT_TOOL_NAME,
|
|
340
|
+
description: "Capture a structured snapshot of the current browser page: screenshot, accessibility violations, Web Vitals, console errors, and network failures. Returns an LLM-friendly summary plus artifact paths.",
|
|
341
|
+
inputSchema: browserCheckpointSchema
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
name: REPORT_TOOL_NAME,
|
|
345
|
+
description: "Generate an HTML, Markdown, or MDX report from all checkpoint manifests in a directory.",
|
|
346
|
+
inputSchema: browserCheckpointReportSchema
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
name: COMPARE_TOOL_NAME,
|
|
350
|
+
description: "Compare two checkpoint runs to surface differences (not yet implemented).",
|
|
351
|
+
inputSchema: browserCheckpointCompareSchema
|
|
352
|
+
}
|
|
353
|
+
];
|
|
354
|
+
async function handleBrowserCheckpoint(args, ctx) {
|
|
355
|
+
const slug = sanitizeSegment(args.name);
|
|
356
|
+
const record = await captureCheckpoint(ctx.page, args.name, {
|
|
357
|
+
outputDir: ctx.outputDir,
|
|
358
|
+
highlightSelector: args.highlightSelector,
|
|
359
|
+
fullPage: args.fullPage ?? true,
|
|
360
|
+
description: args.description,
|
|
361
|
+
collectors: args.collectors
|
|
362
|
+
});
|
|
363
|
+
void slug;
|
|
364
|
+
return formatCheckpointSummary(record);
|
|
365
|
+
}
|
|
366
|
+
async function handleBrowserCheckpointReport(args, testResultsDir = "test-results") {
|
|
367
|
+
const outputDir = args.outputDir ?? "report";
|
|
368
|
+
const config = {
|
|
369
|
+
reporters: {
|
|
370
|
+
html: args.format === void 0 || args.format === "html",
|
|
371
|
+
markdown: args.format === "markdown",
|
|
372
|
+
mdx: args.format === "mdx"
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
const { resolve } = await import("path");
|
|
376
|
+
const results = await runReporters(
|
|
377
|
+
config,
|
|
378
|
+
resolve(process.cwd(), testResultsDir),
|
|
379
|
+
resolve(process.cwd(), outputDir)
|
|
380
|
+
);
|
|
381
|
+
const lines = Object.entries(results).map(([name, result]) => `- ${name}: ${result.summary}`);
|
|
382
|
+
if (lines.length === 0) {
|
|
383
|
+
return "No reports generated.";
|
|
384
|
+
}
|
|
385
|
+
return `Report generation complete:
|
|
386
|
+
${lines.join("\n")}`;
|
|
387
|
+
}
|
|
388
|
+
function handleBrowserCheckpointCompare(_args) {
|
|
389
|
+
return "browser_checkpoint_compare is not yet implemented.";
|
|
390
|
+
}
|
|
391
|
+
function formatCheckpointSummary(record) {
|
|
392
|
+
const lines = [];
|
|
393
|
+
lines.push(`Checkpoint "${record.name}" captured.`);
|
|
394
|
+
lines.push(`URL: ${record.url}`);
|
|
395
|
+
lines.push(`Title: ${record.title}`);
|
|
396
|
+
lines.push("");
|
|
397
|
+
const axeData = record.collectors["axe"];
|
|
398
|
+
if (axeData?.data) {
|
|
399
|
+
const axe = axeData.data;
|
|
400
|
+
if (axe.skipped) {
|
|
401
|
+
lines.push(`Accessibility: skipped (${axe.reason ?? "unknown reason"})`);
|
|
402
|
+
} else {
|
|
403
|
+
const violations = axe.violations ?? 0;
|
|
404
|
+
lines.push(`Accessibility: ${violations} violation${violations !== 1 ? "s" : ""}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const wvData = record.collectors["web-vitals"];
|
|
408
|
+
if (wvData?.data) {
|
|
409
|
+
const wv = wvData.data;
|
|
410
|
+
lines.push("Web Vitals:");
|
|
411
|
+
const metricLines = [];
|
|
412
|
+
for (const [key, metric] of Object.entries(wv)) {
|
|
413
|
+
if (!metric || typeof metric !== "object") continue;
|
|
414
|
+
const m = metric;
|
|
415
|
+
if (m.value === null) continue;
|
|
416
|
+
const ratingIcon = m.rating === "good" ? "\u2705" : m.rating === "needs-improvement" ? "\u26A0\uFE0F" : m.rating === "poor" ? "\u274C" : "";
|
|
417
|
+
const label = key.replace(/Ms$/, "").toUpperCase();
|
|
418
|
+
const formatted = key.endsWith("Ms") ? `${Math.round(m.value)}ms` : m.value.toFixed ? m.value.toFixed(3) : String(m.value);
|
|
419
|
+
metricLines.push(` ${label}: ${formatted} (${m.rating} ${ratingIcon})`.trim());
|
|
420
|
+
}
|
|
421
|
+
if (metricLines.length > 0) {
|
|
422
|
+
lines.push(metricLines.join("\n"));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const consoleData = record.collectors["console"];
|
|
426
|
+
if (consoleData?.data) {
|
|
427
|
+
const entries = consoleData.data;
|
|
428
|
+
const errors = entries.filter((e) => e.type === "error" || e.type === "pageerror");
|
|
429
|
+
if (errors.length > 0) {
|
|
430
|
+
lines.push(`Console: ${errors.length} error${errors.length !== 1 ? "s" : ""}`);
|
|
431
|
+
for (const err of errors.slice(0, 3)) {
|
|
432
|
+
lines.push(` ${err.text}`);
|
|
433
|
+
}
|
|
434
|
+
if (errors.length > 3) {
|
|
435
|
+
lines.push(` ... and ${errors.length - 3} more`);
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
lines.push("Console: no errors");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const netData = record.collectors["network"];
|
|
442
|
+
if (netData?.data) {
|
|
443
|
+
const requests = netData.data;
|
|
444
|
+
const failed = requests.filter((r) => r.status === null || r.status >= 400 || r.failureText);
|
|
445
|
+
if (failed.length > 0) {
|
|
446
|
+
lines.push(`Network: ${failed.length} failed request${failed.length !== 1 ? "s" : ""}`);
|
|
447
|
+
for (const req of failed.slice(0, 3)) {
|
|
448
|
+
const reason = req.failureText ?? `${req.status} ${req.url}`;
|
|
449
|
+
lines.push(` ${req.url} \u2192 ${reason}`);
|
|
450
|
+
}
|
|
451
|
+
if (failed.length > 3) {
|
|
452
|
+
lines.push(` ... and ${failed.length - 3} more`);
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
lines.push("Network: 0 failed requests");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const ssData = record.collectors["screenshot"];
|
|
459
|
+
if (ssData?.summary) {
|
|
460
|
+
const ss = ssData.summary;
|
|
461
|
+
if (ss.screenshotPath) {
|
|
462
|
+
lines.push(`Screenshot: ${ss.screenshotPath}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return lines.join("\n");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/mcp/index.ts
|
|
469
|
+
function ensureMcpSdk() {
|
|
470
|
+
try {
|
|
471
|
+
__require("@modelcontextprotocol/sdk");
|
|
472
|
+
} catch {
|
|
473
|
+
throw new Error(
|
|
474
|
+
"playwright-checkpoint MCP mode requires @modelcontextprotocol/sdk.\nInstall it: npm install @modelcontextprotocol/sdk"
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function getNodeModuleCreateRequire3() {
|
|
479
|
+
const { createRequire } = __require("module");
|
|
480
|
+
return createRequire(import.meta.url);
|
|
481
|
+
}
|
|
482
|
+
async function startMcpProxy(options = {}) {
|
|
483
|
+
ensureMcpSdk();
|
|
484
|
+
const req = getNodeModuleCreateRequire3();
|
|
485
|
+
const sdk = req("@modelcontextprotocol/sdk");
|
|
486
|
+
const sdkServer = req("@modelcontextprotocol/sdk/server");
|
|
487
|
+
const ListToolsRequestSchema = sdk.ListToolsRequestSchema;
|
|
488
|
+
const CallToolRequestSchema = sdk.CallToolRequestSchema;
|
|
489
|
+
const Server = sdkServer.Server;
|
|
490
|
+
const outputDir = options.outputDir ?? "./checkpoints";
|
|
491
|
+
let upstreamConnection = null;
|
|
492
|
+
const upstreamTools = [];
|
|
493
|
+
const upstreamPkg = options.standalone ? null : resolveUpstream({ upstream: options.upstream });
|
|
494
|
+
if (upstreamPkg) {
|
|
495
|
+
console.error(`[playwright-checkpoint MCP] Proxying upstream: ${upstreamPkg}`);
|
|
496
|
+
let passthroughArgs = options.passthrough ?? [];
|
|
497
|
+
if (!options.cdpEndpoint) {
|
|
498
|
+
passthroughArgs = injectDebugPort(passthroughArgs);
|
|
499
|
+
}
|
|
500
|
+
upstreamConnection = spawnUpstream(upstreamPkg, passthroughArgs);
|
|
501
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
502
|
+
try {
|
|
503
|
+
const result = await upstreamConnection.listTools();
|
|
504
|
+
for (const tool of result.tools) {
|
|
505
|
+
upstreamTools.push({
|
|
506
|
+
name: tool.name,
|
|
507
|
+
description: tool.description,
|
|
508
|
+
inputSchema: tool.inputSchema
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
} catch (err) {
|
|
512
|
+
console.error("[playwright-checkpoint MCP] Warning: could not list upstream tools:", err);
|
|
513
|
+
}
|
|
514
|
+
} else {
|
|
515
|
+
console.error("[playwright-checkpoint MCP] Running in standalone mode (no upstream).");
|
|
516
|
+
}
|
|
517
|
+
const checkpointTools = CHECKPOINT_TOOLS.map((t) => ({
|
|
518
|
+
name: t.name,
|
|
519
|
+
description: t.description,
|
|
520
|
+
inputSchema: t.inputSchema
|
|
521
|
+
}));
|
|
522
|
+
const allTools = [...checkpointTools, ...upstreamTools];
|
|
523
|
+
const server = new Server({ name: "playwright-checkpoint", version: "0.1.0-beta.0" }, { capabilities: { tools: {} } });
|
|
524
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
525
|
+
return { tools: allTools };
|
|
526
|
+
});
|
|
527
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
528
|
+
const { name, arguments: args = {} } = request.params;
|
|
529
|
+
if (name === CHECKPOINT_TOOL_NAME) {
|
|
530
|
+
const { page } = await getUpstreamPage(upstreamConnection?.process ?? null, {
|
|
531
|
+
cdpEndpoint: options.cdpEndpoint,
|
|
532
|
+
debugPort: 9222
|
|
533
|
+
});
|
|
534
|
+
const result = await handleBrowserCheckpoint(
|
|
535
|
+
args,
|
|
536
|
+
{ page, outputDir }
|
|
537
|
+
);
|
|
538
|
+
return { content: [{ type: "text", text: result }] };
|
|
539
|
+
}
|
|
540
|
+
if (name === REPORT_TOOL_NAME) {
|
|
541
|
+
const result = await handleBrowserCheckpointReport(
|
|
542
|
+
args
|
|
543
|
+
);
|
|
544
|
+
return { content: [{ type: "text", text: result }] };
|
|
545
|
+
}
|
|
546
|
+
if (name === COMPARE_TOOL_NAME) {
|
|
547
|
+
const result = handleBrowserCheckpointCompare(
|
|
548
|
+
args
|
|
549
|
+
);
|
|
550
|
+
return { content: [{ type: "text", text: result }], isError: true };
|
|
551
|
+
}
|
|
552
|
+
if (upstreamConnection) {
|
|
553
|
+
try {
|
|
554
|
+
const result = await upstreamConnection.callTool(name, args);
|
|
555
|
+
return {
|
|
556
|
+
content: result.content,
|
|
557
|
+
isError: result.isError,
|
|
558
|
+
structuredContent: result.structuredContent
|
|
559
|
+
};
|
|
560
|
+
} catch (err) {
|
|
561
|
+
return {
|
|
562
|
+
content: [
|
|
563
|
+
{
|
|
564
|
+
type: "text",
|
|
565
|
+
text: `Upstream tool "${name}" failed: ${err instanceof Error ? err.message : String(err)}`
|
|
566
|
+
}
|
|
567
|
+
],
|
|
568
|
+
isError: true
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return { content: [{ type: "text", text: `Tool "${name}" is not available.` }], isError: true };
|
|
573
|
+
});
|
|
574
|
+
const transport = new StdioServerTransport();
|
|
575
|
+
await server.connect(transport);
|
|
576
|
+
const cleanup = () => {
|
|
577
|
+
resetCachedConnection();
|
|
578
|
+
upstreamConnection?.close();
|
|
579
|
+
};
|
|
580
|
+
process.on("SIGINT", cleanup);
|
|
581
|
+
process.on("SIGTERM", cleanup);
|
|
582
|
+
}
|
|
583
|
+
export {
|
|
584
|
+
startMcpProxy
|
|
585
|
+
};
|
|
586
|
+
//# sourceMappingURL=index.js.map
|