playwright-e2e-mcp 0.1.1 → 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 +90 -38
- 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
|
@@ -19,17 +19,19 @@ with unique CSS selectors, and validate the replacement selector before re-runni
|
|
|
19
19
|
|
|
20
20
|
## Demo
|
|
21
21
|
|
|
22
|
-
**
|
|
23
|
-
`https://playwright-e2e-mcp.vercel.app/api/mcp
|
|
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):
|
|
24
25
|
|
|
25
|
-

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

|
|
31
32
|
|
|
32
|
-
Images are
|
|
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.
|
|
33
35
|
|
|
34
36
|
## Install
|
|
35
37
|
|
|
@@ -40,7 +42,7 @@ and every other MCP client — pick whichever route fits:
|
|
|
40
42
|
| --- | --- |
|
|
41
43
|
| **npm (canonical, fastest)** | `npx -y playwright-e2e-mcp` |
|
|
42
44
|
| **MCP Registry** (registry-aware clients discover it automatically) | `io.github.trajectiq-ai/E2E` — [listing](https://registry.modelcontextprotocol.io/) |
|
|
43
|
-
| **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) |
|
|
44
46
|
| **Claude Desktop, zero Node setup** | double-click the [`.mcpb` extension](https://github.com/trajectiq-ai/E2E/releases) |
|
|
45
47
|
| **Remote-only clients (ChatGPT connectors)** | `https://playwright-e2e-mcp.vercel.app/api/mcp` |
|
|
46
48
|
|
|
@@ -66,7 +68,7 @@ Details and per-client config: [Installation](#installation) ·
|
|
|
66
68
|
|
|
67
69
|
| Argument | Type | Description |
|
|
68
70
|
| --- | --- | --- |
|
|
69
|
-
| `projectRoot` | string | Project directory (default: server working directory) |
|
|
71
|
+
| `projectRoot` | string | Project directory inside the configured root (default: server working directory) |
|
|
70
72
|
| `testFiles` | string[] | Files/directories relative to the root; `file:line` supported. Omit to run everything |
|
|
71
73
|
| `grep` | string | Only run tests whose title matches this regex |
|
|
72
74
|
| `browser` | `chromium` \| `firefox` \| `webkit` | Playwright project to run (matched against config project names) |
|
|
@@ -77,7 +79,7 @@ Details and per-client config: [Installation](#installation) ·
|
|
|
77
79
|
| `config` | string | `playwright.config` path **or 1-based index** when the project has several |
|
|
78
80
|
| `retryOnFailure` | boolean | Auto-retry failures **once** before reporting them (default `true`; ignored when `retries` is set) |
|
|
79
81
|
| `lastFailed` | boolean | Only re-run tests that failed in the previous run (Playwright `--last-failed`) — the fast fix → re-run loop |
|
|
80
|
-
| `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 |
|
|
81
83
|
|
|
82
84
|
Flakiness handling: by default the server injects `--retries=1` (unless the config
|
|
83
85
|
already sets `retries`), so a test that passes on the retry is reported as **flaky**,
|
|
@@ -172,9 +174,9 @@ plain CSS.
|
|
|
172
174
|
| --- | --- | --- |
|
|
173
175
|
| `description` | string | What the test should cover (required) |
|
|
174
176
|
| `pageUrl` | string | Page the test starts on (default: `baseURL` / `webServer.url` from config) |
|
|
175
|
-
| `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.*` |
|
|
176
178
|
| `write` | boolean | Write the file to disk (default `true`) |
|
|
177
|
-
| `overwrite` | boolean | Replace an existing
|
|
179
|
+
| `overwrite` | boolean | Replace an existing spec at the target path; only specs this tool generated can be replaced |
|
|
178
180
|
| `liveInspect` | boolean | Cross-check selectors against the live page (default on when a URL is known) |
|
|
179
181
|
| `projectRoot` / `config` | string | As with the other tools |
|
|
180
182
|
|
|
@@ -236,11 +238,12 @@ Requirements:
|
|
|
236
238
|
(`npx playwright install chromium`)
|
|
237
239
|
|
|
238
240
|
**No npm account needed** — install straight from GitHub (the `prepare` script
|
|
239
|
-
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.
|
|
240
243
|
|
|
241
244
|
```bash
|
|
242
|
-
npx -y github:trajectiq-ai/E2E
|
|
243
|
-
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
|
|
244
247
|
npx playwright install chromium
|
|
245
248
|
```
|
|
246
249
|
|
|
@@ -248,7 +251,7 @@ Or grab the packaged tarball from the repo's **GitHub Releases** page and instal
|
|
|
248
251
|
it locally:
|
|
249
252
|
|
|
250
253
|
```bash
|
|
251
|
-
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
|
|
252
255
|
```
|
|
253
256
|
|
|
254
257
|
Listed in the **official [MCP Registry](https://registry.modelcontextprotocol.io/)** as
|
|
@@ -264,7 +267,7 @@ Listed in the **official [MCP Registry](https://registry.modelcontextprotocol.io
|
|
|
264
267
|
"mcpServers": {
|
|
265
268
|
"playwright-e2e": {
|
|
266
269
|
"command": "npx",
|
|
267
|
-
"args": ["-y", "github:trajectiq-ai/E2E"],
|
|
270
|
+
"args": ["-y", "github:trajectiq-ai/E2E#v0.1.2"],
|
|
268
271
|
"env": { "PW_MCP_PROJECT_ROOT": "/absolute/path/to/your/project" }
|
|
269
272
|
}
|
|
270
273
|
}
|
|
@@ -278,8 +281,8 @@ when the client launches it somewhere else (e.g. your home directory).
|
|
|
278
281
|
**Codex / VS Code / Copilot CLIs:**
|
|
279
282
|
|
|
280
283
|
```bash
|
|
281
|
-
codex mcp add playwright-e2e -- npx -y github:trajectiq-ai/E2E
|
|
282
|
-
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"]}'
|
|
283
286
|
```
|
|
284
287
|
|
|
285
288
|
Codex's defaults fight this server: the first launch clones the repo and runs
|
|
@@ -290,7 +293,7 @@ Codex's defaults fight this server: the first launch clones the repo and runs
|
|
|
290
293
|
```toml
|
|
291
294
|
[mcp_servers.playwright-e2e]
|
|
292
295
|
command = "npx"
|
|
293
|
-
args = ["-y", "github:trajectiq-ai/E2E"]
|
|
296
|
+
args = ["-y", "github:trajectiq-ai/E2E#v0.1.2"]
|
|
294
297
|
startup_timeout_sec = 60
|
|
295
298
|
tool_timeout_sec = 600
|
|
296
299
|
```
|
|
@@ -298,13 +301,14 @@ tool_timeout_sec = 600
|
|
|
298
301
|
**Claude Desktop (one-click):** download and double-click the `.mcpb` Desktop
|
|
299
302
|
Extension attached to the [latest release](https://github.com/trajectiq-ai/E2E/releases) —
|
|
300
303
|
the bundle ships its own dependencies, so no Node setup is required. On install it
|
|
301
|
-
|
|
304
|
+
asks you to pick your **project root** (required, no default: choose the project
|
|
305
|
+
folder, not your home directory) and wires it
|
|
302
306
|
into `PW_MCP_PROJECT_ROOT`, so the tools point at a real project from the first call.
|
|
303
307
|
|
|
304
308
|
**Claude Code:**
|
|
305
309
|
|
|
306
310
|
```bash
|
|
307
|
-
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
|
|
308
312
|
```
|
|
309
313
|
|
|
310
314
|
**Gemini CLI / Qwen Code:** paste the `mcpServers` block above into
|
|
@@ -345,7 +349,7 @@ Streamable HTTP bridge for exactly that case:
|
|
|
345
349
|
| --- | --- |
|
|
346
350
|
| **Endpoint** | `https://playwright-e2e-mcp.vercel.app/api/mcp` |
|
|
347
351
|
| **Transport** | MCP Streamable HTTP (`POST` JSON in, JSON or SSE out) |
|
|
348
|
-
| **Auth** |
|
|
352
|
+
| **Auth** | optional bearer token (`PW_MCP_HTTP_TOKEN`); without one only read-only tools are served |
|
|
349
353
|
| **Source** | [`api/mcp.ts`](api/mcp.ts) → [`src/http.ts`](src/http.ts) |
|
|
350
354
|
|
|
351
355
|
The bridge runs the *same* `createServer()` as the stdio transport; the SDK
|
|
@@ -353,9 +357,22 @@ serves every request with a fresh server instance, which is what a serverless
|
|
|
353
357
|
function wants. `test/http-bridge.test.mjs` drives the real Node adapter over
|
|
354
358
|
`node:http` so a broken bridge fails in CI, not in ChatGPT.
|
|
355
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
|
+
|
|
356
373
|
**Add it to ChatGPT:** Settings → Connectors → turn on **Advanced → Developer
|
|
357
374
|
mode** → *Create custom connector* → paste the endpoint above → authentication
|
|
358
|
-
**None
|
|
375
|
+
**None** (read-only tools).
|
|
359
376
|
|
|
360
377
|
Codex can also take the remote transport instead of spawning `npx`, if you'd
|
|
361
378
|
rather not ship Playwright to every machine:
|
|
@@ -365,11 +382,11 @@ codex mcp add playwright-e2e-remote --url https://playwright-e2e-mcp.vercel.app/
|
|
|
365
382
|
```
|
|
366
383
|
|
|
367
384
|
**What to expect:** `list-tests` works and reports the specs bundled with the
|
|
368
|
-
deployment.
|
|
369
|
-
`validate-selector`, `diagnose-flaky`, …) cannot download
|
|
370
|
-
serverless function, so they return their normal `NO_PLAYWRIGHT`
|
|
371
|
-
stdio install for real runs; the hosted endpoint is for discovery
|
|
372
|
-
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.
|
|
373
390
|
|
|
374
391
|
```bash
|
|
375
392
|
# verify the handshake without any client
|
|
@@ -379,17 +396,21 @@ curl -X POST https://playwright-e2e-mcp.vercel.app/api/mcp \
|
|
|
379
396
|
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
|
|
380
397
|
```
|
|
381
398
|
|
|
382
|
-
Redeploy after a change:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
npx vercel deploy --yes --prod --token="$VERCEL_TOKEN"
|
|
386
|
-
```
|
|
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.
|
|
387
402
|
|
|
388
403
|
## Configuration
|
|
389
404
|
|
|
390
405
|
| Environment variable | Default | Purpose |
|
|
391
406
|
| --- | --- | --- |
|
|
392
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 |
|
|
393
414
|
| `LOG_LEVEL` | `info` | `debug` \| `info` \| `warn` \| `error` \| `silent` |
|
|
394
415
|
| `LOG_FORMAT` | `text` | `text` or `json` (structured) |
|
|
395
416
|
|
|
@@ -430,15 +451,42 @@ Logs always go to **stderr** — stdout is reserved for the MCP protocol.
|
|
|
430
451
|
| Syntax error in a spec | `SYNTAX_ERROR` with file:line; nothing crashes; `list-tests` falls back to a source scan |
|
|
431
452
|
| MCP client disconnects | Per-request `AbortSignal` kills the run; stdin end triggers shutdown, and every tracked child tree is force-killed (`killActiveChildren`) |
|
|
432
453
|
| Disk full | `ENOSPC` detected → `DISK_FULL` with a "free space" hint; logging never throws |
|
|
433
|
-
| 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 |
|
|
434
455
|
|
|
435
456
|
## Security notes
|
|
436
457
|
|
|
437
458
|
- **No shell**: Playwright is spawned as `node <playwright/cli.js> …` with an argument
|
|
438
|
-
array — no command interpolation.
|
|
439
|
-
|
|
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.
|
|
440
487
|
- **Cleanup**: temp report/script files are written to the OS temp dir and removed;
|
|
441
|
-
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.
|
|
442
490
|
|
|
443
491
|
## Development
|
|
444
492
|
|
|
@@ -459,7 +507,7 @@ Zod v4 standard schemas; every tool declares spec tool annotations.
|
|
|
459
507
|
```bash
|
|
460
508
|
npm install
|
|
461
509
|
npm run build # tsc → dist/ (zero errors)
|
|
462
|
-
npm test # build + test/run-tests.mjs (
|
|
510
|
+
npm test # build + test/run-tests.mjs (unit tests, any Node ≥20)
|
|
463
511
|
npm run e2e # build + e2e/run.mjs: live MCP ↔ Playwright integration suite
|
|
464
512
|
```
|
|
465
513
|
|
|
@@ -469,8 +517,12 @@ discovery, multiple configs, missing install, test-file scanning), the shared to
|
|
|
469
517
|
helpers, the **trace reader** (synthetic trace.zip: error, failed action, DOM snapshot,
|
|
470
518
|
`*.network` failed-request parsing, console error/warning events), the **image diff**
|
|
471
519
|
(PNG round-trip, regions, color shift, dimension changes), the **change analyzer**
|
|
472
|
-
(selector extraction, git + mtime paths)
|
|
473
|
-
(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.
|
|
474
526
|
|
|
475
527
|
### Integration suite (`npm run e2e`)
|
|
476
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. */
|