playwright 1.56.0-alpha-2025-10-01 → 1.56.0-alpha-2025-10-02

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.
@@ -23,7 +23,6 @@ tools:
23
23
  - playwright-test/browser_verify_text_visible
24
24
  - playwright-test/browser_verify_value
25
25
  - playwright-test/browser_wait_for
26
- - playwright-test/generator_log_step
27
26
  - playwright-test/generator_read_log
28
27
  - playwright-test/generator_setup_page
29
28
  - playwright-test/generator_write_test
@@ -38,14 +37,15 @@ application behavior.
38
37
  - Run the `generator_setup_page` tool to set up page for the scenario
39
38
  - For each step and verification in the scenario, do the following:
40
39
  - Use Playwright tool to manually execute it in real-time.
41
- - Immediately after each tool execution, log the step via running `generator_log_step` tool.
40
+ - Use the step description as the intent for each Playwright tool call.
42
41
  - Retrieve generator log via `generator_read_log`
43
42
  - Immediately after reading the test log, invoke `generator_write_test` with the generated source code
44
43
  - File should contain single test
45
44
  - File name must be fs-friendly scenario name
46
45
  - Test must be placed in a describe matching the top-level test plan item
47
46
  - Test title must match the scenario name
48
- - Includes a comment with the step text before each step execution
47
+ - Includes a comment with the step text before each step execution. Do not duplicate comments if step requires
48
+ multiple actions.
49
49
 
50
50
  <example-generation>
51
51
  For following plan:
@@ -144,11 +144,19 @@ class Context {
144
144
  const name = await this.outputFile((0, import_utils.dateAsFileName)("webm"), { origin: "code", reason: "Saving video" });
145
145
  await import_fs.default.promises.mkdir(import_path.default.dirname(name), { recursive: true });
146
146
  const p = await video.path();
147
- try {
148
- if (import_fs.default.existsSync(p))
147
+ if (import_fs.default.existsSync(p)) {
148
+ try {
149
149
  await import_fs.default.promises.rename(p, name);
150
- } catch (e) {
151
- (0, import_log.logUnhandledError)(e);
150
+ } catch (e) {
151
+ if (e.code !== "EXDEV")
152
+ (0, import_log.logUnhandledError)(e);
153
+ try {
154
+ await import_fs.default.promises.copyFile(p, name);
155
+ await import_fs.default.promises.unlink(p);
156
+ } catch (e2) {
157
+ (0, import_log.logUnhandledError)(e2);
158
+ }
159
+ }
152
160
  }
153
161
  }
154
162
  });
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  var response_exports = {};
20
20
  __export(response_exports, {
21
21
  Response: () => Response,
22
+ parseResponse: () => parseResponse,
22
23
  requestDebug: () => requestDebug
23
24
  });
24
25
  module.exports = __toCommonJS(response_exports);
@@ -179,8 +180,49 @@ function trim(text, maxLength) {
179
180
  return text;
180
181
  return text.slice(0, maxLength) + "...";
181
182
  }
183
+ function parseSections(text) {
184
+ const sections = /* @__PURE__ */ new Map();
185
+ const sectionHeaders = text.split(/^### /m).slice(1);
186
+ for (const section of sectionHeaders) {
187
+ const firstNewlineIndex = section.indexOf("\n");
188
+ if (firstNewlineIndex === -1)
189
+ continue;
190
+ const sectionName = section.substring(0, firstNewlineIndex);
191
+ const sectionContent = section.substring(firstNewlineIndex + 1).trim();
192
+ sections.set(sectionName, sectionContent);
193
+ }
194
+ return sections;
195
+ }
196
+ function parseResponse(response) {
197
+ if (response.content?.[0].type !== "text")
198
+ return void 0;
199
+ const text = response.content[0].text;
200
+ const sections = parseSections(text);
201
+ const result = sections.get("Result");
202
+ const code = sections.get("Ran Playwright code");
203
+ const tabs = sections.get("Open tabs");
204
+ const pageState = sections.get("Page state");
205
+ const consoleMessages = sections.get("New console messages");
206
+ const modalState = sections.get("Modal state");
207
+ const downloads = sections.get("Downloads");
208
+ const codeNoFrame = code?.replace(/^```js\n/, "").replace(/\n```$/, "");
209
+ const isError = response.isError;
210
+ const attachments = response.content.slice(1);
211
+ return {
212
+ result,
213
+ code: codeNoFrame,
214
+ tabs,
215
+ pageState,
216
+ consoleMessages,
217
+ modalState,
218
+ downloads,
219
+ isError,
220
+ attachments
221
+ };
222
+ }
182
223
  // Annotate the CommonJS export names for ESM import in node:
183
224
  0 && (module.exports = {
184
225
  Response,
226
+ parseResponse,
185
227
  requestDebug
186
228
  });
@@ -30,7 +30,7 @@ const close = (0, import_tool.defineTool)({
30
30
  title: "Close browser",
31
31
  description: "Close the page",
32
32
  inputSchema: import_bundle.z.object({}),
33
- type: "readOnly"
33
+ type: "action"
34
34
  },
35
35
  handle: async (context, params, response) => {
36
36
  await context.closeBrowserContext();
@@ -48,7 +48,7 @@ const resize = (0, import_tool.defineTabTool)({
48
48
  width: import_bundle.z.number().describe("Width of the browser window"),
49
49
  height: import_bundle.z.number().describe("Height of the browser window")
50
50
  }),
51
- type: "readOnly"
51
+ type: "action"
52
52
  },
53
53
  handle: async (tab, params, response) => {
54
54
  response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`);
@@ -34,7 +34,7 @@ const handleDialog = (0, import_tool.defineTabTool)({
34
34
  accept: import_bundle.z.boolean().describe("Whether to accept the dialog."),
35
35
  promptText: import_bundle.z.string().optional().describe("The text of the prompt in case of a prompt dialog.")
36
36
  }),
37
- type: "destructive"
37
+ type: "action"
38
38
  },
39
39
  handle: async (tab, params, response) => {
40
40
  response.setIncludeSnapshot();
@@ -47,7 +47,7 @@ const evaluate = (0, import_tool.defineTabTool)({
47
47
  title: "Evaluate JavaScript",
48
48
  description: "Evaluate JavaScript expression on page or element",
49
49
  inputSchema: evaluateSchema,
50
- type: "destructive"
50
+ type: "action"
51
51
  },
52
52
  handle: async (tab, params, response) => {
53
53
  response.setIncludeSnapshot();
@@ -33,7 +33,7 @@ const uploadFile = (0, import_tool.defineTabTool)({
33
33
  inputSchema: import_bundle.z.object({
34
34
  paths: import_bundle.z.array(import_bundle.z.string()).optional().describe("The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled.")
35
35
  }),
36
- type: "destructive"
36
+ type: "action"
37
37
  },
38
38
  handle: async (tab, params, response) => {
39
39
  response.setIncludeSnapshot();
@@ -49,7 +49,7 @@ const fillForm = (0, import_tool.defineTabTool)({
49
49
  value: import_bundle.z.string().describe("Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.")
50
50
  })).describe("Fields to fill in")
51
51
  }),
52
- type: "destructive"
52
+ type: "input"
53
53
  },
54
54
  handle: async (tab, params, response) => {
55
55
  for (const field of params.fields) {
@@ -42,7 +42,7 @@ const install = (0, import_tool.defineTool)({
42
42
  title: "Install the browser specified in the config",
43
43
  description: "Install the browser specified in the config. Call this if you get an error about the browser not being installed.",
44
44
  inputSchema: import_bundle.z.object({}),
45
- type: "destructive"
45
+ type: "action"
46
46
  },
47
47
  handle: async (context, params, response) => {
48
48
  const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? "chrome";
@@ -34,7 +34,7 @@ const pressKey = (0, import_tool.defineTabTool)({
34
34
  inputSchema: import_bundle.z.object({
35
35
  key: import_bundle.z.string().describe("Name of the key to press or a character to generate, such as `ArrowLeft` or `a`")
36
36
  }),
37
- type: "destructive"
37
+ type: "input"
38
38
  },
39
39
  handle: async (tab, params, response) => {
40
40
  response.setIncludeSnapshot();
@@ -57,7 +57,7 @@ const type = (0, import_tool.defineTabTool)({
57
57
  title: "Type text",
58
58
  description: "Type text into editable element",
59
59
  inputSchema: typeSchema,
60
- type: "destructive"
60
+ type: "input"
61
61
  },
62
62
  handle: async (tab, params, response) => {
63
63
  const locator = await tab.refLocator(params);
@@ -36,7 +36,7 @@ const mouseMove = (0, import_tool.defineTabTool)({
36
36
  x: import_bundle.z.number().describe("X coordinate"),
37
37
  y: import_bundle.z.number().describe("Y coordinate")
38
38
  }),
39
- type: "readOnly"
39
+ type: "input"
40
40
  },
41
41
  handle: async (tab, params, response) => {
42
42
  response.addCode(`// Move mouse to (${params.x}, ${params.y})`);
@@ -56,7 +56,7 @@ const mouseClick = (0, import_tool.defineTabTool)({
56
56
  x: import_bundle.z.number().describe("X coordinate"),
57
57
  y: import_bundle.z.number().describe("Y coordinate")
58
58
  }),
59
- type: "destructive"
59
+ type: "input"
60
60
  },
61
61
  handle: async (tab, params, response) => {
62
62
  response.setIncludeSnapshot();
@@ -83,7 +83,7 @@ const mouseDrag = (0, import_tool.defineTabTool)({
83
83
  endX: import_bundle.z.number().describe("End X coordinate"),
84
84
  endY: import_bundle.z.number().describe("End Y coordinate")
85
85
  }),
86
- type: "destructive"
86
+ type: "input"
87
87
  },
88
88
  handle: async (tab, params, response) => {
89
89
  response.setIncludeSnapshot();
@@ -32,7 +32,7 @@ const navigate = (0, import_tool.defineTool)({
32
32
  inputSchema: import_bundle.z.object({
33
33
  url: import_bundle.z.string().describe("The URL to navigate to")
34
34
  }),
35
- type: "destructive"
35
+ type: "action"
36
36
  },
37
37
  handle: async (context, params, response) => {
38
38
  const tab = await context.ensureTab();
@@ -48,7 +48,7 @@ const goBack = (0, import_tool.defineTabTool)({
48
48
  title: "Go back",
49
49
  description: "Go back to the previous page",
50
50
  inputSchema: import_bundle.z.object({}),
51
- type: "readOnly"
51
+ type: "action"
52
52
  },
53
53
  handle: async (tab, params, response) => {
54
54
  await tab.page.goBack();
@@ -41,16 +41,6 @@ const screenshotSchema = import_bundle.z.object({
41
41
  element: import_bundle.z.string().optional().describe("Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too."),
42
42
  ref: import_bundle.z.string().optional().describe("Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too."),
43
43
  fullPage: import_bundle.z.boolean().optional().describe("When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.")
44
- }).refine((data) => {
45
- return !!data.element === !!data.ref;
46
- }, {
47
- message: "Both element and ref must be provided or neither.",
48
- path: ["ref", "element"]
49
- }).refine((data) => {
50
- return !(data.fullPage && (data.element || data.ref));
51
- }, {
52
- message: "fullPage cannot be used with element screenshots.",
53
- path: ["fullPage"]
54
44
  });
55
45
  const screenshot = (0, import_tool.defineTabTool)({
56
46
  capability: "core",
@@ -62,6 +52,10 @@ const screenshot = (0, import_tool.defineTabTool)({
62
52
  type: "readOnly"
63
53
  },
64
54
  handle: async (tab, params, response) => {
55
+ if (!!params.element !== !!params.ref)
56
+ throw new Error("Both element and ref must be provided or neither.");
57
+ if (params.fullPage && params.ref)
58
+ throw new Error("fullPage cannot be used with element screenshots.");
65
59
  const fileType = params.type || "png";
66
60
  const fileName = await tab.context.outputFile(params.filename ?? (0, import_utils.dateAsFileName)(fileType), { origin: "llm", reason: "Saving screenshot" });
67
61
  const options = {
@@ -66,7 +66,7 @@ const click = (0, import_tool.defineTabTool)({
66
66
  title: "Click",
67
67
  description: "Perform click on a web page",
68
68
  inputSchema: clickSchema,
69
- type: "destructive"
69
+ type: "input"
70
70
  },
71
71
  handle: async (tab, params, response) => {
72
72
  response.setIncludeSnapshot();
@@ -101,7 +101,7 @@ const drag = (0, import_tool.defineTabTool)({
101
101
  endElement: import_bundle.z.string().describe("Human-readable target element description used to obtain the permission to interact with the element"),
102
102
  endRef: import_bundle.z.string().describe("Exact target element reference from the page snapshot")
103
103
  }),
104
- type: "destructive"
104
+ type: "input"
105
105
  },
106
106
  handle: async (tab, params, response) => {
107
107
  response.setIncludeSnapshot();
@@ -122,7 +122,7 @@ const hover = (0, import_tool.defineTabTool)({
122
122
  title: "Hover mouse",
123
123
  description: "Hover over element on page",
124
124
  inputSchema: elementSchema,
125
- type: "readOnly"
125
+ type: "input"
126
126
  },
127
127
  handle: async (tab, params, response) => {
128
128
  response.setIncludeSnapshot();
@@ -143,7 +143,7 @@ const selectOption = (0, import_tool.defineTabTool)({
143
143
  title: "Select option",
144
144
  description: "Select an option in a dropdown",
145
145
  inputSchema: selectOptionSchema,
146
- type: "destructive"
146
+ type: "input"
147
147
  },
148
148
  handle: async (tab, params, response) => {
149
149
  response.setIncludeSnapshot();
@@ -33,7 +33,7 @@ const browserTabs = (0, import_tool.defineTool)({
33
33
  action: import_bundle.z.enum(["list", "new", "close", "select"]).describe("Operation to perform"),
34
34
  index: import_bundle.z.number().optional().describe("Tab index, used for close/select. If omitted for close, current tab is closed.")
35
35
  }),
36
- type: "destructive"
36
+ type: "action"
37
37
  },
38
38
  handle: async (context, params, response) => {
39
39
  switch (params.action) {
@@ -45,7 +45,7 @@ const verifyElement = (0, import_tool.defineTabTool)({
45
45
  role: import_bundle.z.string().describe('ROLE of the element. Can be found in the snapshot like this: `- {ROLE} "Accessible Name":`'),
46
46
  accessibleName: import_bundle.z.string().describe('ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: `- role "{ACCESSIBLE_NAME}"`')
47
47
  }),
48
- type: "readOnly"
48
+ type: "assertion"
49
49
  },
50
50
  handle: async (tab, params, response) => {
51
51
  const locator = tab.page.getByRole(params.role, { name: params.accessibleName });
@@ -66,7 +66,7 @@ const verifyText = (0, import_tool.defineTabTool)({
66
66
  inputSchema: import_bundle.z.object({
67
67
  text: import_bundle.z.string().describe('TEXT to verify. Can be found in the snapshot like this: `- role "Accessible Name": {TEXT}` or like this: `- text: {TEXT}`')
68
68
  }),
69
- type: "readOnly"
69
+ type: "assertion"
70
70
  },
71
71
  handle: async (tab, params, response) => {
72
72
  const locator = tab.page.getByText(params.text).filter({ visible: true });
@@ -89,7 +89,7 @@ const verifyList = (0, import_tool.defineTabTool)({
89
89
  ref: import_bundle.z.string().describe("Exact target element reference that points to the list"),
90
90
  items: import_bundle.z.array(import_bundle.z.string()).describe("Items to verify")
91
91
  }),
92
- type: "readOnly"
92
+ type: "assertion"
93
93
  },
94
94
  handle: async (tab, params, response) => {
95
95
  const locator = await tab.refLocator({ ref: params.ref, element: params.element });
@@ -122,7 +122,7 @@ const verifyValue = (0, import_tool.defineTabTool)({
122
122
  ref: import_bundle.z.string().describe("Exact target element reference that points to the element"),
123
123
  value: import_bundle.z.string().describe('Value to verify. For checkbox, use "true" or "false".')
124
124
  }),
125
- type: "readOnly"
125
+ type: "assertion"
126
126
  },
127
127
  handle: async (tab, params, response) => {
128
128
  const locator = await tab.refLocator({ ref: params.ref, element: params.element });
@@ -34,7 +34,7 @@ const wait = (0, import_tool.defineTool)({
34
34
  text: import_bundle.z.string().optional().describe("The text to wait for"),
35
35
  textGone: import_bundle.z.string().optional().describe("The text to wait for to disappear")
36
36
  }),
37
- type: "readOnly"
37
+ type: "assertion"
38
38
  },
39
39
  handle: async (context, params, response) => {
40
40
  if (!params.text && !params.textGone && !params.time)
@@ -39,9 +39,8 @@ var import_browserContextFactory = require("./browser/browserContextFactory");
39
39
  var import_proxyBackend = require("./sdk/proxyBackend");
40
40
  var import_browserServerBackend = require("./browser/browserServerBackend");
41
41
  var import_extensionContextFactory = require("./extension/extensionContextFactory");
42
- var import_host = require("./vscode/host");
43
42
  function decorateCommand(command, version) {
44
- command.option("--allowed-hosts <hosts...>", "comma-separated list of hosts this server is allowed to serve from. Defaults to the host the server is bound to. Pass '*' to disable the host check.", import_config.commaSeparatedList).option("--allowed-origins <origins>", "semicolon-separated list of origins to allow the browser to request. Default is to allow all.", import_config.semicolonSeparatedList).option("--blocked-origins <origins>", "semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.", import_config.semicolonSeparatedList).option("--block-service-workers", "block service workers").option("--browser <browser>", "browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.").option("--caps <caps>", "comma-separated list of additional capabilities to enable, possible values: vision, pdf.", import_config.commaSeparatedList).option("--cdp-endpoint <endpoint>", "CDP endpoint to connect to.").option("--cdp-header <headers...>", "CDP headers to send with the connect request, multiple can be specified.", import_config.headerParser).option("--config <path>", "path to the configuration file.").option("--device <device>", 'device to emulate, for example: "iPhone 15"').option("--executable-path <path>", "path to the browser executable.").option("--extension", 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').option("--grant-permissions <permissions...>", 'List of permissions to grant to the browser context, for example "geolocation", "clipboard-read", "clipboard-write".', import_config.commaSeparatedList).option("--headless", "run browser in headless mode, headed by default").option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.").option("--ignore-https-errors", "ignore https errors").option("--init-script <path...>", "path to JavaScript file to add as an initialization script. The script will be evaluated in every page before any of the page's scripts. Can be specified multiple times.").option("--isolated", "keep the browser profile in memory, do not save it to disk.").option("--image-responses <mode>", 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".').option("--no-sandbox", "disable the sandbox for all process types that are normally sandboxed.").option("--output-dir <path>", "path to the directory for output files.").option("--port <port>", "port to listen on for SSE transport.").option("--proxy-bypass <bypass>", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option("--proxy-server <proxy>", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option("--save-session", "Whether to save the Playwright MCP session into the output directory.").option("--save-trace", "Whether to save the Playwright Trace of the session into the output directory.").option("--save-video <size>", 'Whether to save the video of the session into the output directory. For example "--save-video=800x600"', import_config.resolutionParser.bind(null, "--save-video")).option("--secrets <path>", "path to a file containing secrets in the dotenv format", import_config.dotenvFileLoader).option("--shared-browser-context", "reuse the same browser context between all connected HTTP clients.").option("--storage-state <path>", "path to the storage state file for isolated sessions.").option("--timeout-action <timeout>", "specify action timeout in milliseconds, defaults to 5000ms", import_config.numberParser).option("--timeout-navigation <timeout>", "specify navigation timeout in milliseconds, defaults to 60000ms", import_config.numberParser).option("--user-agent <ua string>", "specify user agent string").option("--user-data-dir <path>", "path to the user data directory. If not specified, a temporary directory will be created.").option("--viewport-size <size>", 'specify browser viewport size in pixels, for example "1280x720"', import_config.resolutionParser.bind(null, "--viewport-size")).addOption(new import_utilsBundle.ProgramOption("--connect-tool", "Allow to switch between different browser connection methods.").hideHelp()).addOption(new import_utilsBundle.ProgramOption("--vscode", "VS Code tools.").hideHelp()).addOption(new import_utilsBundle.ProgramOption("--vision", "Legacy option, use --caps=vision instead").hideHelp()).action(async (options) => {
43
+ command.option("--allowed-hosts <hosts...>", "comma-separated list of hosts this server is allowed to serve from. Defaults to the host the server is bound to. Pass '*' to disable the host check.", import_config.commaSeparatedList).option("--allowed-origins <origins>", "semicolon-separated list of origins to allow the browser to request. Default is to allow all.", import_config.semicolonSeparatedList).option("--blocked-origins <origins>", "semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.", import_config.semicolonSeparatedList).option("--block-service-workers", "block service workers").option("--browser <browser>", "browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.").option("--caps <caps>", "comma-separated list of additional capabilities to enable, possible values: vision, pdf.", import_config.commaSeparatedList).option("--cdp-endpoint <endpoint>", "CDP endpoint to connect to.").option("--cdp-header <headers...>", "CDP headers to send with the connect request, multiple can be specified.", import_config.headerParser).option("--config <path>", "path to the configuration file.").option("--device <device>", 'device to emulate, for example: "iPhone 15"').option("--executable-path <path>", "path to the browser executable.").option("--extension", 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').option("--grant-permissions <permissions...>", 'List of permissions to grant to the browser context, for example "geolocation", "clipboard-read", "clipboard-write".', import_config.commaSeparatedList).option("--headless", "run browser in headless mode, headed by default").option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.").option("--ignore-https-errors", "ignore https errors").option("--init-script <path...>", "path to JavaScript file to add as an initialization script. The script will be evaluated in every page before any of the page's scripts. Can be specified multiple times.").option("--isolated", "keep the browser profile in memory, do not save it to disk.").option("--image-responses <mode>", 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".').option("--no-sandbox", "disable the sandbox for all process types that are normally sandboxed.").option("--output-dir <path>", "path to the directory for output files.").option("--port <port>", "port to listen on for SSE transport.").option("--proxy-bypass <bypass>", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option("--proxy-server <proxy>", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option("--save-session", "Whether to save the Playwright MCP session into the output directory.").option("--save-trace", "Whether to save the Playwright Trace of the session into the output directory.").option("--save-video <size>", 'Whether to save the video of the session into the output directory. For example "--save-video=800x600"', import_config.resolutionParser.bind(null, "--save-video")).option("--secrets <path>", "path to a file containing secrets in the dotenv format", import_config.dotenvFileLoader).option("--shared-browser-context", "reuse the same browser context between all connected HTTP clients.").option("--storage-state <path>", "path to the storage state file for isolated sessions.").option("--timeout-action <timeout>", "specify action timeout in milliseconds, defaults to 5000ms", import_config.numberParser).option("--timeout-navigation <timeout>", "specify navigation timeout in milliseconds, defaults to 60000ms", import_config.numberParser).option("--user-agent <ua string>", "specify user agent string").option("--user-data-dir <path>", "path to the user data directory. If not specified, a temporary directory will be created.").option("--viewport-size <size>", 'specify browser viewport size in pixels, for example "1280x720"', import_config.resolutionParser.bind(null, "--viewport-size")).addOption(new import_utilsBundle.ProgramOption("--connect-tool", "Allow to switch between different browser connection methods.").hideHelp()).addOption(new import_utilsBundle.ProgramOption("--vision", "Legacy option, use --caps=vision instead").hideHelp()).action(async (options) => {
45
44
  (0, import_watchdog.setupExitWatchdog)();
46
45
  if (options.vision) {
47
46
  console.error("The --vision option is deprecated, use --caps=vision instead");
@@ -60,10 +59,6 @@ function decorateCommand(command, version) {
60
59
  await mcpServer.start(serverBackendFactory, config.server);
61
60
  return;
62
61
  }
63
- if (options.vscode) {
64
- await (0, import_host.runVSCodeTools)(config);
65
- return;
66
- }
67
62
  if (options.connectTool) {
68
63
  const providers = [
69
64
  {
@@ -34,6 +34,7 @@ __export(bundle_exports, {
34
34
  ListToolsRequestSchema: () => ListToolsRequestSchema,
35
35
  PingRequestSchema: () => PingRequestSchema,
36
36
  ProgressNotificationSchema: () => ProgressNotificationSchema,
37
+ SSEClientTransport: () => SSEClientTransport,
37
38
  SSEServerTransport: () => SSEServerTransport,
38
39
  Server: () => Server,
39
40
  StdioClientTransport: () => StdioClientTransport,
@@ -48,6 +49,7 @@ var bundle = __toESM(require("../../mcpBundleImpl"));
48
49
  const zodToJsonSchema = bundle.zodToJsonSchema;
49
50
  const Client = bundle.Client;
50
51
  const Server = bundle.Server;
52
+ const SSEClientTransport = bundle.SSEClientTransport;
51
53
  const SSEServerTransport = bundle.SSEServerTransport;
52
54
  const StdioClientTransport = bundle.StdioClientTransport;
53
55
  const StdioServerTransport = bundle.StdioServerTransport;
@@ -67,6 +69,7 @@ const z = bundle.z;
67
69
  ListToolsRequestSchema,
68
70
  PingRequestSchema,
69
71
  ProgressNotificationSchema,
72
+ SSEClientTransport,
70
73
  SSEServerTransport,
71
74
  Server,
72
75
  StdioClientTransport,
@@ -39,60 +39,47 @@ var import_tool = require("./tool");
39
39
  var mcpBundle = __toESM(require("./bundle"));
40
40
  var mcpServer = __toESM(require("./server"));
41
41
  var mcpHttp = __toESM(require("./http"));
42
- var import_server = require("./server");
43
42
  const mdbDebug = (0, import_utilsBundle.debug)("pw:mcp:mdb");
44
43
  const errorsDebug = (0, import_utilsBundle.debug)("pw:mcp:errors");
45
44
  const z = mcpBundle.z;
46
45
  class MDBBackend {
47
- constructor(topLevelBackend, allowedOnPause) {
48
- this._stack = [];
46
+ constructor(mainBackend) {
49
47
  this._progress = [];
50
- this._topLevelBackend = topLevelBackend;
51
- this._allowedOnPause = allowedOnPause;
48
+ this._mainBackend = mainBackend;
49
+ this._progressCallback = (params) => {
50
+ if (params.message)
51
+ this._progress.push({ type: "text", text: params.message });
52
+ };
52
53
  }
53
54
  async initialize(server, clientInfo) {
54
- if (!this._clientInfo)
55
+ if (!this._clientInfo) {
55
56
  this._clientInfo = clientInfo;
57
+ await this._mainBackend.initialize?.(server, clientInfo);
58
+ }
56
59
  }
57
60
  async listTools() {
58
- const client = await this._client();
59
- const response = await client.listTools();
60
- return response.tools;
61
+ return await this._mainBackend.listTools();
61
62
  }
62
63
  async callTool(name, args) {
63
- await this._client();
64
- if (name === pushToolsSchema.name)
65
- return await this._pushTools(pushToolsSchema.inputSchema.parse(args || {}));
66
- const interruptPromise = new import_utils.ManualPromise();
67
- this._interruptPromise = interruptPromise;
68
- if (!this._allowedOnPause.includes(name)) {
69
- for (let i = 0; i < this._stack.length - 1; i++) {
70
- if (this._stack[i].toolNames.includes(name))
71
- break;
72
- await this._stack[i].client.close().catch(errorsDebug);
73
- this._stack.shift();
74
- }
64
+ if (name === pushToolsSchema.name) {
65
+ await this._createOnPauseClient(pushToolsSchema.inputSchema.parse(args || {}));
66
+ return { content: [{ type: "text", text: "Tools pushed" }] };
75
67
  }
76
- let entry;
77
- for (let i = 0; i < this._stack.length; i++) {
78
- if (this._stack[i].toolNames.includes(name)) {
79
- entry = this._stack[i];
80
- break;
81
- }
82
- mdbDebug("popping client from stack for ", name);
68
+ if (this._onPauseClient?.tools.find((tool) => tool.name === name)) {
69
+ const result2 = await this._onPauseClient.client.callTool({
70
+ name,
71
+ arguments: args
72
+ });
73
+ await this._mainBackend.afterCallTool?.(name, args, result2);
74
+ return result2;
83
75
  }
84
- if (!entry)
85
- throw new Error(`Tool ${name} not found in the tool stack`);
86
- const client = entry.client;
76
+ await this._onPauseClient?.transport.terminateSession().catch(errorsDebug);
77
+ await this._onPauseClient?.client.close().catch(errorsDebug);
78
+ this._onPauseClient = void 0;
87
79
  const resultPromise = new import_utils.ManualPromise();
88
- entry.resultPromise = resultPromise;
89
- client.callTool({
90
- name,
91
- arguments: args,
92
- _meta: {
93
- progressToken: name + "@" + (0, import_utils.createGuid)().slice(0, 8)
94
- }
95
- }).then((result2) => {
80
+ const interruptPromise = new import_utils.ManualPromise();
81
+ this._interruptPromise = interruptPromise;
82
+ this._mainBackend.callTool(name, args, this._progressCallback).then((result2) => {
96
83
  resultPromise.resolve(result2);
97
84
  }).catch((e) => {
98
85
  resultPromise.resolve({ content: [{ type: "text", text: String(e) }], isError: true });
@@ -106,21 +93,19 @@ class MDBBackend {
106
93
  this._progress.length = 0;
107
94
  return result;
108
95
  }
109
- async _client() {
110
- if (!this._stack.length) {
111
- const transport = await (0, import_server.wrapInProcess)(this._topLevelBackend);
112
- await this._pushClient(transport);
113
- }
114
- return this._stack[0].client;
115
- }
116
- async _pushTools(params) {
117
- mdbDebug("pushing tools to the stack", params.mcpUrl);
118
- const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(params.mcpUrl));
119
- await this._pushClient(transport, params.introMessage);
120
- return { content: [{ type: "text", text: "Tools pushed" }] };
96
+ async _createOnPauseClient(params) {
97
+ if (this._onPauseClient)
98
+ await this._onPauseClient.client.close().catch(errorsDebug);
99
+ this._onPauseClient = await this._createClient(params.mcpUrl);
100
+ this._interruptPromise?.resolve({
101
+ content: [{
102
+ type: "text",
103
+ text: params.introMessage || ""
104
+ }]
105
+ });
106
+ this._interruptPromise = void 0;
121
107
  }
122
- async _pushClient(transport, introMessage) {
123
- mdbDebug("pushing client to the stack");
108
+ async _createClient(url) {
124
109
  const client = new mcpBundle.Client({ name: "Interrupting client", version: "0.0.0" }, { capabilities: { roots: {} } });
125
110
  client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._clientInfo?.roots || [] }));
126
111
  client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
@@ -131,20 +116,10 @@ class MDBBackend {
131
116
  this._progress.push({ type: "text", text: message });
132
117
  }
133
118
  });
119
+ const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(url));
134
120
  await client.connect(transport);
135
- mdbDebug("connected to the new client");
136
121
  const { tools } = await client.listTools();
137
- this._stack.unshift({ client, toolNames: tools.map((tool) => tool.name), resultPromise: void 0 });
138
- mdbDebug("new tools added to the stack:", tools.map((tool) => tool.name));
139
- mdbDebug("interrupting current call:", !!this._interruptPromise);
140
- this._interruptPromise?.resolve({
141
- content: [{
142
- type: "text",
143
- text: introMessage || ""
144
- }]
145
- });
146
- this._interruptPromise = void 0;
147
- return { content: [{ type: "text", text: "Tools pushed" }] };
122
+ return { client, tools, transport };
148
123
  }
149
124
  }
150
125
  const pushToolsSchema = (0, import_tool.defineToolSchema)({
@@ -157,8 +132,8 @@ const pushToolsSchema = (0, import_tool.defineToolSchema)({
157
132
  }),
158
133
  type: "readOnly"
159
134
  });
160
- async function runMainBackend(backendFactory, allowedOnPause, options) {
161
- const mdbBackend = new MDBBackend(backendFactory.create(), allowedOnPause);
135
+ async function runMainBackend(backendFactory, options) {
136
+ const mdbBackend = new MDBBackend(backendFactory.create());
162
137
  const factory = {
163
138
  ...backendFactory,
164
139
  create: () => mdbBackend
@@ -23,15 +23,20 @@ __export(tool_exports, {
23
23
  });
24
24
  module.exports = __toCommonJS(tool_exports);
25
25
  var import_bundle = require("../sdk/bundle");
26
- function toMcpTool(tool) {
26
+ const typesWithIntent = ["action", "assertion", "input"];
27
+ function toMcpTool(tool, options) {
28
+ const inputSchema = options?.addIntent && typesWithIntent.includes(tool.type) ? tool.inputSchema.extend({
29
+ intent: import_bundle.z.string().describe("The intent of the call, for example the test step description plan idea")
30
+ }) : tool.inputSchema;
31
+ const readOnly = tool.type === "readOnly" || tool.type === "assertion";
27
32
  return {
28
33
  name: tool.name,
29
34
  description: tool.description,
30
- inputSchema: (0, import_bundle.zodToJsonSchema)(tool.inputSchema, { strictUnions: true }),
35
+ inputSchema: (0, import_bundle.zodToJsonSchema)(inputSchema, { strictUnions: true }),
31
36
  annotations: {
32
37
  title: tool.title,
33
- readOnlyHint: tool.type === "readOnly",
34
- destructiveHint: tool.type === "destructive",
38
+ readOnlyHint: readOnly,
39
+ destructiveHint: !readOnly,
35
40
  openWorldHint: true
36
41
  }
37
42
  };