playwright-e2e-mcp 0.1.1 → 0.1.3
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 +123 -45
- 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 +4 -2
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.3` (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.3
|
|
246
|
+
npm install -D github:trajectiq-ai/E2E#v0.1.3 @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.3/playwright-e2e-mcp-0.1.3.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.3"],
|
|
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.3
|
|
285
|
+
code --add-mcp '{"name":"playwright-e2e","command":"npx","args":["-y","github:trajectiq-ai/E2E#v0.1.3"]}'
|
|
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.3"]
|
|
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.3
|
|
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** | bearer token (`PW_MCP_HTTP_TOKEN`). The deployment at this URL requires it; a bridge started without the variable serves only the read-only tools |
|
|
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,47 +357,88 @@ 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
|
+
**This deployment is token-protected.** `https://playwright-e2e-mcp.vercel.app`
|
|
361
|
+
has `PW_MCP_HTTP_TOKEN` set, so a request without `Authorization: Bearer <token>`
|
|
362
|
+
— including a ChatGPT connector created with authentication *None* — gets
|
|
363
|
+
`401 {"error":"unauthorized"}`. Ask the operator for the token, or run your own
|
|
364
|
+
bridge (below).
|
|
365
|
+
|
|
366
|
+
**Open vs. token-protected.** With `PW_MCP_HTTP_TOKEN` set (at least 16
|
|
367
|
+
characters; use a random value) all eight tools are served to clients that send
|
|
368
|
+
`Authorization: Bearer <token>`, and every other request gets `401`. Without that
|
|
369
|
+
variable anyone who reaches the URL can call the bridge, so it degrades to
|
|
370
|
+
`list-tests` and `get-failure` in restricted mode: `list-tests` scans sources
|
|
371
|
+
instead of running `playwright test --list` (which would execute the project's
|
|
372
|
+
config), callers cannot pick another `projectRoot`, and nothing spawns a process,
|
|
373
|
+
drives a browser or writes a file. Child processes started over HTTP get only an
|
|
374
|
+
allowlisted environment. `PW_MCP_ALLOWED_HOSTS` (comma separated, `*` for any)
|
|
375
|
+
limits the accepted `Host` header; without a token and without that variable,
|
|
376
|
+
only `localhost` names and the deployment's own Vercel hostnames are accepted
|
|
377
|
+
(DNS-rebinding protection).
|
|
378
|
+
|
|
356
379
|
**Add it to ChatGPT:** Settings → Connectors → turn on **Advanced → Developer
|
|
357
|
-
mode** → *Create custom connector* → paste the endpoint above
|
|
358
|
-
**None
|
|
380
|
+
mode** → *Create custom connector* → paste the endpoint above. Authentication
|
|
381
|
+
**None** only works against a bridge with no token (read-only tools), so for this
|
|
382
|
+
deployment choose the connector's **API key** authentication and send
|
|
383
|
+
`Authorization: Bearer <token>` — or point the connector at your own deployment
|
|
384
|
+
with `PW_MCP_HTTP_TOKEN` unset.
|
|
385
|
+
|
|
386
|
+
To self-host the open, read-only variant, deploy this repo with
|
|
387
|
+
`PW_MCP_HTTP_TOKEN` unset; nothing else changes.
|
|
359
388
|
|
|
360
389
|
Codex can also take the remote transport instead of spawning `npx`, if you'd
|
|
361
|
-
rather not ship Playwright to every machine
|
|
390
|
+
rather not ship Playwright to every machine. Point it at the token through an
|
|
391
|
+
environment variable so the secret stays out of the config file:
|
|
362
392
|
|
|
363
393
|
```bash
|
|
364
|
-
|
|
394
|
+
export PW_MCP_HTTP_TOKEN=… # ask the operator for the value
|
|
395
|
+
codex mcp add playwright-e2e-remote --url https://playwright-e2e-mcp.vercel.app/api/mcp \
|
|
396
|
+
--bearer-token-env-var PW_MCP_HTTP_TOKEN
|
|
365
397
|
```
|
|
366
398
|
|
|
367
399
|
**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.
|
|
400
|
+
deployment. Even with a token, tools that spawn a browser (`run-test`,
|
|
401
|
+
`inspect-page`, `validate-selector`, `diagnose-flaky`, …) cannot download
|
|
402
|
+
Chromium in a serverless function, so they return their normal `NO_PLAYWRIGHT`
|
|
403
|
+
hint. Use the stdio install for real runs; the hosted endpoint is for discovery
|
|
404
|
+
and for clients that cannot run local processes.
|
|
373
405
|
|
|
374
406
|
```bash
|
|
375
|
-
# verify the handshake without any client
|
|
407
|
+
# verify the handshake without any client (the live deployment needs the token;
|
|
408
|
+
# drop the authorization header and it answers 401 instead)
|
|
376
409
|
curl -X POST https://playwright-e2e-mcp.vercel.app/api/mcp \
|
|
377
410
|
-H 'content-type: application/json' \
|
|
378
411
|
-H 'accept: application/json, text/event-stream' \
|
|
412
|
+
-H "authorization: Bearer $PW_MCP_HTTP_TOKEN" \
|
|
379
413
|
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
|
|
380
414
|
```
|
|
381
415
|
|
|
382
|
-
Redeploy after a change:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
npx vercel deploy --yes --prod --token="$VERCEL_TOKEN"
|
|
386
|
-
```
|
|
416
|
+
Redeploy after a change: merge to `main`. Vercel's Git integration deploys
|
|
417
|
+
every push, so there is no token to manage and no CLI step — watch the
|
|
418
|
+
`Vercel` commit status for the deployment result.
|
|
387
419
|
|
|
388
420
|
## Configuration
|
|
389
421
|
|
|
390
422
|
| Environment variable | Default | Purpose |
|
|
391
423
|
| --- | --- | --- |
|
|
392
424
|
| `PW_MCP_PROJECT_ROOT` | server cwd | Default project root for every tool |
|
|
393
|
-
| `
|
|
394
|
-
| `
|
|
425
|
+
| `PW_MCP_ALLOWED_ROOTS` | — | Extra directories a caller may pass as `projectRoot` (`:`-separated, `;` on Windows). Anything outside these and the default root is rejected |
|
|
426
|
+
| `PW_MCP_HTTP_TOKEN` | — | HTTP bridge only: bearer token (16+ characters) that unlocks all tools (see above) |
|
|
427
|
+
| `PW_MCP_ALLOWED_HOSTS` | localhost + Vercel hostnames when there is no token | HTTP bridge only: comma-separated `Host` allowlist; `*` accepts any |
|
|
428
|
+
| `PW_MCP_PASSTHROUGH_ENV` | — | HTTP bridge only: comma-separated extra variables passed to test runs (e.g. `BASE_URL`) |
|
|
429
|
+
| `PW_MCP_MAX_CHILDREN` | `4` | HTTP bridge only: how many test runs and browser probes may run at once |
|
|
430
|
+
| `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 |
|
|
431
|
+
| `LOG_LEVEL` / `MCP_LOG_LEVEL` | `info` | `debug` \| `info` \| `warn` \| `error` \| `silent`; `LOG_LEVEL` wins when both are set |
|
|
432
|
+
| `LOG_FORMAT` / `MCP_LOG_FORMAT` | `text` | `text`, or `json` (also `ndjson`) for one structured JSON object per line — ready for a log ingester |
|
|
433
|
+
|
|
434
|
+
Logs always go to **stderr** — stdout is reserved for the MCP protocol. Unrecognized values
|
|
435
|
+
fall back to the default, so a typo in `LOG_LEVEL` logs at `info` and one in `LOG_FORMAT`
|
|
436
|
+
logs as text rather than silencing the server. With `LOG_FORMAT=json` each line is a single
|
|
437
|
+
object (`time`, `level`, `message` plus context):
|
|
395
438
|
|
|
396
|
-
|
|
439
|
+
```json
|
|
440
|
+
{"time":"2026-10-07T08:00:00.000Z","level":"info","message":"server ready","name":"playwright-e2e-mcp"}
|
|
441
|
+
```
|
|
397
442
|
|
|
398
443
|
## Typical workflow
|
|
399
444
|
|
|
@@ -430,15 +475,42 @@ Logs always go to **stderr** — stdout is reserved for the MCP protocol.
|
|
|
430
475
|
| Syntax error in a spec | `SYNTAX_ERROR` with file:line; nothing crashes; `list-tests` falls back to a source scan |
|
|
431
476
|
| MCP client disconnects | Per-request `AbortSignal` kills the run; stdin end triggers shutdown, and every tracked child tree is force-killed (`killActiveChildren`) |
|
|
432
477
|
| 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
|
|
478
|
+
| 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
479
|
|
|
435
480
|
## Security notes
|
|
436
481
|
|
|
437
482
|
- **No shell**: Playwright is spawned as `node <playwright/cli.js> …` with an argument
|
|
438
|
-
array — no command interpolation.
|
|
439
|
-
|
|
483
|
+
array — no command interpolation. Extra `args` are limited to an allowlist of
|
|
484
|
+
Playwright flags, each with a checked value (`--flag=value`), so a caller cannot
|
|
485
|
+
swap in another `--config` or `--output` or hand git an option through
|
|
486
|
+
`--only-changed`. Paths with a segment starting with `-` are rejected, so a test
|
|
487
|
+
file name cannot be read as a flag.
|
|
488
|
+
- **Path sandbox**: user paths must stay inside the project root, checked lexically
|
|
489
|
+
and again with symlinks resolved. A caller-supplied `projectRoot` must sit inside
|
|
490
|
+
`PW_MCP_PROJECT_ROOT` (or a `PW_MCP_ALLOWED_ROOTS` entry).
|
|
491
|
+
- **Generated code**: `generate-e2e-test` only writes `*.spec.*` / `*.test.*` files,
|
|
492
|
+
only overwrites specs whose header it wrote itself, never writes through a symlink,
|
|
493
|
+
and escapes every value it puts into strings or comments.
|
|
494
|
+
- **HTTP bridge**: read-only tools unless `PW_MCP_HTTP_TOKEN` is set; child processes
|
|
495
|
+
started over HTTP get only an allowlisted environment (`PATH`, `HOME`, temp dirs,
|
|
496
|
+
locale, `CI`, `PLAYWRIGHT_*`, `npm_config_*` without embedded credentials, plus
|
|
497
|
+
anything in `PW_MCP_PASSTHROUGH_ENV`), at most `PW_MCP_MAX_CHILDREN` run at once
|
|
498
|
+
(a disconnected client frees its slot immediately), and internal error messages
|
|
499
|
+
are not returned to clients. The allowlist only covers the child's own
|
|
500
|
+
environment: test code runs as the same OS user, so on Linux it could still read
|
|
501
|
+
the server's startup environment from `/proc`. That is why only token holders
|
|
502
|
+
can run project code; keep other secrets out of the bridge's environment, or run
|
|
503
|
+
it under a separate user.
|
|
504
|
+
- **SSRF guard**: on the HTTP bridge (or with `PW_MCP_BLOCK_PRIVATE_URLS=1`) the URL tools
|
|
505
|
+
refuse hosts that resolve to loopback, private, link-local/metadata or reserved
|
|
506
|
+
addresses, and route the browser through a local proxy that applies the same check
|
|
507
|
+
to every redirect hop and subresource; IPv6 forms that embed an IPv4 address
|
|
508
|
+
(mapped, NAT64, 6to4, Teredo) are blocked too, and WebRTC UDP is disabled so a
|
|
509
|
+
page cannot reach the network around the proxy. It is off for stdio by default because
|
|
510
|
+
opening `http://localhost` dev servers is what these tools are for.
|
|
440
511
|
- **Cleanup**: temp report/script files are written to the OS temp dir and removed;
|
|
441
|
-
child processes are tracked and killed on shutdown.
|
|
512
|
+
child processes are tracked and killed on shutdown. Trace decompression has one
|
|
513
|
+
size budget per archive, and PNGs are capped at 16,384 px per side and 50 M pixels.
|
|
442
514
|
|
|
443
515
|
## Development
|
|
444
516
|
|
|
@@ -459,8 +531,10 @@ Zod v4 standard schemas; every tool declares spec tool annotations.
|
|
|
459
531
|
```bash
|
|
460
532
|
npm install
|
|
461
533
|
npm run build # tsc → dist/ (zero errors)
|
|
462
|
-
npm test # build + test/run-tests.mjs (
|
|
534
|
+
npm test # build + test/run-tests.mjs (unit tests, any Node ≥20)
|
|
463
535
|
npm run e2e # build + e2e/run.mjs: live MCP ↔ Playwright integration suite
|
|
536
|
+
npm run mcpb # build + bundle dist/ into playwright-e2e-mcp-<version>.mcpb (+ .sha256)
|
|
537
|
+
npm run mcpb:smoke # ...then install that bundle in a temp dir, launch it and assert the handshake
|
|
464
538
|
```
|
|
465
539
|
|
|
466
540
|
Tests cover the report parser (sample Playwright JSON, trace attachments), path utils
|
|
@@ -469,8 +543,12 @@ discovery, multiple configs, missing install, test-file scanning), the shared to
|
|
|
469
543
|
helpers, the **trace reader** (synthetic trace.zip: error, failed action, DOM snapshot,
|
|
470
544
|
`*.network` failed-request parsing, console error/warning events), the **image diff**
|
|
471
545
|
(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)
|
|
546
|
+
(selector extraction, git + mtime paths), the **flaky verdict logic**
|
|
547
|
+
(failure signatures, CONSISTENTLY FAILING / FLAKY / NOT REPRODUCING / NO TESTS RAN),
|
|
548
|
+
the **HTTP bridge** (token, restricted mode, Host allowlist), the **security
|
|
549
|
+
regressions** (sandbox escapes, argument smuggling, symlink writes, code injection,
|
|
550
|
+
SSRF ranges, env scrubbing, decode limits), the `.mcpb` manifest, the `.mcpb` zip
|
|
551
|
+
extractor, and the MCP config files.
|
|
474
552
|
|
|
475
553
|
### Integration suite (`npm run e2e`)
|
|
476
554
|
|
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. */
|