@cotestdev/mcp_playwright 0.0.14 → 0.0.15

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.
Files changed (47) hide show
  1. package/lib/mcp/browser/browserContextFactory.js +49 -13
  2. package/lib/mcp/browser/browserServerBackend.js +5 -2
  3. package/lib/mcp/browser/config.js +95 -23
  4. package/lib/mcp/browser/context.js +28 -3
  5. package/lib/mcp/browser/response.js +240 -57
  6. package/lib/mcp/browser/sessionLog.js +1 -1
  7. package/lib/mcp/browser/tab.js +96 -69
  8. package/lib/mcp/browser/tools/common.js +8 -8
  9. package/lib/mcp/browser/tools/console.js +6 -3
  10. package/lib/mcp/browser/tools/dialogs.js +13 -13
  11. package/lib/mcp/browser/tools/evaluate.js +9 -20
  12. package/lib/mcp/browser/tools/files.js +10 -5
  13. package/lib/mcp/browser/tools/form.js +11 -22
  14. package/lib/mcp/browser/tools/install.js +3 -3
  15. package/lib/mcp/browser/tools/keyboard.js +12 -12
  16. package/lib/mcp/browser/tools/mouse.js +14 -14
  17. package/lib/mcp/browser/tools/navigate.js +5 -5
  18. package/lib/mcp/browser/tools/network.js +16 -5
  19. package/lib/mcp/browser/tools/pdf.js +7 -18
  20. package/lib/mcp/browser/tools/runCode.js +77 -0
  21. package/lib/mcp/browser/tools/screenshot.js +44 -33
  22. package/lib/mcp/browser/tools/snapshot.js +42 -33
  23. package/lib/mcp/browser/tools/tabs.js +7 -10
  24. package/lib/mcp/browser/tools/tool.js +8 -7
  25. package/lib/mcp/browser/tools/tracing.js +4 -4
  26. package/lib/mcp/browser/tools/utils.js +50 -52
  27. package/lib/mcp/browser/tools/verify.js +23 -34
  28. package/lib/mcp/browser/tools/wait.js +6 -6
  29. package/lib/mcp/browser/tools.js +4 -3
  30. package/lib/mcp/extension/cdpRelay.js +1 -1
  31. package/lib/mcp/extension/extensionContextFactory.js +4 -3
  32. package/lib/mcp/log.js +2 -2
  33. package/lib/mcp/program.js +21 -29
  34. package/lib/mcp/sdk/exports.js +1 -5
  35. package/lib/mcp/sdk/http.js +37 -50
  36. package/lib/mcp/sdk/server.js +61 -9
  37. package/lib/mcp/sdk/tool.js +5 -4
  38. package/lib/mcp/test/browserBackend.js +67 -61
  39. package/lib/mcp/test/generatorTools.js +122 -0
  40. package/lib/mcp/test/plannerTools.js +144 -0
  41. package/lib/mcp/test/seed.js +82 -0
  42. package/lib/mcp/test/streams.js +10 -7
  43. package/lib/mcp/test/testBackend.js +44 -24
  44. package/lib/mcp/test/testContext.js +243 -14
  45. package/lib/mcp/test/testTools.js +23 -109
  46. package/lib/util.js +12 -6
  47. package/package.json +1 -1
@@ -21,9 +21,9 @@ __export(tabs_exports, {
21
21
  default: () => tabs_default
22
22
  });
23
23
  module.exports = __toCommonJS(tabs_exports);
24
- var import_bundle = require("../../sdk/bundle");
25
- var import_common = require("./common");
24
+ var import_mcpBundle = require("playwright-core/lib/mcpBundle");
26
25
  var import_tool = require("./tool");
26
+ var import_common = require("./common");
27
27
  const browserTabs = (0, import_tool.defineTool)({
28
28
  capability: "core-tabs",
29
29
  schema: {
@@ -31,10 +31,10 @@ const browserTabs = (0, import_tool.defineTool)({
31
31
  title: "Manage tabs",
32
32
  description: "List, create, close, or select a browser tab.",
33
33
  inputSchema: import_common.baseSchema.extend({
34
- action: import_bundle.z.enum(["list", "new", "close", "select"]).describe("Operation to perform"),
35
- index: import_bundle.z.number().optional().describe("Tab index, used for close/select. If omitted for close, current tab is closed.")
34
+ action: import_mcpBundle.z.enum(["list", "new", "close", "select"]).describe("Operation to perform"),
35
+ index: import_mcpBundle.z.number().optional().describe("Tab index, used for close/select. If omitted for close, current tab is closed.")
36
36
  }),
37
- type: "destructive"
37
+ type: "action"
38
38
  },
39
39
  handle: async (context, params, response) => {
40
40
  switch (params.action) {
@@ -44,23 +44,20 @@ const browserTabs = (0, import_tool.defineTool)({
44
44
  return;
45
45
  }
46
46
  case "new": {
47
- response.addCode(`page = await context.newPage();`);
48
47
  await context.newTab();
49
48
  response.setIncludeTabs();
50
49
  return;
51
50
  }
52
51
  case "close": {
53
- response.addCode(`await page.close();`);
54
52
  await context.closeTab(params.index);
55
- response.setIncludeSnapshot();
53
+ response.setIncludeFullSnapshot();
56
54
  return;
57
55
  }
58
56
  case "select": {
59
57
  if (params.index === void 0)
60
58
  throw new Error("Tab index is required");
61
- response.addCode(`page = pages[${params.index}];`);
62
59
  await context.selectTab(params.index);
63
- response.setIncludeSnapshot();
60
+ response.setIncludeFullSnapshot();
64
61
  return;
65
62
  }
66
63
  }
@@ -31,14 +31,15 @@ function defineTabTool(tool) {
31
31
  handle: async (context, params, response) => {
32
32
  const tab = await context.ensureTab();
33
33
  const modalStates = tab.modalStates().map((state) => state.type);
34
- if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState))
35
- response.addError(`Error: The tool "${tool.schema.name}" can only be used when there is related modal state present.
36
- ` + tab.modalStatesMarkdown().join("\n"));
37
- else if (!tool.clearsModalState && modalStates.length)
38
- response.addError(`Error: Tool "${tool.schema.name}" does not handle the modal state.
39
- ` + tab.modalStatesMarkdown().join("\n"));
40
- else
34
+ if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState)) {
35
+ response.setIncludeModalStates(tab.modalStates());
36
+ response.addError(`Error: The tool "${tool.schema.name}" can only be used when there is related modal state present.`);
37
+ } else if (!tool.clearsModalState && modalStates.length) {
38
+ response.setIncludeModalStates(tab.modalStates());
39
+ response.addError(`Error: Tool "${tool.schema.name}" does not handle the modal state.`);
40
+ } else {
41
41
  return tool.handle(tab, params, response);
42
+ }
42
43
  }
43
44
  };
44
45
  }
@@ -21,7 +21,7 @@ __export(tracing_exports, {
21
21
  default: () => tracing_default
22
22
  });
23
23
  module.exports = __toCommonJS(tracing_exports);
24
- var import_bundle = require("../../sdk/bundle");
24
+ var import_mcpBundle = require("playwright-core/lib/mcpBundle");
25
25
  var import_tool = require("./tool");
26
26
  const tracingStart = (0, import_tool.defineTool)({
27
27
  capability: "tracing",
@@ -29,12 +29,12 @@ const tracingStart = (0, import_tool.defineTool)({
29
29
  name: "browser_start_tracing",
30
30
  title: "Start tracing",
31
31
  description: "Start trace recording",
32
- inputSchema: import_bundle.z.object({}),
32
+ inputSchema: import_mcpBundle.z.object({}),
33
33
  type: "readOnly"
34
34
  },
35
35
  handle: async (context, params, response) => {
36
36
  const browserContext = await context.ensureBrowserContext();
37
- const tracesDir = await context.outputFile(`traces`, { origin: "code" });
37
+ const tracesDir = await context.outputFile(`traces`, { origin: "code", reason: "Collecting trace" });
38
38
  const name = "trace-" + Date.now();
39
39
  await browserContext.tracing.start({
40
40
  name,
@@ -56,7 +56,7 @@ const tracingStop = (0, import_tool.defineTool)({
56
56
  name: "browser_stop_tracing",
57
57
  title: "Stop tracing",
58
58
  description: "Stop trace recording",
59
- inputSchema: import_bundle.z.object({}),
59
+ inputSchema: import_mcpBundle.z.object({}),
60
60
  type: "readOnly"
61
61
  },
62
62
  handle: async (context, params, response) => {
@@ -20,77 +20,75 @@ var utils_exports = {};
20
20
  __export(utils_exports, {
21
21
  callOnPageNoTrace: () => callOnPageNoTrace,
22
22
  dateAsFileName: () => dateAsFileName,
23
- generateLocator: () => generateLocator,
23
+ eventWaiter: () => eventWaiter,
24
24
  waitForCompletion: () => waitForCompletion
25
25
  });
26
26
  module.exports = __toCommonJS(utils_exports);
27
- var import_utils = require("playwright-core/lib/utils");
28
27
  async function waitForCompletion(tab, callback) {
29
- const requests = /* @__PURE__ */ new Set();
30
- let frameNavigated = false;
31
- let waitCallback = () => {
32
- };
33
- const waitBarrier = new Promise((f) => {
34
- waitCallback = f;
35
- });
36
- const requestListener = (request) => requests.add(request);
37
- const requestFinishedListener = (request) => {
38
- requests.delete(request);
39
- if (!requests.size)
40
- waitCallback();
41
- };
42
- const frameNavigateListener = (frame) => {
43
- if (frame.parentFrame())
44
- return;
45
- frameNavigated = true;
46
- dispose();
47
- clearTimeout(timeout);
48
- void tab.waitForLoadState("load").then(waitCallback);
49
- };
50
- const onTimeout = () => {
51
- dispose();
52
- waitCallback();
53
- };
54
- tab.page.on("request", requestListener);
55
- tab.page.on("requestfinished", requestFinishedListener);
56
- tab.page.on("framenavigated", frameNavigateListener);
57
- const timeout = setTimeout(onTimeout, 1e4);
58
- const dispose = () => {
28
+ const requests = [];
29
+ const requestListener = (request) => requests.push(request);
30
+ const disposeListeners = () => {
59
31
  tab.page.off("request", requestListener);
60
- tab.page.off("requestfinished", requestFinishedListener);
61
- tab.page.off("framenavigated", frameNavigateListener);
62
- clearTimeout(timeout);
63
32
  };
33
+ tab.page.on("request", requestListener);
34
+ let result;
64
35
  try {
65
- const result = await callback();
66
- if (!requests.size && !frameNavigated)
67
- waitCallback();
68
- await waitBarrier;
69
- await tab.waitForTimeout(1e3);
70
- return result;
36
+ result = await callback();
37
+ await tab.waitForTimeout(500);
71
38
  } finally {
72
- dispose();
39
+ disposeListeners();
73
40
  }
74
- }
75
- async function generateLocator(locator) {
76
- try {
77
- const { resolvedSelector } = await locator._resolveSelector();
78
- return (0, import_utils.asLocator)("javascript", resolvedSelector);
79
- } catch (e) {
80
- throw new Error("Ref not found, likely because element was removed. Use browser_snapshot to see what elements are currently on the page.");
41
+ const requestedNavigation = requests.some((request) => request.isNavigationRequest());
42
+ if (requestedNavigation) {
43
+ await tab.page.mainFrame().waitForLoadState("load", { timeout: 1e4 }).catch(() => {
44
+ });
45
+ return result;
81
46
  }
47
+ const promises = [];
48
+ for (const request of requests) {
49
+ if (["document", "stylesheet", "script", "xhr", "fetch"].includes(request.resourceType()))
50
+ promises.push(request.response().then((r) => r?.finished()).catch(() => {
51
+ }));
52
+ else
53
+ promises.push(request.response().catch(() => {
54
+ }));
55
+ }
56
+ const timeout = new Promise((resolve) => setTimeout(resolve, 5e3));
57
+ await Promise.race([Promise.all(promises), timeout]);
58
+ if (requests.length)
59
+ await tab.waitForTimeout(500);
60
+ return result;
82
61
  }
83
62
  async function callOnPageNoTrace(page, callback) {
84
63
  return await page._wrapApiCall(() => callback(page), { internal: true });
85
64
  }
86
- function dateAsFileName() {
65
+ function dateAsFileName(extension) {
87
66
  const date = /* @__PURE__ */ new Date();
88
- return date.toISOString().replace(/[:.]/g, "-");
67
+ return `page-${date.toISOString().replace(/[:.]/g, "-")}.${extension}`;
68
+ }
69
+ function eventWaiter(page, event, timeout) {
70
+ const disposables = [];
71
+ const eventPromise = new Promise((resolve, reject) => {
72
+ page.on(event, resolve);
73
+ disposables.push(() => page.off(event, resolve));
74
+ });
75
+ let abort;
76
+ const abortPromise = new Promise((resolve, reject) => {
77
+ abort = () => resolve(void 0);
78
+ });
79
+ const timeoutPromise = new Promise((f) => {
80
+ const timeoutId = setTimeout(() => f(void 0), timeout);
81
+ disposables.push(() => clearTimeout(timeoutId));
82
+ });
83
+ return {
84
+ promise: Promise.race([eventPromise, abortPromise, timeoutPromise]).finally(() => disposables.forEach((dispose) => dispose())),
85
+ abort
86
+ };
89
87
  }
90
88
  // Annotate the CommonJS export names for ESM import in node:
91
89
  0 && (module.exports = {
92
90
  callOnPageNoTrace,
93
91
  dateAsFileName,
94
- generateLocator,
92
+ eventWaiter,
95
93
  waitForCompletion
96
94
  });
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,24 +15,15 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
  var verify_exports = {};
30
20
  __export(verify_exports, {
31
21
  default: () => verify_default
32
22
  });
33
23
  module.exports = __toCommonJS(verify_exports);
34
- var import_bundle = require("../../sdk/bundle");
24
+ var import_mcpBundle = require("playwright-core/lib/mcpBundle");
25
+ var import_utils = require("playwright-core/lib/utils");
35
26
  var import_tool = require("./tool");
36
- var javascript = __toESM(require("../codegen"));
37
- var import_utils = require("./utils");
38
27
  var import_common = require("./common");
39
28
  const verifyElement = (0, import_tool.defineTabTool)({
40
29
  capability: "testing",
@@ -43,10 +32,10 @@ const verifyElement = (0, import_tool.defineTabTool)({
43
32
  title: "Verify element visible",
44
33
  description: "Verify element is visible on the page",
45
34
  inputSchema: import_common.baseSchema.extend({
46
- role: import_bundle.z.string().describe('ROLE of the element. Can be found in the snapshot like this: `- {ROLE} "Accessible Name":`'),
47
- accessibleName: import_bundle.z.string().describe('ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: `- role "{ACCESSIBLE_NAME}"`')
35
+ role: import_mcpBundle.z.string().describe('ROLE of the element. Can be found in the snapshot like this: `- {ROLE} "Accessible Name":`'),
36
+ accessibleName: import_mcpBundle.z.string().describe('ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: `- role "{ACCESSIBLE_NAME}"`')
48
37
  }),
49
- type: "readOnly"
38
+ type: "assertion"
50
39
  },
51
40
  handle: async (tab, params, response) => {
52
41
  const locator = tab.page.getByRole(params.role, { name: params.accessibleName });
@@ -54,7 +43,7 @@ const verifyElement = (0, import_tool.defineTabTool)({
54
43
  response.addError(`Element with role "${params.role}" and accessible name "${params.accessibleName}" not found`);
55
44
  return;
56
45
  }
57
- response.addCode(`await expect(page.getByRole(${javascript.escapeWithQuotes(params.role)}, { name: ${javascript.escapeWithQuotes(params.accessibleName)} })).toBeVisible();`);
46
+ response.addCode(`await expect(page.getByRole(${(0, import_utils.escapeWithQuotes)(params.role)}, { name: ${(0, import_utils.escapeWithQuotes)(params.accessibleName)} })).toBeVisible();`);
58
47
  response.addResult("Done");
59
48
  }
60
49
  });
@@ -65,9 +54,9 @@ const verifyText = (0, import_tool.defineTabTool)({
65
54
  title: "Verify text visible",
66
55
  description: `Verify text is visible on the page. Prefer ${verifyElement.schema.name} if possible.`,
67
56
  inputSchema: import_common.baseSchema.extend({
68
- 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}`')
57
+ text: import_mcpBundle.z.string().describe('TEXT to verify. Can be found in the snapshot like this: `- role "Accessible Name": {TEXT}` or like this: `- text: {TEXT}`')
69
58
  }),
70
- type: "readOnly"
59
+ type: "assertion"
71
60
  },
72
61
  handle: async (tab, params, response) => {
73
62
  const locator = tab.page.getByText(params.text).filter({ visible: true });
@@ -75,7 +64,7 @@ const verifyText = (0, import_tool.defineTabTool)({
75
64
  response.addError("Text not found");
76
65
  return;
77
66
  }
78
- response.addCode(`await expect(page.getByText(${javascript.escapeWithQuotes(params.text)})).toBeVisible();`);
67
+ response.addCode(`await expect(page.getByText(${(0, import_utils.escapeWithQuotes)(params.text)})).toBeVisible();`);
79
68
  response.addResult("Done");
80
69
  }
81
70
  });
@@ -86,14 +75,14 @@ const verifyList = (0, import_tool.defineTabTool)({
86
75
  title: "Verify list visible",
87
76
  description: "Verify list is visible on the page",
88
77
  inputSchema: import_common.baseSchema.extend({
89
- element: import_bundle.z.string().describe("Human-readable list description"),
90
- ref: import_bundle.z.string().describe("Exact target element reference that points to the list"),
91
- items: import_bundle.z.array(import_bundle.z.string()).describe("Items to verify")
78
+ element: import_mcpBundle.z.string().describe("Human-readable list description"),
79
+ ref: import_mcpBundle.z.string().describe("Exact target element reference that points to the list"),
80
+ items: import_mcpBundle.z.array(import_mcpBundle.z.string()).describe("Items to verify")
92
81
  }),
93
- type: "readOnly"
82
+ type: "assertion"
94
83
  },
95
84
  handle: async (tab, params, response) => {
96
- const locator = await tab.refLocator({ ref: params.ref, element: params.element });
85
+ const { locator } = await tab.refLocator({ ref: params.ref, element: params.element });
97
86
  const itemTexts = [];
98
87
  for (const item of params.items) {
99
88
  const itemLocator = locator.getByText(item);
@@ -105,7 +94,7 @@ const verifyList = (0, import_tool.defineTabTool)({
105
94
  }
106
95
  const ariaSnapshot = `\`
107
96
  - list:
108
- ${itemTexts.map((t) => ` - listitem: ${javascript.escapeWithQuotes(t, '"')}`).join("\n")}
97
+ ${itemTexts.map((t) => ` - listitem: ${(0, import_utils.escapeWithQuotes)(t, '"')}`).join("\n")}
109
98
  \``;
110
99
  response.addCode(`await expect(page.locator('body')).toMatchAriaSnapshot(${ariaSnapshot});`);
111
100
  response.addResult("Done");
@@ -118,23 +107,23 @@ const verifyValue = (0, import_tool.defineTabTool)({
118
107
  title: "Verify value",
119
108
  description: "Verify element value",
120
109
  inputSchema: import_common.baseSchema.extend({
121
- type: import_bundle.z.enum(["textbox", "checkbox", "radio", "combobox", "slider"]).describe("Type of the element"),
122
- element: import_bundle.z.string().describe("Human-readable element description"),
123
- ref: import_bundle.z.string().describe("Exact target element reference that points to the element"),
124
- value: import_bundle.z.string().describe('Value to verify. For checkbox, use "true" or "false".')
110
+ type: import_mcpBundle.z.enum(["textbox", "checkbox", "radio", "combobox", "slider"]).describe("Type of the element"),
111
+ element: import_mcpBundle.z.string().describe("Human-readable element description"),
112
+ ref: import_mcpBundle.z.string().describe("Exact target element reference that points to the element"),
113
+ value: import_mcpBundle.z.string().describe('Value to verify. For checkbox, use "true" or "false".')
125
114
  }),
126
- type: "readOnly"
115
+ type: "assertion"
127
116
  },
128
117
  handle: async (tab, params, response) => {
129
- const locator = await tab.refLocator({ ref: params.ref, element: params.element });
130
- const locatorSource = `page.${await (0, import_utils.generateLocator)(locator)}`;
118
+ const { locator, resolved } = await tab.refLocator({ ref: params.ref, element: params.element });
119
+ const locatorSource = `page.${resolved}`;
131
120
  if (params.type === "textbox" || params.type === "slider" || params.type === "combobox") {
132
121
  const value = await locator.inputValue();
133
122
  if (value !== params.value) {
134
123
  response.addError(`Expected value "${params.value}", but got "${value}"`);
135
124
  return;
136
125
  }
137
- response.addCode(`await expect(${locatorSource}).toHaveValue(${javascript.quote(params.value)});`);
126
+ response.addCode(`await expect(${locatorSource}).toHaveValue(${(0, import_utils.escapeWithQuotes)(params.value)});`);
138
127
  } else if (params.type === "checkbox" || params.type === "radio") {
139
128
  const value = await locator.isChecked();
140
129
  if (value !== (params.value === "true")) {
@@ -21,9 +21,9 @@ __export(wait_exports, {
21
21
  default: () => wait_default
22
22
  });
23
23
  module.exports = __toCommonJS(wait_exports);
24
- var import_bundle = require("../../sdk/bundle");
25
- var import_common = require("./common");
24
+ var import_mcpBundle = require("playwright-core/lib/mcpBundle");
26
25
  var import_tool = require("./tool");
26
+ var import_common = require("./common");
27
27
  const wait = (0, import_tool.defineTool)({
28
28
  capability: "core",
29
29
  schema: {
@@ -31,11 +31,11 @@ const wait = (0, import_tool.defineTool)({
31
31
  title: "Wait for",
32
32
  description: "Wait for text to appear or disappear or a specified time to pass",
33
33
  inputSchema: import_common.baseSchema.extend({
34
- time: import_bundle.z.number().optional().describe("The time to wait in seconds"),
35
- text: import_bundle.z.string().optional().describe("The text to wait for"),
36
- textGone: import_bundle.z.string().optional().describe("The text to wait for to disappear")
34
+ time: import_mcpBundle.z.number().optional().describe("The time to wait in seconds"),
35
+ text: import_mcpBundle.z.string().optional().describe("The text to wait for"),
36
+ textGone: import_mcpBundle.z.string().optional().describe("The text to wait for to disappear")
37
37
  }),
38
- type: "readOnly"
38
+ type: "assertion"
39
39
  },
40
40
  handle: async (context, params, response) => {
41
41
  if (!params.text && !params.textGone && !params.time)
@@ -41,14 +41,15 @@ var import_keyboard = __toESM(require("./tools/keyboard"));
41
41
  var import_mouse = __toESM(require("./tools/mouse"));
42
42
  var import_navigate = __toESM(require("./tools/navigate"));
43
43
  var import_pdf = __toESM(require("./tools/pdf"));
44
+ var import_runCode = __toESM(require("./tools/runCode"));
44
45
  var import_snapshot = __toESM(require("./tools/snapshot"));
45
46
  var import_screenshot = __toESM(require("./tools/screenshot"));
46
- var import_script = __toESM(require("./tools/script"));
47
47
  var import_tabs = __toESM(require("./tools/tabs"));
48
48
  var import_tracing = __toESM(require("./tools/tracing"));
49
49
  var import_wait = __toESM(require("./tools/wait"));
50
50
  var import_verify = __toESM(require("./tools/verify"));
51
51
  const browserTools = [
52
+ ...import_common.default,
52
53
  ...import_common.default,
53
54
  // ...console,
54
55
  ...import_dialogs.default,
@@ -61,16 +62,16 @@ const browserTools = [
61
62
  // ...network,
62
63
  ...import_mouse.default,
63
64
  ...import_pdf.default,
65
+ ...import_runCode.default,
64
66
  ...import_screenshot.default,
65
67
  ...import_snapshot.default,
66
- ...import_script.default,
67
68
  ...import_tabs.default,
68
69
  ...import_tracing.default,
69
70
  ...import_wait.default,
70
71
  ...import_verify.default
71
72
  ];
72
73
  function filteredTools(config) {
73
- return browserTools.filter((tool) => tool.capability.startsWith("core") || tool.capability === "testing" || config.capabilities?.includes(tool.capability));
74
+ return browserTools.filter((tool) => tool.capability.startsWith("core") || config.capabilities?.includes(tool.capability));
74
75
  }
75
76
  // Annotate the CommonJS export names for ESM import in node:
76
77
  0 && (module.exports = {
@@ -44,7 +44,7 @@ class CDPRelayServer {
44
44
  this._playwrightConnection = null;
45
45
  this._extensionConnection = null;
46
46
  this._nextSessionId = 1;
47
- this._wsHost = (0, import_http2.httpAddressToString)(server.address()).replace(/^http/, "ws");
47
+ this._wsHost = (0, import_http2.addressToString)(server.address(), { protocol: "ws" });
48
48
  this._browserChannel = browserChannel;
49
49
  this._userDataDir = userDataDir;
50
50
  this._executablePath = executablePath;
@@ -33,7 +33,7 @@ __export(extensionContextFactory_exports, {
33
33
  module.exports = __toCommonJS(extensionContextFactory_exports);
34
34
  var playwright = __toESM(require("playwright-core"));
35
35
  var import_utilsBundle = require("playwright-core/lib/utilsBundle");
36
- var import_http = require("../sdk/http");
36
+ var import_utils = require("playwright-core/lib/utils");
37
37
  var import_cdpRelay = require("./cdpRelay");
38
38
  const debugLogger = (0, import_utilsBundle.debug)("pw:mcp:relay");
39
39
  class ExtensionContextFactory {
@@ -55,10 +55,11 @@ class ExtensionContextFactory {
55
55
  async _obtainBrowser(clientInfo, abortSignal, toolName) {
56
56
  const relay = await this._startRelay(abortSignal);
57
57
  await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal, toolName);
58
- return await playwright.chromium.connectOverCDP(relay.cdpEndpoint());
58
+ return await playwright.chromium.connectOverCDP(relay.cdpEndpoint(), { isLocal: true });
59
59
  }
60
60
  async _startRelay(abortSignal) {
61
- const httpServer = await (0, import_http.startHttpServer)({});
61
+ const httpServer = (0, import_utils.createHttpServer)();
62
+ await (0, import_utils.startHttpServer)(httpServer, {});
62
63
  if (abortSignal.aborted) {
63
64
  httpServer.close();
64
65
  throw new Error(abortSignal.reason);
package/lib/mcp/log.js CHANGED
@@ -23,9 +23,9 @@ __export(log_exports, {
23
23
  });
24
24
  module.exports = __toCommonJS(log_exports);
25
25
  var import_utilsBundle = require("playwright-core/lib/utilsBundle");
26
- const errorsDebug = (0, import_utilsBundle.debug)("pw:mcp:errors");
26
+ const errorDebug = (0, import_utilsBundle.debug)("pw:mcp:error");
27
27
  function logUnhandledError(error) {
28
- errorsDebug(error);
28
+ errorDebug(error);
29
29
  }
30
30
  const testDebug = (0, import_utilsBundle.debug)("pw:mcp:test");
31
31
  // Annotate the CommonJS export names for ESM import in node:
@@ -31,23 +31,33 @@ __export(program_exports, {
31
31
  decorateCommand: () => decorateCommand
32
32
  });
33
33
  module.exports = __toCommonJS(program_exports);
34
+ var import_fs = __toESM(require("fs"));
34
35
  var import_utilsBundle = require("playwright-core/lib/utilsBundle");
36
+ var import_server = require("playwright-core/lib/server");
35
37
  var mcpServer = __toESM(require("./sdk/server"));
36
38
  var import_config = require("./browser/config");
37
39
  var import_watchdog = require("./browser/watchdog");
38
40
  var import_browserContextFactory = require("./browser/browserContextFactory");
39
- var import_proxyBackend = require("./sdk/proxyBackend");
40
41
  var import_browserServerBackend = require("./browser/browserServerBackend");
41
42
  var import_extensionContextFactory = require("./extension/extensionContextFactory");
42
- var import_host = require("./vscode/host");
43
43
  function decorateCommand(command, version) {
44
- command.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("--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 "1280, 720"').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) => {
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 TRUSTED origins to allow the browser to request. Default is to allow all.\nImportant: *does not* serve as a security boundary and *does not* affect redirects. ", 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.\nImportant: *does not* serve as a security boundary and *does not* affect redirects.", 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("--console-level <level>", 'level of console messages to return: "error", "warning", "info", "debug". Each level includes the messages of more severe levels.', import_config.enumParser.bind(null, "--console-level", ["error", "warning", "info", "debug"])).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-page <path...>", "path to TypeScript file to evaluate on Playwright page object").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".', import_config.enumParser.bind(null, "--image-responses", ["allow", "omit"])).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("--snapshot-mode <mode>", 'when taking snapshots for responses, specifies the mode to use. Can be "incremental", "full", or "none". Default is incremental.').option("--storage-state <path>", "path to the storage state file for isolated sessions.").option("--test-id-attribute <attribute>", 'specify the attribute to use for test ids, defaults to "data-testid"').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("--vision", "Legacy option, use --caps=vision instead").hideHelp()).action(async (options) => {
45
45
  (0, import_watchdog.setupExitWatchdog)();
46
46
  if (options.vision) {
47
47
  console.error("The --vision option is deprecated, use --caps=vision instead");
48
48
  options.caps = "vision";
49
49
  }
50
50
  const config = await (0, import_config.resolveCLIConfig)(options);
51
+ if (config.saveVideo && !checkFfmpeg()) {
52
+ console.error(import_utilsBundle.colors.red(`
53
+ Error: ffmpeg required to save the video is not installed.`));
54
+ console.error(`
55
+ Please run the command below. It will install a local copy of ffmpeg and will not change any system-wide settings.`);
56
+ console.error(`
57
+ npx playwright install ffmpeg
58
+ `);
59
+ process.exit(1);
60
+ }
51
61
  const browserContextFactory = (0, import_browserContextFactory.contextFactory)(config);
52
62
  const extensionContextFactory = new import_extensionContextFactory.ExtensionContextFactory(config.browser.launchOptions.channel || "chrome", config.browser.userDataDir, config.browser.launchOptions.executablePath);
53
63
  if (options.extension) {
@@ -60,32 +70,6 @@ function decorateCommand(command, version) {
60
70
  await mcpServer.start(serverBackendFactory, config.server);
61
71
  return;
62
72
  }
63
- if (options.vscode) {
64
- await (0, import_host.runVSCodeTools)(config);
65
- return;
66
- }
67
- if (options.connectTool) {
68
- const providers = [
69
- {
70
- name: "default",
71
- description: "Starts standalone browser",
72
- connect: () => mcpServer.wrapInProcess(new import_browserServerBackend.BrowserServerBackend(config, browserContextFactory))
73
- },
74
- {
75
- name: "extension",
76
- description: "Connect to a browser using the Playwright MCP extension",
77
- connect: () => mcpServer.wrapInProcess(new import_browserServerBackend.BrowserServerBackend(config, extensionContextFactory))
78
- }
79
- ];
80
- const factory2 = {
81
- name: "Playwright w/ switch",
82
- nameInConfig: "playwright-switch",
83
- version,
84
- create: () => new import_proxyBackend.ProxyBackend(providers)
85
- };
86
- await mcpServer.start(factory2, config.server);
87
- return;
88
- }
89
73
  const factory = {
90
74
  name: "Playwright",
91
75
  nameInConfig: "playwright",
@@ -95,6 +79,14 @@ function decorateCommand(command, version) {
95
79
  await mcpServer.start(factory, config.server);
96
80
  });
97
81
  }
82
+ function checkFfmpeg() {
83
+ try {
84
+ const executable = import_server.registry.findExecutable("ffmpeg");
85
+ return import_fs.default.existsSync(executable.executablePath("javascript"));
86
+ } catch (error) {
87
+ return false;
88
+ }
89
+ }
98
90
  // Annotate the CommonJS export names for ESM import in node:
99
91
  0 && (module.exports = {
100
92
  decorateCommand
@@ -16,17 +16,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
16
16
  var exports_exports = {};
17
17
  module.exports = __toCommonJS(exports_exports);
18
18
  __reExport(exports_exports, require("./inProcessTransport"), module.exports);
19
- __reExport(exports_exports, require("./proxyBackend"), module.exports);
20
19
  __reExport(exports_exports, require("./server"), module.exports);
21
20
  __reExport(exports_exports, require("./tool"), module.exports);
22
21
  __reExport(exports_exports, require("./http"), module.exports);
23
- __reExport(exports_exports, require("./mdb"), module.exports);
24
22
  // Annotate the CommonJS export names for ESM import in node:
25
23
  0 && (module.exports = {
26
24
  ...require("./inProcessTransport"),
27
- ...require("./proxyBackend"),
28
25
  ...require("./server"),
29
26
  ...require("./tool"),
30
- ...require("./http"),
31
- ...require("./mdb")
27
+ ...require("./http")
32
28
  });