@playwright/mcp 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,7 +33,19 @@ A Model Context Protocol (MCP) server that provides browser automation capabilit
33
33
 
34
34
  #### Installation in VS Code
35
35
 
36
- Install the Playwright MCP server using the VS Code CLI:
36
+ Install the Playwright MCP server in VS Code using one of these buttons:
37
+
38
+ <!--
39
+ // Generate using?:
40
+ const config = JSON.stringify({ name: 'playwright', command: 'npx', args: ["-y", "@playwright/mcp@latest"] });
41
+ const urlForWebsites = `vscode:mcp/install?${encodeURIComponent(config)}`;
42
+ // Github markdown does not allow linking to `vscode:` directly, so you can use our redirect:
43
+ const urlForGithub = `https://insiders.vscode.dev/redirect?url=${encodeURIComponent(urlForWebsites)}`;
44
+ -->
45
+
46
+ [<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D)
47
+
48
+ Alternatively, you can install the Playwright MCP server using the VS Code CLI:
37
49
 
38
50
  ```bash
39
51
  # For VS Code
@@ -47,6 +59,18 @@ code-insiders --add-mcp '{"name":"playwright","command":"npx","args":["@playwrig
47
59
 
48
60
  After installation, the Playwright MCP server will be available for use with your GitHub Copilot agent in VS Code.
49
61
 
62
+ ### User data directory
63
+
64
+ Playwright MCP will launch Chrome browser with the new profile, located at
65
+
66
+ ```
67
+ - `%USERPROFILE%\AppData\Local\ms-playwright\mcp-chrome-profile` on Windows
68
+ - `~/Library/Caches/ms-playwright/mcp-chrome-profile` on macOS
69
+ - `~/.cache/ms-playwright/mcp-chrome-profile` on Linux
70
+ ```
71
+
72
+ All the logged in information will be stored in that profile, you can delete it between sessions if you'dlike to clear the offline state.
73
+
50
74
 
51
75
  ### Running headless browser (Browser without GUI).
52
76
 
@@ -121,6 +145,20 @@ To use Vision Mode, add the `--vision` flag when starting the server:
121
145
  Vision Mode works best with the computer use models that are able to interact with elements using
122
146
  X Y coordinate space, based on the provided screenshot.
123
147
 
148
+ ### Programmatic usage with custom transports
149
+
150
+ ```js
151
+ import { createServer } from '@playwright/mcp';
152
+
153
+ // ...
154
+
155
+ const server = createServer({
156
+ launchOptions: { headless: true }
157
+ });
158
+ transport = new SSEServerTransport("/messages", res);
159
+ server.connect(transport);
160
+ ```
161
+
124
162
  ### Snapshot Mode
125
163
 
126
164
  The Playwright MCP provides a set of tools for browser automation. Here are all available tools:
@@ -166,6 +204,13 @@ The Playwright MCP provides a set of tools for browser automation. Here are all
166
204
  - `text` (string): Text to type into the element
167
205
  - `submit` (boolean): Whether to submit entered text (press Enter after)
168
206
 
207
+ - **browser_select_option**
208
+ - Description: Select option in a dropdown
209
+ - Parameters:
210
+ - `element` (string): Human-readable element description used to obtain permission to interact with the element
211
+ - `ref` (string): Exact target element reference from the page snapshot
212
+ - `values` (array): Array of values to select in the dropdown.
213
+
169
214
  - **browser_press_key**
170
215
  - Description: Press a key on the keyboard
171
216
  - Parameters:
@@ -179,6 +224,11 @@ The Playwright MCP provides a set of tools for browser automation. Here are all
179
224
  - Description: Save page as PDF
180
225
  - Parameters: None
181
226
 
227
+ - **browser_take_screenshot**
228
+ - Description: Capture screenshot of the page
229
+ - Parameters:
230
+ - `raw` (string): Optionally returns lossless PNG screenshot. JPEG by default.
231
+
182
232
  - **browser_wait**
183
233
  - Description: Wait for a specified time in seconds
184
234
  - Parameters:
package/index.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copyright (c) Microsoft Corporation.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ import type { LaunchOptions } from 'playwright';
19
+ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
20
+
21
+ type Options = {
22
+ /**
23
+ * Path to the user data directory.
24
+ */
25
+ userDataDir?: string;
26
+
27
+ /**
28
+ * Launch options for the browser.
29
+ */
30
+ launchOptions?: LaunchOptions;
31
+
32
+ /**
33
+ * Use screenshots instead of snapshots. Less accurate, reliable and overall
34
+ * slower, but contains visual representation of the page.
35
+ * @default false
36
+ */
37
+ vision?: boolean;
38
+ };
39
+
40
+ export function createServer(options?: Options): Server;
package/index.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copyright (c) Microsoft Corporation.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ const { createServer } = require('./lib/index');
19
+ module.exports = { createServer };
package/lib/context.js CHANGED
@@ -51,56 +51,67 @@ Object.defineProperty(exports, "__esModule", { value: true });
51
51
  exports.Context = void 0;
52
52
  const playwright = __importStar(require("playwright"));
53
53
  class Context {
54
+ _userDataDir;
54
55
  _launchOptions;
55
56
  _browser;
56
57
  _page;
57
58
  _console = [];
58
- _initializePromise;
59
- constructor(launchOptions) {
59
+ _createPagePromise;
60
+ constructor(userDataDir, launchOptions) {
61
+ this._userDataDir = userDataDir;
60
62
  this._launchOptions = launchOptions;
61
63
  }
62
- async ensurePage() {
63
- await this._initialize();
64
- return this._page;
65
- }
66
- async ensureConsole() {
67
- await this._initialize();
68
- return this._console;
69
- }
70
- async close() {
71
- const page = await this.ensurePage();
72
- await page.close();
73
- }
74
- async _initialize() {
75
- if (this._initializePromise)
76
- return this._initializePromise;
77
- this._initializePromise = (async () => {
78
- this._browser = await createBrowser(this._launchOptions);
79
- this._page = await this._browser.newPage();
80
- this._page.on('console', event => this._console.push(event));
81
- this._page.on('framenavigated', frame => {
64
+ async createPage() {
65
+ if (this._createPagePromise)
66
+ return this._createPagePromise;
67
+ this._createPagePromise = (async () => {
68
+ const { browser, page } = await this._createPage();
69
+ page.on('console', event => this._console.push(event));
70
+ page.on('framenavigated', frame => {
82
71
  if (!frame.parentFrame())
83
72
  this._console.length = 0;
84
73
  });
85
- this._page.on('close', () => this._reset());
74
+ page.on('close', () => this._onPageClose());
75
+ this._page = page;
76
+ this._browser = browser;
77
+ return page;
86
78
  })();
87
- return this._initializePromise;
79
+ return this._createPagePromise;
88
80
  }
89
- _reset() {
81
+ _onPageClose() {
90
82
  const browser = this._browser;
91
- this._initializePromise = undefined;
83
+ const page = this._page;
84
+ void page?.context()?.close().then(() => browser?.close()).catch(() => { });
85
+ this._createPagePromise = undefined;
92
86
  this._browser = undefined;
93
87
  this._page = undefined;
94
88
  this._console.length = 0;
95
- void browser?.close();
96
89
  }
97
- }
98
- exports.Context = Context;
99
- async function createBrowser(launchOptions) {
100
- if (process.env.PLAYWRIGHT_WS_ENDPOINT) {
101
- const url = new URL(process.env.PLAYWRIGHT_WS_ENDPOINT);
102
- url.searchParams.set('launch-options', JSON.stringify(launchOptions));
103
- return await playwright.chromium.connect(String(url));
90
+ existingPage() {
91
+ if (!this._page)
92
+ throw new Error('Navigate to a location to create a page');
93
+ return this._page;
94
+ }
95
+ async console() {
96
+ return this._console;
97
+ }
98
+ async close() {
99
+ if (!this._page)
100
+ return;
101
+ await this._page.close();
102
+ }
103
+ async _createPage() {
104
+ if (process.env.PLAYWRIGHT_WS_ENDPOINT) {
105
+ const url = new URL(process.env.PLAYWRIGHT_WS_ENDPOINT);
106
+ if (this._launchOptions)
107
+ url.searchParams.set('launch-options', JSON.stringify(this._launchOptions));
108
+ const browser = await playwright.chromium.connect(String(url));
109
+ const page = await browser.newPage();
110
+ return { browser, page };
111
+ }
112
+ const context = await playwright.chromium.launchPersistentContext(this._userDataDir, this._launchOptions);
113
+ const [page] = context.pages();
114
+ return { page };
104
115
  }
105
- return await playwright.chromium.launch({ channel: 'chrome', ...launchOptions });
106
116
  }
117
+ exports.Context = Context;
package/lib/index.js ADDED
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) Microsoft Corporation.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
21
+ desc = { enumerable: true, get: function() { return m[k]; } };
22
+ }
23
+ Object.defineProperty(o, k2, desc);
24
+ }) : (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ o[k2] = m[k];
27
+ }));
28
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
30
+ }) : function(o, v) {
31
+ o["default"] = v;
32
+ });
33
+ var __importStar = (this && this.__importStar) || (function () {
34
+ var ownKeys = function(o) {
35
+ ownKeys = Object.getOwnPropertyNames || function (o) {
36
+ var ar = [];
37
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
38
+ return ar;
39
+ };
40
+ return ownKeys(o);
41
+ };
42
+ return function (mod) {
43
+ if (mod && mod.__esModule) return mod;
44
+ var result = {};
45
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
46
+ __setModuleDefault(result, mod);
47
+ return result;
48
+ };
49
+ })();
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.createServer = createServer;
52
+ const server_1 = require("./server");
53
+ const snapshot = __importStar(require("./tools/snapshot"));
54
+ const common = __importStar(require("./tools/common"));
55
+ const screenshot = __importStar(require("./tools/screenshot"));
56
+ const console_1 = require("./resources/console");
57
+ const commonTools = [
58
+ common.pressKey,
59
+ common.wait,
60
+ common.pdf,
61
+ common.close,
62
+ ];
63
+ const snapshotTools = [
64
+ common.navigate(true),
65
+ common.goBack(true),
66
+ common.goForward(true),
67
+ snapshot.snapshot,
68
+ snapshot.click,
69
+ snapshot.hover,
70
+ snapshot.type,
71
+ snapshot.selectOption,
72
+ snapshot.screenshot,
73
+ ...commonTools,
74
+ ];
75
+ const screenshotTools = [
76
+ common.navigate(false),
77
+ common.goBack(false),
78
+ common.goForward(false),
79
+ screenshot.screenshot,
80
+ screenshot.moveMouse,
81
+ screenshot.click,
82
+ screenshot.drag,
83
+ screenshot.type,
84
+ ...commonTools,
85
+ ];
86
+ const resources = [
87
+ console_1.console,
88
+ ];
89
+ const packageJSON = require('../package.json');
90
+ function createServer(options) {
91
+ const tools = options?.vision ? screenshotTools : snapshotTools;
92
+ return (0, server_1.createServerWithTools)({
93
+ name: 'Playwright',
94
+ version: packageJSON.version,
95
+ tools,
96
+ resources,
97
+ userDataDir: options?.userDataDir ?? '',
98
+ launchOptions: options?.launchOptions,
99
+ });
100
+ }
package/lib/program.js CHANGED
@@ -14,101 +14,56 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
- if (k2 === undefined) k2 = k;
19
- var desc = Object.getOwnPropertyDescriptor(m, k);
20
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
21
- desc = { enumerable: true, get: function() { return m[k]; } };
22
- }
23
- Object.defineProperty(o, k2, desc);
24
- }) : (function(o, m, k, k2) {
25
- if (k2 === undefined) k2 = k;
26
- o[k2] = m[k];
27
- }));
28
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
- Object.defineProperty(o, "default", { enumerable: true, value: v });
30
- }) : function(o, v) {
31
- o["default"] = v;
32
- });
33
- var __importStar = (this && this.__importStar) || (function () {
34
- var ownKeys = function(o) {
35
- ownKeys = Object.getOwnPropertyNames || function (o) {
36
- var ar = [];
37
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
38
- return ar;
39
- };
40
- return ownKeys(o);
41
- };
42
- return function (mod) {
43
- if (mod && mod.__esModule) return mod;
44
- var result = {};
45
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
46
- __setModuleDefault(result, mod);
47
- return result;
48
- };
49
- })();
17
+ var __importDefault = (this && this.__importDefault) || function (mod) {
18
+ return (mod && mod.__esModule) ? mod : { "default": mod };
19
+ };
50
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
+ const fs_1 = __importDefault(require("fs"));
22
+ const os_1 = __importDefault(require("os"));
23
+ const path_1 = __importDefault(require("path"));
51
24
  const commander_1 = require("commander");
52
- const server_1 = require("./server");
53
- const snapshot = __importStar(require("./tools/snapshot"));
54
- const common = __importStar(require("./tools/common"));
55
- const screenshot = __importStar(require("./tools/screenshot"));
56
- const console_1 = require("./resources/console");
25
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
26
+ const index_1 = require("./index");
57
27
  const packageJSON = require('../package.json');
58
28
  commander_1.program
59
29
  .version('Version ' + packageJSON.version)
60
30
  .name(packageJSON.name)
61
31
  .option('--headless', 'Run browser in headless mode, headed by default')
32
+ .option('--user-data-dir <path>', 'Path to the user data directory')
62
33
  .option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
63
34
  .action(async (options) => {
64
35
  const launchOptions = {
65
36
  headless: !!options.headless,
37
+ channel: 'chrome',
66
38
  };
67
- const tools = options.vision ? screenshotTools : snapshotTools;
68
- const server = new server_1.Server({
69
- name: 'Playwright',
70
- version: packageJSON.version,
71
- tools,
72
- resources,
73
- }, launchOptions);
39
+ const server = (0, index_1.createServer)({
40
+ userDataDir: options.userDataDir ?? await userDataDir(),
41
+ launchOptions,
42
+ vision: !!options.vision,
43
+ });
74
44
  setupExitWatchdog(server);
75
- await server.start();
45
+ const transport = new stdio_js_1.StdioServerTransport();
46
+ await server.connect(transport);
76
47
  });
77
48
  function setupExitWatchdog(server) {
78
49
  process.stdin.on('close', async () => {
79
50
  setTimeout(() => process.exit(0), 15000);
80
- await server?.stop();
51
+ await server.close();
81
52
  process.exit(0);
82
53
  });
83
54
  }
84
- const commonTools = [
85
- common.pressKey,
86
- common.wait,
87
- common.pdf,
88
- common.close,
89
- ];
90
- const snapshotTools = [
91
- common.navigate(true),
92
- common.goBack(true),
93
- common.goForward(true),
94
- snapshot.snapshot,
95
- snapshot.click,
96
- snapshot.hover,
97
- snapshot.type,
98
- ...commonTools,
99
- ];
100
- const screenshotTools = [
101
- common.navigate(false),
102
- common.goBack(false),
103
- common.goForward(false),
104
- screenshot.screenshot,
105
- screenshot.moveMouse,
106
- screenshot.click,
107
- screenshot.drag,
108
- screenshot.type,
109
- ...commonTools,
110
- ];
111
- const resources = [
112
- console_1.console,
113
- ];
114
55
  commander_1.program.parse(process.argv);
56
+ async function userDataDir() {
57
+ let cacheDirectory;
58
+ if (process.platform === 'linux')
59
+ cacheDirectory = process.env.XDG_CACHE_HOME || path_1.default.join(os_1.default.homedir(), '.cache');
60
+ else if (process.platform === 'darwin')
61
+ cacheDirectory = path_1.default.join(os_1.default.homedir(), 'Library', 'Caches');
62
+ else if (process.platform === 'win32')
63
+ cacheDirectory = process.env.LOCALAPPDATA || path_1.default.join(os_1.default.homedir(), 'AppData', 'Local');
64
+ else
65
+ throw new Error('Unsupported platform: ' + process.platform);
66
+ const result = path_1.default.join(cacheDirectory, 'ms-playwright', 'mcp-chrome-profile');
67
+ await fs_1.default.promises.mkdir(result, { recursive: true });
68
+ return result;
69
+ }
@@ -23,14 +23,12 @@ exports.console = {
23
23
  mimeType: 'text/plain',
24
24
  },
25
25
  read: async (context, uri) => {
26
- const result = [];
27
- for (const message of await context.ensureConsole()) {
28
- result.push({
26
+ const messages = await context.console();
27
+ const log = messages.map(message => `[${message.type().toUpperCase()}] ${message.text()}`).join('\n');
28
+ return [{
29
29
  uri,
30
30
  mimeType: 'text/plain',
31
- text: `[${message.type().toUpperCase()}] ${message.text()}`,
32
- });
33
- }
34
- return result;
31
+ text: log
32
+ }];
35
33
  },
36
34
  };
package/lib/server.js CHANGED
@@ -15,65 +15,54 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.Server = void 0;
18
+ exports.createServerWithTools = createServerWithTools;
19
19
  const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
20
- const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
21
20
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
22
21
  const context_1 = require("./context");
23
- class Server {
24
- _server;
25
- _tools;
26
- _context;
27
- constructor(options, launchOptions) {
28
- const { name, version, tools, resources } = options;
29
- this._context = new context_1.Context(launchOptions);
30
- this._server = new index_js_1.Server({ name, version }, {
31
- capabilities: {
32
- tools: {},
33
- resources: {},
34
- }
35
- });
36
- this._tools = tools;
37
- this._server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
38
- return { tools: tools.map(tool => tool.schema) };
39
- });
40
- this._server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
41
- return { resources: resources.map(resource => resource.schema) };
42
- });
43
- this._server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
44
- const tool = this._tools.find(tool => tool.schema.name === request.params.name);
45
- if (!tool) {
46
- return {
47
- content: [{ type: 'text', text: `Tool "${request.params.name}" not found` }],
48
- isError: true,
49
- };
50
- }
51
- try {
52
- const result = await tool.handle(this._context, request.params.arguments);
53
- return result;
54
- }
55
- catch (error) {
56
- return {
57
- content: [{ type: 'text', text: String(error) }],
58
- isError: true,
59
- };
60
- }
61
- });
62
- this._server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) => {
63
- const resource = resources.find(resource => resource.schema.uri === request.params.uri);
64
- if (!resource)
65
- return { contents: [] };
66
- const contents = await resource.read(this._context, request.params.uri);
67
- return { contents };
68
- });
69
- }
70
- async start() {
71
- const transport = new stdio_js_1.StdioServerTransport();
72
- await this._server.connect(transport);
73
- }
74
- async stop() {
75
- await this._server.close();
76
- await this._context.close();
77
- }
22
+ function createServerWithTools(options) {
23
+ const { name, version, tools, resources, userDataDir, launchOptions } = options;
24
+ const context = new context_1.Context(userDataDir, launchOptions);
25
+ const server = new index_js_1.Server({ name, version }, {
26
+ capabilities: {
27
+ tools: {},
28
+ resources: {},
29
+ }
30
+ });
31
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
32
+ return { tools: tools.map(tool => tool.schema) };
33
+ });
34
+ server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
35
+ return { resources: resources.map(resource => resource.schema) };
36
+ });
37
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
38
+ const tool = tools.find(tool => tool.schema.name === request.params.name);
39
+ if (!tool) {
40
+ return {
41
+ content: [{ type: 'text', text: `Tool "${request.params.name}" not found` }],
42
+ isError: true,
43
+ };
44
+ }
45
+ try {
46
+ const result = await tool.handle(context, request.params.arguments);
47
+ return result;
48
+ }
49
+ catch (error) {
50
+ return {
51
+ content: [{ type: 'text', text: String(error) }],
52
+ isError: true,
53
+ };
54
+ }
55
+ });
56
+ server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) => {
57
+ const resource = resources.find(resource => resource.schema.uri === request.params.uri);
58
+ if (!resource)
59
+ return { contents: [] };
60
+ const contents = await resource.read(context, request.params.uri);
61
+ return { contents };
62
+ });
63
+ server.close = async () => {
64
+ await server.close();
65
+ await context.close();
66
+ };
67
+ return server;
78
68
  }
79
- exports.Server = Server;
@@ -35,7 +35,7 @@ const navigate = snapshot => ({
35
35
  },
36
36
  handle: async (context, params) => {
37
37
  const validatedParams = navigateSchema.parse(params);
38
- const page = await context.ensurePage();
38
+ const page = await context.createPage();
39
39
  await page.goto(validatedParams.url, { waitUntil: 'domcontentloaded' });
40
40
  // Cap load event to 5 seconds, the page is operational at this point.
41
41
  await page.waitForLoadState('load', { timeout: 5000 }).catch(() => { });
@@ -58,10 +58,7 @@ const goBack = snapshot => ({
58
58
  inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(goBackSchema),
59
59
  },
60
60
  handle: async (context) => {
61
- return await (0, utils_1.runAndWait)(context, 'Navigated back', async () => {
62
- const page = await context.ensurePage();
63
- await page.goBack();
64
- }, snapshot);
61
+ return await (0, utils_1.runAndWait)(context, 'Navigated back', async (page) => page.goBack(), snapshot);
65
62
  },
66
63
  });
67
64
  exports.goBack = goBack;
@@ -73,10 +70,7 @@ const goForward = snapshot => ({
73
70
  inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(goForwardSchema),
74
71
  },
75
72
  handle: async (context) => {
76
- return await (0, utils_1.runAndWait)(context, 'Navigated forward', async () => {
77
- const page = await context.ensurePage();
78
- await page.goForward();
79
- }, snapshot);
73
+ return await (0, utils_1.runAndWait)(context, 'Navigated forward', async (page) => page.goForward(), snapshot);
80
74
  },
81
75
  });
82
76
  exports.goForward = goForward;
@@ -91,8 +85,7 @@ exports.wait = {
91
85
  },
92
86
  handle: async (context, params) => {
93
87
  const validatedParams = waitSchema.parse(params);
94
- const page = await context.ensurePage();
95
- await page.waitForTimeout(Math.min(10000, validatedParams.time * 1000));
88
+ await new Promise(f => setTimeout(f, Math.min(10000, validatedParams.time * 1000)));
96
89
  return {
97
90
  content: [{
98
91
  type: 'text',
@@ -125,7 +118,7 @@ exports.pdf = {
125
118
  inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(pdfSchema),
126
119
  },
127
120
  handle: async (context) => {
128
- const page = await context.ensurePage();
121
+ const page = context.existingPage();
129
122
  const fileName = path_1.default.join(os_1.default.tmpdir(), `/page-${new Date().toISOString()}.pdf`);
130
123
  await page.pdf({ path: fileName });
131
124
  return {
@@ -26,7 +26,7 @@ exports.screenshot = {
26
26
  inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({})),
27
27
  },
28
28
  handle: async (context) => {
29
- const page = await context.ensurePage();
29
+ const page = context.existingPage();
30
30
  const screenshot = await page.screenshot({ type: 'jpeg', quality: 50, scale: 'css' });
31
31
  return {
32
32
  content: [{ type: 'image', data: screenshot.toString('base64'), mimeType: 'image/jpeg' }],
@@ -48,7 +48,7 @@ exports.moveMouse = {
48
48
  },
49
49
  handle: async (context, params) => {
50
50
  const validatedParams = moveMouseSchema.parse(params);
51
- const page = await context.ensurePage();
51
+ const page = context.existingPage();
52
52
  await page.mouse.move(validatedParams.x, validatedParams.y);
53
53
  return {
54
54
  content: [{ type: 'text', text: `Moved mouse to (${validatedParams.x}, ${validatedParams.y})` }],
@@ -18,7 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
18
18
  return (mod && mod.__esModule) ? mod : { "default": mod };
19
19
  };
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.type = exports.hover = exports.drag = exports.click = exports.snapshot = void 0;
21
+ exports.screenshot = exports.selectOption = exports.type = exports.hover = exports.drag = exports.click = exports.snapshot = void 0;
22
22
  const zod_1 = require("zod");
23
23
  const zod_to_json_schema_1 = __importDefault(require("zod-to-json-schema"));
24
24
  const utils_1 = require("./utils");
@@ -29,7 +29,7 @@ exports.snapshot = {
29
29
  inputSchema: (0, zod_to_json_schema_1.default)(zod_1.z.object({})),
30
30
  },
31
31
  handle: async (context) => {
32
- return await (0, utils_1.captureAriaSnapshot)(await context.ensurePage());
32
+ return await (0, utils_1.captureAriaSnapshot)(context.existingPage());
33
33
  },
34
34
  };
35
35
  const elementSchema = zod_1.z.object({
@@ -99,6 +99,49 @@ exports.type = {
99
99
  }, true);
100
100
  },
101
101
  };
102
+ const selectOptionSchema = elementSchema.extend({
103
+ values: zod_1.z.array(zod_1.z.string()).describe('Array of values to select in the dropdown. This can be a single value or multiple values.'),
104
+ });
105
+ exports.selectOption = {
106
+ schema: {
107
+ name: 'browser_select_option',
108
+ description: 'Select an option in a dropdown',
109
+ inputSchema: (0, zod_to_json_schema_1.default)(selectOptionSchema),
110
+ },
111
+ handle: async (context, params) => {
112
+ const validatedParams = selectOptionSchema.parse(params);
113
+ return await (0, utils_1.runAndWait)(context, `Selected option in "${validatedParams.element}"`, async (page) => {
114
+ const locator = refLocator(page, validatedParams.ref);
115
+ await locator.selectOption(validatedParams.values);
116
+ }, true);
117
+ },
118
+ };
119
+ const screenshotSchema = zod_1.z.object({
120
+ raw: zod_1.z.boolean().optional().describe('Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.'),
121
+ });
122
+ exports.screenshot = {
123
+ schema: {
124
+ name: 'browser_take_screenshot',
125
+ description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
126
+ inputSchema: (0, zod_to_json_schema_1.default)(screenshotSchema),
127
+ },
128
+ handle: async (context, params) => {
129
+ const validatedParams = screenshotSchema.parse(params);
130
+ const page = context.existingPage();
131
+ const options = validatedParams.raw ? { type: 'png', scale: 'css' } : { type: 'jpeg', quality: 50, scale: 'css' };
132
+ const screenshot = await page.screenshot(options);
133
+ return {
134
+ content: [{ type: 'image', data: screenshot.toString('base64'), mimeType: validatedParams.raw ? 'image/png' : 'image/jpeg' }],
135
+ };
136
+ },
137
+ };
102
138
  function refLocator(page, ref) {
103
- return page.locator(`aria-ref=${ref}`);
139
+ let frame = page.frames()[0];
140
+ const match = ref.match(/^f(\d+)(.*)/);
141
+ if (match) {
142
+ const frameIndex = parseInt(match[1], 10);
143
+ frame = page.frames()[frameIndex];
144
+ ref = match[2];
145
+ }
146
+ return frame.locator(`aria-ref=${ref}`);
104
147
  }
@@ -16,6 +16,7 @@
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.runAndWait = runAndWait;
19
+ exports.captureAllFrameSnapshot = captureAllFrameSnapshot;
19
20
  exports.captureAriaSnapshot = captureAriaSnapshot;
20
21
  async function waitForCompletion(page, callback) {
21
22
  const requests = new Set();
@@ -65,21 +66,29 @@ async function waitForCompletion(page, callback) {
65
66
  }
66
67
  }
67
68
  async function runAndWait(context, status, callback, snapshot = false) {
68
- const page = await context.ensurePage();
69
+ const page = context.existingPage();
69
70
  await waitForCompletion(page, () => callback(page));
70
71
  return snapshot ? captureAriaSnapshot(page, status) : {
71
72
  content: [{ type: 'text', text: status }],
72
73
  };
73
74
  }
75
+ async function captureAllFrameSnapshot(page) {
76
+ const snapshots = await Promise.all(page.frames().map(frame => frame.locator('html').ariaSnapshot({ ref: true })));
77
+ const scopedSnapshots = snapshots.map((snapshot, frameIndex) => {
78
+ if (frameIndex === 0)
79
+ return snapshot;
80
+ return snapshot.replaceAll('[ref=', `[ref=f${frameIndex}`);
81
+ });
82
+ return scopedSnapshots.join('\n');
83
+ }
74
84
  async function captureAriaSnapshot(page, status = '') {
75
- const snapshot = await page.locator('html').ariaSnapshot({ ref: true });
76
85
  return {
77
86
  content: [{ type: 'text', text: `${status ? `${status}\n` : ''}
78
87
  - Page URL: ${page.url()}
79
88
  - Page Title: ${await page.title()}
80
89
  - Page Snapshot
81
90
  \`\`\`yaml
82
- ${snapshot}
91
+ ${await captureAllFrameSnapshot(page)}
83
92
  \`\`\`
84
93
  `
85
94
  }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playwright/mcp",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Playwright Tools for MCP",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,27 +18,27 @@
18
18
  "build": "tsc",
19
19
  "lint": "eslint .",
20
20
  "watch": "tsc --watch",
21
- "test": "playwright test"
21
+ "test": "playwright test",
22
+ "clean": "rm -rf lib",
23
+ "publish": "npm run clean && npm run build && npm run test && npm publish"
22
24
  },
23
25
  "exports": {
24
- "./servers/server": "./lib/servers/server.js",
25
- "./servers/screenshot": "./lib/servers/screenshot.js",
26
- "./servers/snapshot": "./lib/servers/snapshot.js",
27
- "./tools/common": "./lib/tools/common.js",
28
- "./tools/screenshot": "./lib/tools/screenshot.js",
29
- "./tools/snapshot": "./lib/tools/snapshot.js",
30
- "./package.json": "./package.json"
26
+ "./package.json": "./package.json",
27
+ ".": {
28
+ "types": "./index.d.ts",
29
+ "default": "./index.js"
30
+ }
31
31
  },
32
32
  "dependencies": {
33
33
  "@modelcontextprotocol/sdk": "^1.6.1",
34
34
  "commander": "^13.1.0",
35
- "playwright": "1.52.0-alpha-2025-03-21",
35
+ "playwright": "1.52.0-alpha-1743011787000",
36
36
  "zod-to-json-schema": "^3.24.4"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@eslint/eslintrc": "^3.2.0",
40
40
  "@eslint/js": "^9.19.0",
41
- "@playwright/test": "1.52.0-alpha-2025-03-21",
41
+ "@playwright/test": "1.52.0-alpha-1743011787000",
42
42
  "@stylistic/eslint-plugin": "^3.0.1",
43
43
  "@typescript-eslint/eslint-plugin": "^8.26.1",
44
44
  "@typescript-eslint/parser": "^8.26.1",
@@ -50,6 +50,6 @@
50
50
  "typescript": "^5.8.2"
51
51
  },
52
52
  "bin": {
53
- "mcp": "cli.js"
53
+ "mcp-server-playwright": "cli.js"
54
54
  }
55
55
  }