playwright-e2e-mcp 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -34
- package/dist/http.d.ts +19 -1
- package/dist/http.js +98 -9
- package/dist/http.js.map +1 -1
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +13 -0
- package/dist/server.js +25 -12
- package/dist/server.js.map +1 -1
- package/dist/tools/compare-visual-state.js +11 -8
- package/dist/tools/compare-visual-state.js.map +1 -1
- package/dist/tools/generate-e2e-test.js +46 -10
- package/dist/tools/generate-e2e-test.js.map +1 -1
- package/dist/tools/inspect-page.js +2 -0
- package/dist/tools/inspect-page.js.map +1 -1
- package/dist/tools/list-tests.js +8 -1
- package/dist/tools/list-tests.js.map +1 -1
- package/dist/tools/run-test.js +1 -1
- package/dist/tools/run-test.js.map +1 -1
- package/dist/tools/shared.d.ts +9 -2
- package/dist/tools/shared.js +135 -10
- package/dist/tools/shared.js.map +1 -1
- package/dist/tools/validate-selector.js +2 -0
- package/dist/tools/validate-selector.js.map +1 -1
- package/dist/types/index.d.ts +7 -0
- package/dist/utils/change-analyzer.js +31 -26
- package/dist/utils/change-analyzer.js.map +1 -1
- package/dist/utils/image-diff.js +23 -4
- package/dist/utils/image-diff.js.map +1 -1
- package/dist/utils/path-utils.d.ts +25 -3
- package/dist/utils/path-utils.js +159 -7
- package/dist/utils/path-utils.js.map +1 -1
- package/dist/utils/playwright-runner.d.ts +14 -10
- package/dist/utils/playwright-runner.js +128 -30
- package/dist/utils/playwright-runner.js.map +1 -1
- package/dist/utils/report-parser.js +7 -6
- package/dist/utils/report-parser.js.map +1 -1
- package/dist/utils/trace-reader.js +23 -6
- package/dist/utils/trace-reader.js.map +1 -1
- package/dist/utils/url-policy.d.ts +29 -0
- package/dist/utils/url-policy.js +99 -0
- package/dist/utils/url-policy.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,22 @@ and a concrete "how to fix" hint. When a test fails because a selector no longer
|
|
|
17
17
|
matches, the agent can open the **live page** in a headless browser, see the real DOM
|
|
18
18
|
with unique CSS selectors, and validate the replacement selector before re-running.
|
|
19
19
|
|
|
20
|
+
## Demo
|
|
21
|
+
|
|
22
|
+
**Hosted endpoint** — what an `initialize` + `tools/list` round-trip against
|
|
23
|
+
`https://playwright-e2e-mcp.vercel.app/api/mcp` returns for a client that sends the
|
|
24
|
+
bearer token (without one, only `list-tests` and `get-failure` are listed):
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
**A real test run** — `run-test` served over stdio by `npx -y playwright-e2e-mcp`
|
|
29
|
+
against the bundled `examples/sample-test.spec.ts` (actual output, unedited):
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
Images are rendered with `node scripts/gen-demo-images.mjs`: the run-test card is real
|
|
34
|
+
captured output; the endpoint card is an illustration of the authenticated listing.
|
|
35
|
+
|
|
20
36
|
## Install
|
|
21
37
|
|
|
22
38
|
Works with Claude Desktop, Claude Code, Cursor, Windsurf, Codex, Gemini CLI, Freebuff
|
|
@@ -24,8 +40,9 @@ and every other MCP client — pick whichever route fits:
|
|
|
24
40
|
|
|
25
41
|
| Route | How |
|
|
26
42
|
| --- | --- |
|
|
43
|
+
| **npm (canonical, fastest)** | `npx -y playwright-e2e-mcp` |
|
|
27
44
|
| **MCP Registry** (registry-aware clients discover it automatically) | `io.github.trajectiq-ai/E2E` — [listing](https://registry.modelcontextprotocol.io/) |
|
|
28
|
-
| **Any client, no npm account needed** | `npx -y github:trajectiq-ai/E2E` |
|
|
45
|
+
| **Any client, no npm account needed** | `npx -y github:trajectiq-ai/E2E#v0.1.2` (pin a release tag) |
|
|
29
46
|
| **Claude Desktop, zero Node setup** | double-click the [`.mcpb` extension](https://github.com/trajectiq-ai/E2E/releases) |
|
|
30
47
|
| **Remote-only clients (ChatGPT connectors)** | `https://playwright-e2e-mcp.vercel.app/api/mcp` |
|
|
31
48
|
|
|
@@ -51,7 +68,7 @@ Details and per-client config: [Installation](#installation) ·
|
|
|
51
68
|
|
|
52
69
|
| Argument | Type | Description |
|
|
53
70
|
| --- | --- | --- |
|
|
54
|
-
| `projectRoot` | string | Project directory (default: server working directory) |
|
|
71
|
+
| `projectRoot` | string | Project directory inside the configured root (default: server working directory) |
|
|
55
72
|
| `testFiles` | string[] | Files/directories relative to the root; `file:line` supported. Omit to run everything |
|
|
56
73
|
| `grep` | string | Only run tests whose title matches this regex |
|
|
57
74
|
| `browser` | `chromium` \| `firefox` \| `webkit` | Playwright project to run (matched against config project names) |
|
|
@@ -62,7 +79,7 @@ Details and per-client config: [Installation](#installation) ·
|
|
|
62
79
|
| `config` | string | `playwright.config` path **or 1-based index** when the project has several |
|
|
63
80
|
| `retryOnFailure` | boolean | Auto-retry failures **once** before reporting them (default `true`; ignored when `retries` is set) |
|
|
64
81
|
| `lastFailed` | boolean | Only re-run tests that failed in the previous run (Playwright `--last-failed`) — the fast fix → re-run loop |
|
|
65
|
-
| `args` | string[] | Extra
|
|
82
|
+
| `args` | string[] | Extra Playwright flags from an allowlist (`--repeat-each=N`, `--max-failures=N`, `--update-snapshots`, `--shard=1/3`, `--trace=on`, …); values go after `=` and are checked, and flags that take a path, such as `--config` or `--output`, are rejected |
|
|
66
83
|
|
|
67
84
|
Flakiness handling: by default the server injects `--retries=1` (unless the config
|
|
68
85
|
already sets `retries`), so a test that passes on the retry is reported as **flaky**,
|
|
@@ -157,9 +174,9 @@ plain CSS.
|
|
|
157
174
|
| --- | --- | --- |
|
|
158
175
|
| `description` | string | What the test should cover (required) |
|
|
159
176
|
| `pageUrl` | string | Page the test starts on (default: `baseURL` / `webServer.url` from config) |
|
|
160
|
-
| `testDir` / `file` | string | Where to write the spec (default: detected `testDir` + `generated/<slug>.spec.ts`) |
|
|
177
|
+
| `testDir` / `file` | string | Where to write the spec (default: detected `testDir` + `generated/<slug>.spec.ts`); `file` must end in `.spec.*` or `.test.*` |
|
|
161
178
|
| `write` | boolean | Write the file to disk (default `true`) |
|
|
162
|
-
| `overwrite` | boolean | Replace an existing
|
|
179
|
+
| `overwrite` | boolean | Replace an existing spec at the target path; only specs this tool generated can be replaced |
|
|
163
180
|
| `liveInspect` | boolean | Cross-check selectors against the live page (default on when a URL is known) |
|
|
164
181
|
| `projectRoot` / `config` | string | As with the other tools |
|
|
165
182
|
|
|
@@ -221,11 +238,12 @@ Requirements:
|
|
|
221
238
|
(`npx playwright install chromium`)
|
|
222
239
|
|
|
223
240
|
**No npm account needed** — install straight from GitHub (the `prepare` script
|
|
224
|
-
builds `dist/` automatically on install):
|
|
241
|
+
builds `dist/` automatically on install). Pin a release tag: an unpinned
|
|
242
|
+
`github:trajectiq-ai/E2E` runs whatever is on the default branch at that moment.
|
|
225
243
|
|
|
226
244
|
```bash
|
|
227
|
-
npx -y github:trajectiq-ai/E2E
|
|
228
|
-
npm install -D github:trajectiq-ai/E2E @playwright/test # or as a project dependency
|
|
245
|
+
npx -y github:trajectiq-ai/E2E#v0.1.2
|
|
246
|
+
npm install -D github:trajectiq-ai/E2E#v0.1.2 @playwright/test # or as a project dependency
|
|
229
247
|
npx playwright install chromium
|
|
230
248
|
```
|
|
231
249
|
|
|
@@ -233,7 +251,7 @@ Or grab the packaged tarball from the repo's **GitHub Releases** page and instal
|
|
|
233
251
|
it locally:
|
|
234
252
|
|
|
235
253
|
```bash
|
|
236
|
-
npm install -D https://github.com/trajectiq-ai/E2E/releases/download/v0.1.
|
|
254
|
+
npm install -D https://github.com/trajectiq-ai/E2E/releases/download/v0.1.2/playwright-e2e-mcp-0.1.2.tgz
|
|
237
255
|
```
|
|
238
256
|
|
|
239
257
|
Listed in the **official [MCP Registry](https://registry.modelcontextprotocol.io/)** as
|
|
@@ -249,7 +267,7 @@ Listed in the **official [MCP Registry](https://registry.modelcontextprotocol.io
|
|
|
249
267
|
"mcpServers": {
|
|
250
268
|
"playwright-e2e": {
|
|
251
269
|
"command": "npx",
|
|
252
|
-
"args": ["-y", "github:trajectiq-ai/E2E"],
|
|
270
|
+
"args": ["-y", "github:trajectiq-ai/E2E#v0.1.2"],
|
|
253
271
|
"env": { "PW_MCP_PROJECT_ROOT": "/absolute/path/to/your/project" }
|
|
254
272
|
}
|
|
255
273
|
}
|
|
@@ -263,8 +281,8 @@ when the client launches it somewhere else (e.g. your home directory).
|
|
|
263
281
|
**Codex / VS Code / Copilot CLIs:**
|
|
264
282
|
|
|
265
283
|
```bash
|
|
266
|
-
codex mcp add playwright-e2e -- npx -y github:trajectiq-ai/E2E
|
|
267
|
-
code --add-mcp '{"name":"playwright-e2e","command":"npx","args":["-y","github:trajectiq-ai/E2E"]}'
|
|
284
|
+
codex mcp add playwright-e2e -- npx -y github:trajectiq-ai/E2E#v0.1.2
|
|
285
|
+
code --add-mcp '{"name":"playwright-e2e","command":"npx","args":["-y","github:trajectiq-ai/E2E#v0.1.2"]}'
|
|
268
286
|
```
|
|
269
287
|
|
|
270
288
|
Codex's defaults fight this server: the first launch clones the repo and runs
|
|
@@ -275,7 +293,7 @@ Codex's defaults fight this server: the first launch clones the repo and runs
|
|
|
275
293
|
```toml
|
|
276
294
|
[mcp_servers.playwright-e2e]
|
|
277
295
|
command = "npx"
|
|
278
|
-
args = ["-y", "github:trajectiq-ai/E2E"]
|
|
296
|
+
args = ["-y", "github:trajectiq-ai/E2E#v0.1.2"]
|
|
279
297
|
startup_timeout_sec = 60
|
|
280
298
|
tool_timeout_sec = 600
|
|
281
299
|
```
|
|
@@ -283,13 +301,14 @@ tool_timeout_sec = 600
|
|
|
283
301
|
**Claude Desktop (one-click):** download and double-click the `.mcpb` Desktop
|
|
284
302
|
Extension attached to the [latest release](https://github.com/trajectiq-ai/E2E/releases) —
|
|
285
303
|
the bundle ships its own dependencies, so no Node setup is required. On install it
|
|
286
|
-
|
|
304
|
+
asks you to pick your **project root** (required, no default: choose the project
|
|
305
|
+
folder, not your home directory) and wires it
|
|
287
306
|
into `PW_MCP_PROJECT_ROOT`, so the tools point at a real project from the first call.
|
|
288
307
|
|
|
289
308
|
**Claude Code:**
|
|
290
309
|
|
|
291
310
|
```bash
|
|
292
|
-
claude mcp add playwright-e2e -- npx -y github:trajectiq-ai/E2E
|
|
311
|
+
claude mcp add playwright-e2e -- npx -y github:trajectiq-ai/E2E#v0.1.2
|
|
293
312
|
```
|
|
294
313
|
|
|
295
314
|
**Gemini CLI / Qwen Code:** paste the `mcpServers` block above into
|
|
@@ -330,7 +349,7 @@ Streamable HTTP bridge for exactly that case:
|
|
|
330
349
|
| --- | --- |
|
|
331
350
|
| **Endpoint** | `https://playwright-e2e-mcp.vercel.app/api/mcp` |
|
|
332
351
|
| **Transport** | MCP Streamable HTTP (`POST` JSON in, JSON or SSE out) |
|
|
333
|
-
| **Auth** |
|
|
352
|
+
| **Auth** | optional bearer token (`PW_MCP_HTTP_TOKEN`); without one only read-only tools are served |
|
|
334
353
|
| **Source** | [`api/mcp.ts`](api/mcp.ts) → [`src/http.ts`](src/http.ts) |
|
|
335
354
|
|
|
336
355
|
The bridge runs the *same* `createServer()` as the stdio transport; the SDK
|
|
@@ -338,9 +357,22 @@ serves every request with a fresh server instance, which is what a serverless
|
|
|
338
357
|
function wants. `test/http-bridge.test.mjs` drives the real Node adapter over
|
|
339
358
|
`node:http` so a broken bridge fails in CI, not in ChatGPT.
|
|
340
359
|
|
|
360
|
+
**Open vs. token-protected.** When the deployment has no `PW_MCP_HTTP_TOKEN`,
|
|
361
|
+
anyone can reach the URL, so the bridge serves only `list-tests` and
|
|
362
|
+
`get-failure`, in restricted mode: `list-tests` scans sources instead of running
|
|
363
|
+
`playwright test --list` (which would execute the project's config), callers
|
|
364
|
+
cannot pick another `projectRoot`, and nothing spawns a process, drives a browser
|
|
365
|
+
or writes a file. Set `PW_MCP_HTTP_TOKEN` (at least 16 characters; use a random
|
|
366
|
+
value) to serve all eight tools to clients that send `Authorization: Bearer <token>`;
|
|
367
|
+
other requests get `401`. Child processes started over HTTP get only an allowlisted
|
|
368
|
+
environment. `PW_MCP_ALLOWED_HOSTS` (comma separated, `*` for any) limits the
|
|
369
|
+
accepted `Host` header; without a token and without that variable, only
|
|
370
|
+
`localhost` names and the deployment's own Vercel hostnames are accepted
|
|
371
|
+
(DNS-rebinding protection).
|
|
372
|
+
|
|
341
373
|
**Add it to ChatGPT:** Settings → Connectors → turn on **Advanced → Developer
|
|
342
374
|
mode** → *Create custom connector* → paste the endpoint above → authentication
|
|
343
|
-
**None
|
|
375
|
+
**None** (read-only tools).
|
|
344
376
|
|
|
345
377
|
Codex can also take the remote transport instead of spawning `npx`, if you'd
|
|
346
378
|
rather not ship Playwright to every machine:
|
|
@@ -350,11 +382,11 @@ codex mcp add playwright-e2e-remote --url https://playwright-e2e-mcp.vercel.app/
|
|
|
350
382
|
```
|
|
351
383
|
|
|
352
384
|
**What to expect:** `list-tests` works and reports the specs bundled with the
|
|
353
|
-
deployment.
|
|
354
|
-
`validate-selector`, `diagnose-flaky`, …) cannot download
|
|
355
|
-
serverless function, so they return their normal `NO_PLAYWRIGHT`
|
|
356
|
-
stdio install for real runs; the hosted endpoint is for discovery
|
|
357
|
-
clients that cannot run local processes.
|
|
385
|
+
deployment. Even with a token, tools that spawn a browser (`run-test`,
|
|
386
|
+
`inspect-page`, `validate-selector`, `diagnose-flaky`, …) cannot download
|
|
387
|
+
Chromium in a serverless function, so they return their normal `NO_PLAYWRIGHT`
|
|
388
|
+
hint. Use the stdio install for real runs; the hosted endpoint is for discovery
|
|
389
|
+
and for clients that cannot run local processes.
|
|
358
390
|
|
|
359
391
|
```bash
|
|
360
392
|
# verify the handshake without any client
|
|
@@ -364,17 +396,21 @@ curl -X POST https://playwright-e2e-mcp.vercel.app/api/mcp \
|
|
|
364
396
|
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
|
|
365
397
|
```
|
|
366
398
|
|
|
367
|
-
Redeploy after a change:
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
npx vercel deploy --yes --prod --token="$VERCEL_TOKEN"
|
|
371
|
-
```
|
|
399
|
+
Redeploy after a change: merge to `main`. Vercel's Git integration deploys
|
|
400
|
+
every push, so there is no token to manage and no CLI step — watch the
|
|
401
|
+
`Vercel` commit status for the deployment result.
|
|
372
402
|
|
|
373
403
|
## Configuration
|
|
374
404
|
|
|
375
405
|
| Environment variable | Default | Purpose |
|
|
376
406
|
| --- | --- | --- |
|
|
377
407
|
| `PW_MCP_PROJECT_ROOT` | server cwd | Default project root for every tool |
|
|
408
|
+
| `PW_MCP_ALLOWED_ROOTS` | — | Extra directories a caller may pass as `projectRoot` (`:`-separated, `;` on Windows). Anything outside these and the default root is rejected |
|
|
409
|
+
| `PW_MCP_HTTP_TOKEN` | — | HTTP bridge only: bearer token (16+ characters) that unlocks all tools (see above) |
|
|
410
|
+
| `PW_MCP_ALLOWED_HOSTS` | localhost + Vercel hostnames when there is no token | HTTP bridge only: comma-separated `Host` allowlist; `*` accepts any |
|
|
411
|
+
| `PW_MCP_PASSTHROUGH_ENV` | — | HTTP bridge only: comma-separated extra variables passed to test runs (e.g. `BASE_URL`) |
|
|
412
|
+
| `PW_MCP_MAX_CHILDREN` | `4` | HTTP bridge only: how many test runs and browser probes may run at once |
|
|
413
|
+
| `PW_MCP_BLOCK_PRIVATE_URLS` | off (on for the HTTP bridge) | `1` makes the URL tools refuse loopback, private-network and cloud-metadata addresses, including redirects and subresources |
|
|
378
414
|
| `LOG_LEVEL` | `info` | `debug` \| `info` \| `warn` \| `error` \| `silent` |
|
|
379
415
|
| `LOG_FORMAT` | `text` | `text` or `json` (structured) |
|
|
380
416
|
|
|
@@ -415,15 +451,42 @@ Logs always go to **stderr** — stdout is reserved for the MCP protocol.
|
|
|
415
451
|
| Syntax error in a spec | `SYNTAX_ERROR` with file:line; nothing crashes; `list-tests` falls back to a source scan |
|
|
416
452
|
| MCP client disconnects | Per-request `AbortSignal` kills the run; stdin end triggers shutdown, and every tracked child tree is force-killed (`killActiveChildren`) |
|
|
417
453
|
| Disk full | `ENOSPC` detected → `DISK_FULL` with a "free space" hint; logging never throws |
|
|
418
|
-
| Malicious paths | `../../etc/passwd`, absolute paths outside the root, URLs and null bytes are rejected with `INVALID_PATH`; CLI args
|
|
454
|
+
| Malicious paths | `../../etc/passwd`, absolute paths outside the root, a `projectRoot` outside the allowed roots, symlinks that leave the root, URLs and null bytes are rejected with `INVALID_PATH`; extra CLI args must be allowlisted Playwright flags |
|
|
419
455
|
|
|
420
456
|
## Security notes
|
|
421
457
|
|
|
422
458
|
- **No shell**: Playwright is spawned as `node <playwright/cli.js> …` with an argument
|
|
423
|
-
array — no command interpolation.
|
|
424
|
-
|
|
459
|
+
array — no command interpolation. Extra `args` are limited to an allowlist of
|
|
460
|
+
Playwright flags, each with a checked value (`--flag=value`), so a caller cannot
|
|
461
|
+
swap in another `--config` or `--output` or hand git an option through
|
|
462
|
+
`--only-changed`. Paths with a segment starting with `-` are rejected, so a test
|
|
463
|
+
file name cannot be read as a flag.
|
|
464
|
+
- **Path sandbox**: user paths must stay inside the project root, checked lexically
|
|
465
|
+
and again with symlinks resolved. A caller-supplied `projectRoot` must sit inside
|
|
466
|
+
`PW_MCP_PROJECT_ROOT` (or a `PW_MCP_ALLOWED_ROOTS` entry).
|
|
467
|
+
- **Generated code**: `generate-e2e-test` only writes `*.spec.*` / `*.test.*` files,
|
|
468
|
+
only overwrites specs whose header it wrote itself, never writes through a symlink,
|
|
469
|
+
and escapes every value it puts into strings or comments.
|
|
470
|
+
- **HTTP bridge**: read-only tools unless `PW_MCP_HTTP_TOKEN` is set; child processes
|
|
471
|
+
started over HTTP get only an allowlisted environment (`PATH`, `HOME`, temp dirs,
|
|
472
|
+
locale, `CI`, `PLAYWRIGHT_*`, `npm_config_*` without embedded credentials, plus
|
|
473
|
+
anything in `PW_MCP_PASSTHROUGH_ENV`), at most `PW_MCP_MAX_CHILDREN` run at once
|
|
474
|
+
(a disconnected client frees its slot immediately), and internal error messages
|
|
475
|
+
are not returned to clients. The allowlist only covers the child's own
|
|
476
|
+
environment: test code runs as the same OS user, so on Linux it could still read
|
|
477
|
+
the server's startup environment from `/proc`. That is why only token holders
|
|
478
|
+
can run project code; keep other secrets out of the bridge's environment, or run
|
|
479
|
+
it under a separate user.
|
|
480
|
+
- **SSRF guard**: on the HTTP bridge (or with `PW_MCP_BLOCK_PRIVATE_URLS=1`) the URL tools
|
|
481
|
+
refuse hosts that resolve to loopback, private, link-local/metadata or reserved
|
|
482
|
+
addresses, and route the browser through a local proxy that applies the same check
|
|
483
|
+
to every redirect hop and subresource; IPv6 forms that embed an IPv4 address
|
|
484
|
+
(mapped, NAT64, 6to4, Teredo) are blocked too, and WebRTC UDP is disabled so a
|
|
485
|
+
page cannot reach the network around the proxy. It is off for stdio by default because
|
|
486
|
+
opening `http://localhost` dev servers is what these tools are for.
|
|
425
487
|
- **Cleanup**: temp report/script files are written to the OS temp dir and removed;
|
|
426
|
-
child processes are tracked and killed on shutdown.
|
|
488
|
+
child processes are tracked and killed on shutdown. Trace decompression has one
|
|
489
|
+
size budget per archive, and PNGs are capped at 16,384 px per side and 50 M pixels.
|
|
427
490
|
|
|
428
491
|
## Development
|
|
429
492
|
|
|
@@ -444,7 +507,7 @@ Zod v4 standard schemas; every tool declares spec tool annotations.
|
|
|
444
507
|
```bash
|
|
445
508
|
npm install
|
|
446
509
|
npm run build # tsc → dist/ (zero errors)
|
|
447
|
-
npm test # build + test/run-tests.mjs (
|
|
510
|
+
npm test # build + test/run-tests.mjs (unit tests, any Node ≥20)
|
|
448
511
|
npm run e2e # build + e2e/run.mjs: live MCP ↔ Playwright integration suite
|
|
449
512
|
```
|
|
450
513
|
|
|
@@ -454,8 +517,12 @@ discovery, multiple configs, missing install, test-file scanning), the shared to
|
|
|
454
517
|
helpers, the **trace reader** (synthetic trace.zip: error, failed action, DOM snapshot,
|
|
455
518
|
`*.network` failed-request parsing, console error/warning events), the **image diff**
|
|
456
519
|
(PNG round-trip, regions, color shift, dimension changes), the **change analyzer**
|
|
457
|
-
(selector extraction, git + mtime paths)
|
|
458
|
-
(failure signatures, CONSISTENTLY FAILING / FLAKY / NOT REPRODUCING / NO TESTS RAN)
|
|
520
|
+
(selector extraction, git + mtime paths), the **flaky verdict logic**
|
|
521
|
+
(failure signatures, CONSISTENTLY FAILING / FLAKY / NOT REPRODUCING / NO TESTS RAN),
|
|
522
|
+
the **HTTP bridge** (token, restricted mode, Host allowlist), the **security
|
|
523
|
+
regressions** (sandbox escapes, argument smuggling, symlink writes, code injection,
|
|
524
|
+
SSRF ranges, env scrubbing, decode limits), the `.mcpb` manifest and the MCP config
|
|
525
|
+
files.
|
|
459
526
|
|
|
460
527
|
### Integration suite (`npm run e2e`)
|
|
461
528
|
|
package/dist/http.d.ts
CHANGED
|
@@ -16,8 +16,26 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
16
16
|
import type { McpHttpHandler } from '@modelcontextprotocol/server';
|
|
17
17
|
/** Mirrors the SDK's default POST body bound, so oversized bodies die early. */
|
|
18
18
|
export declare const MAX_BODY_BYTES: number;
|
|
19
|
+
export interface McpHttpHandlerOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Bearer token clients must send (`Authorization: Bearer <token>`).
|
|
22
|
+
* Defaults to PW_MCP_HTTP_TOKEN. With a token every tool is served;
|
|
23
|
+
* without one only READ_ONLY_TOOLS are, in restricted mode, so an open
|
|
24
|
+
* endpoint can never spawn processes, drive a browser or write files.
|
|
25
|
+
*/
|
|
26
|
+
token?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Hostnames accepted in the Host header (DNS-rebinding protection).
|
|
29
|
+
* Defaults to PW_MCP_ALLOWED_HOSTS (comma separated; `*` allows any).
|
|
30
|
+
* When unset and there is no token, only localhost names and the
|
|
31
|
+
* deployment's own Vercel hostnames are accepted.
|
|
32
|
+
*/
|
|
33
|
+
allowedHosts?: string[];
|
|
34
|
+
}
|
|
35
|
+
/** Tokens shorter than this are refused: they can be guessed. */
|
|
36
|
+
export declare const MIN_TOKEN_LENGTH = 16;
|
|
19
37
|
/** Build the fetch-shaped MCP handler backed by a fresh server per request. */
|
|
20
|
-
export declare function createMcpHttpHandler(): McpHttpHandler;
|
|
38
|
+
export declare function createMcpHttpHandler(options?: McpHttpHandlerOptions): McpHttpHandler;
|
|
21
39
|
/**
|
|
22
40
|
* Serve one Node request through the MCP handler.
|
|
23
41
|
*
|
package/dist/http.js
CHANGED
|
@@ -12,17 +12,90 @@
|
|
|
12
12
|
* The stdio transport in server.ts is untouched; this is an additional way to
|
|
13
13
|
* reach the same eight tools.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
16
|
+
import { createMcpHandler, hostHeaderValidationResponse } from '@modelcontextprotocol/server';
|
|
17
|
+
import { createServer, READ_ONLY_TOOLS } from './server.js';
|
|
18
|
+
import { setMaxChildren, setScrubChildEnv } from './utils/playwright-runner.js';
|
|
19
|
+
import { setBlockPrivateUrls } from './utils/url-policy.js';
|
|
17
20
|
import { logger } from './utils/logger.js';
|
|
18
21
|
/** Mirrors the SDK's default POST body bound, so oversized bodies die early. */
|
|
19
22
|
export const MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
23
|
+
/** Tokens shorter than this are refused: they can be guessed. */
|
|
24
|
+
export const MIN_TOKEN_LENGTH = 16;
|
|
25
|
+
/** Default Host allowlist for an open (token-less) bridge. */
|
|
26
|
+
function defaultAllowedHosts() {
|
|
27
|
+
const vercel = [process.env.VERCEL_URL, process.env.VERCEL_BRANCH_URL, process.env.VERCEL_PROJECT_PRODUCTION_URL]
|
|
28
|
+
.map((host) => host?.trim().toLowerCase())
|
|
29
|
+
.filter((host) => Boolean(host));
|
|
30
|
+
return ['localhost', '127.0.0.1', '[::1]', ...vercel];
|
|
31
|
+
}
|
|
32
|
+
function envList(value) {
|
|
33
|
+
return (value ?? '')
|
|
34
|
+
.split(',')
|
|
35
|
+
.map((item) => item.trim())
|
|
36
|
+
.filter((item) => item !== '');
|
|
37
|
+
}
|
|
38
|
+
/** Constant-time comparison of the presented bearer token (hashed, so length does not leak). */
|
|
39
|
+
function tokenMatches(header, token) {
|
|
40
|
+
const match = /^Bearer\s+(.+)$/i.exec(header?.trim() ?? '');
|
|
41
|
+
if (!match)
|
|
42
|
+
return false;
|
|
43
|
+
const digest = (value) => createHash('sha256').update(value).digest();
|
|
44
|
+
return timingSafeEqual(digest(match[1].trim()), digest(token));
|
|
45
|
+
}
|
|
46
|
+
/** PW_MCP_MAX_CHILDREN as a positive integer; anything else means the default of 4. */
|
|
47
|
+
function maxChildrenFromEnv() {
|
|
48
|
+
const value = Number(process.env.PW_MCP_MAX_CHILDREN ?? '');
|
|
49
|
+
return Number.isInteger(value) && value > 0 ? value : 4;
|
|
50
|
+
}
|
|
51
|
+
function jsonResponse(status, body, headers = {}) {
|
|
52
|
+
return new Response(JSON.stringify(body), {
|
|
53
|
+
status,
|
|
54
|
+
headers: { 'content-type': 'application/json', ...headers },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
20
57
|
/** Build the fetch-shaped MCP handler backed by a fresh server per request. */
|
|
21
|
-
export function createMcpHttpHandler() {
|
|
22
|
-
|
|
58
|
+
export function createMcpHttpHandler(options = {}) {
|
|
59
|
+
const token = (options.token ?? process.env.PW_MCP_HTTP_TOKEN ?? '').trim();
|
|
60
|
+
if (token && token.length < MIN_TOKEN_LENGTH) {
|
|
61
|
+
throw new Error(`PW_MCP_HTTP_TOKEN must be at least ${MIN_TOKEN_LENGTH} characters (use a random value).`);
|
|
62
|
+
}
|
|
63
|
+
const configuredHosts = options.allowedHosts ?? envList(process.env.PW_MCP_ALLOWED_HOSTS);
|
|
64
|
+
const allowedHosts = configuredHosts.includes('*')
|
|
65
|
+
? []
|
|
66
|
+
: configuredHosts.length > 0 || token
|
|
67
|
+
? configuredHosts
|
|
68
|
+
: defaultAllowedHosts();
|
|
69
|
+
// Children spawned on behalf of HTTP callers must not see deployment
|
|
70
|
+
// secrets, and URL tools must not reach the deployment's private network.
|
|
71
|
+
setScrubChildEnv(true);
|
|
72
|
+
setMaxChildren(maxChildrenFromEnv());
|
|
73
|
+
setBlockPrivateUrls(true);
|
|
74
|
+
if (!token) {
|
|
75
|
+
logger.warn('PW_MCP_HTTP_TOKEN is not set; serving read-only tools only', { tools: READ_ONLY_TOOLS });
|
|
76
|
+
}
|
|
77
|
+
const inner = createMcpHandler((context) => createServer({
|
|
78
|
+
...(token ? {} : { tools: READ_ONLY_TOOLS, restricted: true }),
|
|
79
|
+
// Lets tools stop their children when the HTTP client disconnects.
|
|
80
|
+
requestSignal: context?.requestInfo?.signal,
|
|
81
|
+
}), {
|
|
23
82
|
legacy: 'stateless',
|
|
24
83
|
onerror: (error) => logger.error('mcp http handler error', { error }),
|
|
25
84
|
});
|
|
85
|
+
return {
|
|
86
|
+
...inner,
|
|
87
|
+
fetch: async (request, requestOptions) => {
|
|
88
|
+
if (allowedHosts.length > 0) {
|
|
89
|
+
const rejected = hostHeaderValidationResponse(request, allowedHosts);
|
|
90
|
+
if (rejected)
|
|
91
|
+
return rejected;
|
|
92
|
+
}
|
|
93
|
+
if (token && !tokenMatches(request.headers.get('authorization'), token)) {
|
|
94
|
+
return jsonResponse(401, { error: 'unauthorized' }, { 'www-authenticate': 'Bearer' });
|
|
95
|
+
}
|
|
96
|
+
return inner.fetch(request, requestOptions);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
26
99
|
}
|
|
27
100
|
/** A failure that maps onto an HTTP status before any response is written. */
|
|
28
101
|
class RequestError extends Error {
|
|
@@ -59,17 +132,18 @@ async function readBody(req) {
|
|
|
59
132
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
60
133
|
size += buf.byteLength;
|
|
61
134
|
if (size > MAX_BODY_BYTES) {
|
|
62
|
-
|
|
135
|
+
// Stop reading; the caller answers 413 and then closes the connection.
|
|
136
|
+
req.pause();
|
|
63
137
|
throw new RequestError(413, 'request body too large');
|
|
64
138
|
}
|
|
65
139
|
chunks.push(buf);
|
|
66
140
|
}
|
|
67
141
|
return Buffer.concat(chunks, size);
|
|
68
142
|
}
|
|
69
|
-
function buildRequest(req, method, body) {
|
|
143
|
+
function buildRequest(req, method, body, signal) {
|
|
70
144
|
const protocol = (firstHeader(req.headers['x-forwarded-proto']) ?? 'http').split(',')[0].trim();
|
|
71
145
|
const host = firstHeader(req.headers.host) ?? 'localhost';
|
|
72
|
-
const init = { method, headers: toHeaders(req.headers) };
|
|
146
|
+
const init = { method, headers: toHeaders(req.headers), signal };
|
|
73
147
|
if (body && body.byteLength > 0)
|
|
74
148
|
init.body = body;
|
|
75
149
|
return new Request(`${protocol}://${host}${req.url ?? '/'}`, init);
|
|
@@ -93,11 +167,18 @@ function waitDrain(res) {
|
|
|
93
167
|
* mid-stream failure closes the socket so the client stops waiting.
|
|
94
168
|
*/
|
|
95
169
|
export async function handleNodeRequest(handler, req, res) {
|
|
170
|
+
// Abort the request (and the tool's children) when the client goes away
|
|
171
|
+
// before the response is complete, so its child slot is freed at once.
|
|
172
|
+
const disconnect = new AbortController();
|
|
173
|
+
res.once('close', () => {
|
|
174
|
+
if (!res.writableFinished)
|
|
175
|
+
disconnect.abort();
|
|
176
|
+
});
|
|
96
177
|
let response;
|
|
97
178
|
try {
|
|
98
179
|
const method = (req.method ?? 'GET').toUpperCase();
|
|
99
180
|
const body = method === 'GET' || method === 'HEAD' ? undefined : await readBody(req);
|
|
100
|
-
response = await handler.fetch(buildRequest(req, method, body));
|
|
181
|
+
response = await handler.fetch(buildRequest(req, method, body, disconnect.signal));
|
|
101
182
|
}
|
|
102
183
|
catch (err) {
|
|
103
184
|
const status = err instanceof RequestError ? err.status : 500;
|
|
@@ -107,7 +188,15 @@ export async function handleNodeRequest(handler, req, res) {
|
|
|
107
188
|
if (!res.headersSent && !res.writableEnded) {
|
|
108
189
|
res.statusCode = status;
|
|
109
190
|
res.setHeader('content-type', 'application/json');
|
|
110
|
-
|
|
191
|
+
// Only our own RequestError messages are meant for clients; anything
|
|
192
|
+
// else may carry paths or internals, so it stays in the server log.
|
|
193
|
+
const message = err instanceof RequestError ? err.message : 'internal server error';
|
|
194
|
+
if (status === 413)
|
|
195
|
+
res.setHeader('connection', 'close');
|
|
196
|
+
res.end(JSON.stringify({ error: message }), () => {
|
|
197
|
+
if (status === 413)
|
|
198
|
+
req.destroy();
|
|
199
|
+
});
|
|
111
200
|
}
|
|
112
201
|
else if (!res.writableEnded) {
|
|
113
202
|
res.destroy();
|
package/dist/http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -33,9 +33,17 @@ async function main() {
|
|
|
33
33
|
'Usage: playwright-e2e-mcp [--version] [--help]',
|
|
34
34
|
'',
|
|
35
35
|
'Connects over stdio (MCP). Configuration via environment:',
|
|
36
|
-
' PW_MCP_PROJECT_ROOT
|
|
37
|
-
'
|
|
38
|
-
'
|
|
36
|
+
' PW_MCP_PROJECT_ROOT default project root (default: cwd)',
|
|
37
|
+
' PW_MCP_ALLOWED_ROOTS extra roots callers may pass as projectRoot',
|
|
38
|
+
' PW_MCP_BLOCK_PRIVATE_URLS 1 = URL tools refuse private/loopback/metadata addresses',
|
|
39
|
+
' LOG_LEVEL / MCP_LOG_LEVEL debug | info | warn | error | silent (default: info)',
|
|
40
|
+
' LOG_FORMAT / MCP_LOG_FORMAT text | json (default: text)',
|
|
41
|
+
'',
|
|
42
|
+
'HTTP bridge only (api/mcp.ts, src/http.ts):',
|
|
43
|
+
' PW_MCP_HTTP_TOKEN bearer token (16+ chars) that unlocks all tools',
|
|
44
|
+
' PW_MCP_ALLOWED_HOSTS Host allowlist, comma separated (* = any)',
|
|
45
|
+
' PW_MCP_PASSTHROUGH_ENV extra variables passed to test runs',
|
|
46
|
+
' PW_MCP_MAX_CHILDREN concurrent runs (default: 4)',
|
|
39
47
|
'',
|
|
40
48
|
].join('\n'));
|
|
41
49
|
return;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,SAAS,YAAY;IACnB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9B,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACtE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,cAAc,IAAI,CAAC,CAAC;QAC5C,OAAO;IACT,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACnE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB;YACE,4EAA4E;YAC5E,EAAE;YACF,gDAAgD;YAChD,EAAE;YACF,2DAA2D;YAC3D,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,SAAS,YAAY;IACnB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9B,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACtE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,cAAc,IAAI,CAAC,CAAC;QAC5C,OAAO;IACT,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACnE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB;YACE,4EAA4E;YAC5E,EAAE;YACF,gDAAgD;YAChD,EAAE;YACF,2DAA2D;YAC3D,kEAAkE;YAClE,0EAA0E;YAC1E,uFAAuF;YACvF,mFAAmF;YACnF,4DAA4D;YAC5D,EAAE;YACF,6CAA6C;YAC7C,8EAA8E;YAC9E,wEAAwE;YACxE,kEAAkE;YAClE,2DAA2D;YAC3D,EAAE;SACH,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;QACF,OAAO;IACT,CAAC;IAED,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;IACpD,MAAM,WAAW,EAAE,CAAC;AACtB,CAAC;AAED,IAAI,YAAY,EAAE,EAAE,CAAC;IACnB,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QAC5B,MAAM,GAAG,GAAG,YAAY,CAAC,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,oBAAoB,EAAE,EAAE,CAAC,CAAC;QACvE,GAAG,CAAC,KAAK,CAAC,wBAAwB,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QACpD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACpF,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
package/dist/server.d.ts
CHANGED
|
@@ -14,7 +14,20 @@ export interface CreateServerOptions {
|
|
|
14
14
|
projectRoot?: string;
|
|
15
15
|
/** Share a store across servers (tests). */
|
|
16
16
|
store?: ToolStore;
|
|
17
|
+
/** Register only these tools (default: all). Used by the hosted HTTP bridge. */
|
|
18
|
+
tools?: readonly string[];
|
|
19
|
+
/** Unauthenticated HTTP mode: see ToolContext.restricted. */
|
|
20
|
+
restricted?: boolean;
|
|
21
|
+
/** Aborted when the HTTP client goes away; combined with the MCP request signal. */
|
|
22
|
+
requestSignal?: AbortSignal;
|
|
17
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Tools that neither drive a browser nor write files. The HTTP bridge
|
|
26
|
+
* serves only these unless it is protected by a token, and runs them in
|
|
27
|
+
* restricted mode, where list-tests scans sources instead of spawning
|
|
28
|
+
* Playwright (which would execute the project's config and spec files).
|
|
29
|
+
*/
|
|
30
|
+
export declare const READ_ONLY_TOOLS: readonly string[];
|
|
18
31
|
export declare function resolveDefaultProjectRoot(): string;
|
|
19
32
|
export declare function createServer(options?: CreateServerOptions): McpServer;
|
|
20
33
|
/** Close the server, kill orphaned children, and exit. */
|