playwright 1.56.0-alpha-2025-10-01 → 1.56.0-beta-1759412259000
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/lib/agents/generator.md +3 -3
- package/lib/mcp/browser/context.js +12 -4
- package/lib/mcp/browser/response.js +42 -0
- package/lib/mcp/browser/tools/common.js +2 -2
- package/lib/mcp/browser/tools/dialogs.js +1 -1
- package/lib/mcp/browser/tools/evaluate.js +1 -1
- package/lib/mcp/browser/tools/files.js +1 -1
- package/lib/mcp/browser/tools/form.js +1 -1
- package/lib/mcp/browser/tools/install.js +1 -1
- package/lib/mcp/browser/tools/keyboard.js +2 -2
- package/lib/mcp/browser/tools/mouse.js +3 -3
- package/lib/mcp/browser/tools/navigate.js +2 -2
- package/lib/mcp/browser/tools/screenshot.js +4 -10
- package/lib/mcp/browser/tools/snapshot.js +4 -4
- package/lib/mcp/browser/tools/tabs.js +1 -1
- package/lib/mcp/browser/tools/verify.js +4 -4
- package/lib/mcp/browser/tools/wait.js +1 -1
- package/lib/mcp/program.js +1 -6
- package/lib/mcp/sdk/bundle.js +3 -0
- package/lib/mcp/sdk/mdb.js +42 -67
- package/lib/mcp/sdk/tool.js +9 -4
- package/lib/mcp/test/generatorTools.js +6 -24
- package/lib/mcp/test/plannerTools.js +2 -2
- package/lib/mcp/test/seed.js +1 -2
- package/lib/mcp/test/testBackend.js +9 -7
- package/lib/mcp/test/testContext.js +23 -14
- package/lib/mcpBundleImpl.js +12 -12
- package/lib/program.js +2 -2
- package/package.json +2 -2
- package/lib/mcp/vscode/host.js +0 -187
- package/lib/mcp/vscode/main.js +0 -77
package/lib/agents/generator.md
CHANGED
|
@@ -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
|
-
-
|
|
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
|
-
|
|
148
|
-
|
|
147
|
+
if (import_fs.default.existsSync(p)) {
|
|
148
|
+
try {
|
|
149
149
|
await import_fs.default.promises.rename(p, name);
|
|
150
|
-
|
|
151
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
37
|
+
type: "assertion"
|
|
38
38
|
},
|
|
39
39
|
handle: async (context, params, response) => {
|
|
40
40
|
if (!params.text && !params.textGone && !params.time)
|
package/lib/mcp/program.js
CHANGED
|
@@ -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("--
|
|
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
|
{
|
package/lib/mcp/sdk/bundle.js
CHANGED
|
@@ -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,
|
package/lib/mcp/sdk/mdb.js
CHANGED
|
@@ -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(
|
|
48
|
-
this._stack = [];
|
|
46
|
+
constructor(mainBackend) {
|
|
49
47
|
this._progress = [];
|
|
50
|
-
this.
|
|
51
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
return
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
161
|
-
const mdbBackend = new MDBBackend(backendFactory.create()
|
|
135
|
+
async function runMainBackend(backendFactory, options) {
|
|
136
|
+
const mdbBackend = new MDBBackend(backendFactory.create());
|
|
162
137
|
const factory = {
|
|
163
138
|
...backendFactory,
|
|
164
139
|
create: () => mdbBackend
|
package/lib/mcp/sdk/tool.js
CHANGED
|
@@ -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
|
-
|
|
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)(
|
|
35
|
+
inputSchema: (0, import_bundle.zodToJsonSchema)(inputSchema, { strictUnions: true }),
|
|
31
36
|
annotations: {
|
|
32
37
|
title: tool.title,
|
|
33
|
-
readOnlyHint:
|
|
34
|
-
destructiveHint:
|
|
38
|
+
readOnlyHint: readOnly,
|
|
39
|
+
destructiveHint: !readOnly,
|
|
35
40
|
openWorldHint: true
|
|
36
41
|
}
|
|
37
42
|
};
|