@playwright/mcp 0.0.28 → 0.0.29

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
@@ -52,6 +52,12 @@ After installation, the Playwright MCP server will be available for use with you
52
52
  <details>
53
53
  <summary><b>Install in Cursor</b></summary>
54
54
 
55
+ #### Click the button to install:
56
+
57
+ [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=playwright&config=eyJjb21tYW5kIjoibnB4IEBwbGF5d3JpZ2h0L21jcEBsYXRlc3QifQ%3D%3D)
58
+
59
+ #### Or install manually:
60
+
55
61
  Go to `Cursor Settings` -> `MCP` -> `Add new MCP Server`. Name to your liking, use `command` type with the command `npx @playwright/mcp`. You can also verify config or add command like arguments via clicking `Edit`.
56
62
 
57
63
  ```js
@@ -106,6 +112,27 @@ Follow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user),
106
112
  ```
107
113
  </details>
108
114
 
115
+ <details>
116
+ <summary><b>Install in Qodo Gen</b></summary>
117
+
118
+ Open [Qodo Gen](https://docs.qodo.ai/qodo-documentation/qodo-gen) chat panel in VSCode or IntelliJ → Connect more tools → + Add new MCP → Paste the following configuration:
119
+
120
+ ```js
121
+ {
122
+ "mcpServers": {
123
+ "playwright": {
124
+ "command": "npx",
125
+ "args": [
126
+ "@playwright/mcp@latest"
127
+ ]
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ Click <code>Save</code>.
134
+ </details>
135
+
109
136
  ### Configuration
110
137
 
111
138
  Playwright MCP server supports following arguments. They can be provided in the JSON configuration above, as a part of the `"args"` list:
@@ -124,6 +151,7 @@ Playwright MCP server supports following arguments. They can be provided in the
124
151
  --block-service-workers block service workers
125
152
  --browser <browser> browser or chrome channel to use, possible
126
153
  values: chrome, firefox, webkit, msedge.
154
+ --browser-agent <endpoint> Use browser agent (experimental).
127
155
  --caps <caps> comma-separated list of capabilities to enable,
128
156
  possible values: tabs, pdf, history, wait, files,
129
157
  install. Default is all.
@@ -354,7 +382,7 @@ http.createServer(async (req, res) => {
354
382
  // Creates a headless Playwright MCP server with SSE transport
355
383
  const connection = await createConnection({ browser: { launchOptions: { headless: true } } });
356
384
  const transport = new SSEServerTransport('/messages', res);
357
- await connection.connect(transport);
385
+ await connection.sever.connect(transport);
358
386
 
359
387
  // ...
360
388
  });
package/config.d.ts CHANGED
@@ -23,6 +23,11 @@ export type Config = {
23
23
  * The browser to use.
24
24
  */
25
25
  browser?: {
26
+ /**
27
+ * Use browser agent (experimental).
28
+ */
29
+ browserAgent?: string;
30
+
26
31
  /**
27
32
  * The type of browser to use.
28
33
  */
package/index.d.ts CHANGED
@@ -16,13 +16,11 @@
16
16
  */
17
17
 
18
18
  import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
19
- import type { Config } from './config';
20
- import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
19
+ import type { Config } from './config.js';
21
20
  import type { BrowserContext } from 'playwright';
22
21
 
23
22
  export type Connection = {
24
23
  server: Server;
25
- connect(transport: Transport): Promise<void>;
26
24
  close(): Promise<void>;
27
25
  };
28
26
 
@@ -14,10 +14,12 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import fs from 'node:fs';
17
- import os from 'node:os';
17
+ import net from 'node:net';
18
18
  import path from 'node:path';
19
+ import os from 'node:os';
19
20
  import debug from 'debug';
20
21
  import * as playwright from 'playwright';
22
+ import { userDataDir } from './fileUtils.js';
21
23
  const testDebug = debug('pw:mcp:test');
22
24
  export function contextFactory(browserConfig) {
23
25
  if (browserConfig.remoteEndpoint)
@@ -26,6 +28,8 @@ export function contextFactory(browserConfig) {
26
28
  return new CdpContextFactory(browserConfig);
27
29
  if (browserConfig.isolated)
28
30
  return new IsolatedContextFactory(browserConfig);
31
+ if (browserConfig.browserAgent)
32
+ return new BrowserServerContextFactory(browserConfig);
29
33
  return new PersistentContextFactory(browserConfig);
30
34
  }
31
35
  class BaseContextFactory {
@@ -78,6 +82,7 @@ class IsolatedContextFactory extends BaseContextFactory {
78
82
  super('isolated', browserConfig);
79
83
  }
80
84
  async _doObtainBrowser() {
85
+ await injectCdpPort(this.browserConfig);
81
86
  const browserType = playwright[this.browserConfig.browserName];
82
87
  return browserType.launch({
83
88
  ...this.browserConfig.launchOptions,
@@ -126,6 +131,7 @@ class PersistentContextFactory {
126
131
  this.browserConfig = browserConfig;
127
132
  }
128
133
  async createContext() {
134
+ await injectCdpPort(this.browserConfig);
129
135
  testDebug('create browser context (persistent)');
130
136
  const userDataDir = this.browserConfig.userDataDir ?? await this._createUserDataDir();
131
137
  this._userDataDirs.add(userDataDir);
@@ -177,3 +183,45 @@ class PersistentContextFactory {
177
183
  return result;
178
184
  }
179
185
  }
186
+ export class BrowserServerContextFactory extends BaseContextFactory {
187
+ constructor(browserConfig) {
188
+ super('persistent', browserConfig);
189
+ }
190
+ async _doObtainBrowser() {
191
+ const response = await fetch(new URL(`/json/launch`, this.browserConfig.browserAgent), {
192
+ method: 'POST',
193
+ body: JSON.stringify({
194
+ browserType: this.browserConfig.browserName,
195
+ userDataDir: this.browserConfig.userDataDir ?? await this._createUserDataDir(),
196
+ launchOptions: this.browserConfig.launchOptions,
197
+ contextOptions: this.browserConfig.contextOptions,
198
+ }),
199
+ });
200
+ const info = await response.json();
201
+ if (info.error)
202
+ throw new Error(info.error);
203
+ return await playwright.chromium.connectOverCDP(`http://localhost:${info.cdpPort}/`);
204
+ }
205
+ async _doCreateContext(browser) {
206
+ return this.browserConfig.isolated ? await browser.newContext() : browser.contexts()[0];
207
+ }
208
+ async _createUserDataDir() {
209
+ const dir = await userDataDir(this.browserConfig);
210
+ await fs.promises.mkdir(dir, { recursive: true });
211
+ return dir;
212
+ }
213
+ }
214
+ async function injectCdpPort(browserConfig) {
215
+ if (browserConfig.browserName === 'chromium')
216
+ browserConfig.launchOptions.cdpPort = await findFreePort();
217
+ }
218
+ async function findFreePort() {
219
+ return new Promise((resolve, reject) => {
220
+ const server = net.createServer();
221
+ server.listen(0, () => {
222
+ const { port } = server.address();
223
+ server.close(() => resolve(port));
224
+ });
225
+ server.on('error', reject);
226
+ });
227
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /* eslint-disable no-console */
17
+ import net from 'net';
18
+ import { program } from 'commander';
19
+ import playwright from 'playwright';
20
+ import { HttpServer } from './httpServer.js';
21
+ import { packageJSON } from './package.js';
22
+ class BrowserServer {
23
+ _server = new HttpServer();
24
+ _entries = [];
25
+ constructor() {
26
+ this._setupExitHandler();
27
+ }
28
+ async start(port) {
29
+ await this._server.start({ port });
30
+ this._server.routePath('/json/list', (req, res) => {
31
+ this._handleJsonList(res);
32
+ });
33
+ this._server.routePath('/json/launch', async (req, res) => {
34
+ void this._handleLaunchBrowser(req, res).catch(e => console.error(e));
35
+ });
36
+ this._setEntries([]);
37
+ }
38
+ _handleJsonList(res) {
39
+ const list = this._entries.map(browser => browser.info);
40
+ res.end(JSON.stringify(list));
41
+ }
42
+ async _handleLaunchBrowser(req, res) {
43
+ const request = await readBody(req);
44
+ let info = this._entries.map(entry => entry.info).find(info => info.userDataDir === request.userDataDir);
45
+ if (!info || info.error)
46
+ info = await this._newBrowser(request);
47
+ res.end(JSON.stringify(info));
48
+ }
49
+ async _newBrowser(request) {
50
+ const cdpPort = await findFreePort();
51
+ request.launchOptions.cdpPort = cdpPort;
52
+ const info = {
53
+ browserType: request.browserType,
54
+ userDataDir: request.userDataDir,
55
+ cdpPort,
56
+ launchOptions: request.launchOptions,
57
+ contextOptions: request.contextOptions,
58
+ };
59
+ const browserType = playwright[request.browserType];
60
+ const { browser, error } = await browserType.launchPersistentContext(request.userDataDir, {
61
+ ...request.launchOptions,
62
+ ...request.contextOptions,
63
+ handleSIGINT: false,
64
+ handleSIGTERM: false,
65
+ }).then(context => {
66
+ return { browser: context.browser(), error: undefined };
67
+ }).catch(error => {
68
+ return { browser: undefined, error: error.message };
69
+ });
70
+ this._setEntries([...this._entries, {
71
+ browser,
72
+ info: {
73
+ browserType: request.browserType,
74
+ userDataDir: request.userDataDir,
75
+ cdpPort,
76
+ launchOptions: request.launchOptions,
77
+ contextOptions: request.contextOptions,
78
+ error,
79
+ },
80
+ }]);
81
+ browser?.on('disconnected', () => {
82
+ this._setEntries(this._entries.filter(entry => entry.browser !== browser));
83
+ });
84
+ return info;
85
+ }
86
+ _updateReport() {
87
+ // Clear the current line and move cursor to top of screen
88
+ process.stdout.write('\x1b[2J\x1b[H');
89
+ process.stdout.write(`Playwright Browser Server v${packageJSON.version}\n`);
90
+ process.stdout.write(`Listening on ${this._server.urlPrefix('human-readable')}\n\n`);
91
+ if (this._entries.length === 0) {
92
+ process.stdout.write('No browsers currently running\n');
93
+ return;
94
+ }
95
+ process.stdout.write('Running browsers:\n');
96
+ for (const entry of this._entries) {
97
+ const status = entry.browser ? 'running' : 'error';
98
+ const statusColor = entry.browser ? '\x1b[32m' : '\x1b[31m'; // green for running, red for error
99
+ process.stdout.write(`${statusColor}${entry.info.browserType}\x1b[0m (${entry.info.userDataDir}) - ${statusColor}${status}\x1b[0m\n`);
100
+ if (entry.info.error)
101
+ process.stdout.write(` Error: ${entry.info.error}\n`);
102
+ }
103
+ }
104
+ _setEntries(entries) {
105
+ this._entries = entries;
106
+ this._updateReport();
107
+ }
108
+ _setupExitHandler() {
109
+ let isExiting = false;
110
+ const handleExit = async () => {
111
+ if (isExiting)
112
+ return;
113
+ isExiting = true;
114
+ setTimeout(() => process.exit(0), 15000);
115
+ for (const entry of this._entries)
116
+ await entry.browser?.close().catch(() => { });
117
+ process.exit(0);
118
+ };
119
+ process.stdin.on('close', handleExit);
120
+ process.on('SIGINT', handleExit);
121
+ process.on('SIGTERM', handleExit);
122
+ }
123
+ }
124
+ program
125
+ .name('browser-agent')
126
+ .option('-p, --port <port>', 'Port to listen on', '9224')
127
+ .action(async (options) => {
128
+ await main(options);
129
+ });
130
+ void program.parseAsync(process.argv);
131
+ async function main(options) {
132
+ const server = new BrowserServer();
133
+ await server.start(+options.port);
134
+ }
135
+ function readBody(req) {
136
+ return new Promise((resolve, reject) => {
137
+ const chunks = [];
138
+ req.on('data', (chunk) => chunks.push(chunk));
139
+ req.on('end', () => resolve(JSON.parse(Buffer.concat(chunks).toString())));
140
+ });
141
+ }
142
+ async function findFreePort() {
143
+ return new Promise((resolve, reject) => {
144
+ const server = net.createServer();
145
+ server.listen(0, () => {
146
+ const { port } = server.address();
147
+ server.close(() => resolve(port));
148
+ });
149
+ server.on('error', reject);
150
+ });
151
+ }
package/lib/config.js CHANGED
@@ -14,7 +14,6 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import fs from 'fs';
17
- import net from 'net';
18
17
  import os from 'os';
19
18
  import path from 'path';
20
19
  import { devices } from 'playwright';
@@ -48,8 +47,6 @@ export async function resolveCLIConfig(cliOptions) {
48
47
  // Derive artifact output directory from config.outputDir
49
48
  if (result.saveTrace)
50
49
  result.browser.launchOptions.tracesDir = path.join(result.outputDir, 'traces');
51
- if (result.browser.browserName === 'chromium')
52
- result.browser.launchOptions.cdpPort = await findFreePort();
53
50
  return result;
54
51
  }
55
52
  export async function configFromCLIOptions(cliOptions) {
@@ -114,6 +111,7 @@ export async function configFromCLIOptions(cliOptions) {
114
111
  contextOptions.serviceWorkers = 'block';
115
112
  const result = {
116
113
  browser: {
114
+ browserAgent: cliOptions.browserAgent ?? process.env.PW_BROWSER_AGENT,
117
115
  browserName,
118
116
  isolated: cliOptions.isolated,
119
117
  userDataDir: cliOptions.userDataDir,
@@ -137,16 +135,6 @@ export async function configFromCLIOptions(cliOptions) {
137
135
  };
138
136
  return result;
139
137
  }
140
- async function findFreePort() {
141
- return new Promise((resolve, reject) => {
142
- const server = net.createServer();
143
- server.listen(0, () => {
144
- const { port } = server.address();
145
- server.close(() => resolve(port));
146
- });
147
- server.on('error', reject);
148
- });
149
- }
150
138
  async function loadConfig(configFile) {
151
139
  if (!configFile)
152
140
  return {};
@@ -167,6 +155,8 @@ function pickDefined(obj) {
167
155
  }
168
156
  function mergeConfig(base, overrides) {
169
157
  const browser = {
158
+ ...pickDefined(base.browser),
159
+ ...pickDefined(overrides.browser),
170
160
  browserName: overrides.browser?.browserName ?? base.browser?.browserName ?? 'chromium',
171
161
  isolated: overrides.browser?.isolated ?? base.browser?.isolated ?? false,
172
162
  launchOptions: {
@@ -178,9 +168,6 @@ function mergeConfig(base, overrides) {
178
168
  ...pickDefined(base.browser?.contextOptions),
179
169
  ...pickDefined(overrides.browser?.contextOptions),
180
170
  },
181
- userDataDir: overrides.browser?.userDataDir ?? base.browser?.userDataDir,
182
- cdpEndpoint: overrides.browser?.cdpEndpoint ?? base.browser?.cdpEndpoint,
183
- remoteEndpoint: overrides.browser?.remoteEndpoint ?? base.browser?.remoteEndpoint,
184
171
  };
185
172
  if (browser.browserName !== 'chromium' && browser.launchOptions)
186
173
  delete browser.launchOptions.channel;
package/lib/context.js CHANGED
@@ -67,7 +67,7 @@ export class Context {
67
67
  }
68
68
  currentTabOrDie() {
69
69
  if (!this._currentTab)
70
- throw new Error('No current snapshot available. Capture a snapshot of navigate to a new location first.');
70
+ throw new Error('No current snapshot available. Capture a snapshot or navigate to a new location first.');
71
71
  return this._currentTab;
72
72
  }
73
73
  async newTab() {
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+ export function cacheDir() {
19
+ let cacheDirectory;
20
+ if (process.platform === 'linux')
21
+ cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
22
+ else if (process.platform === 'darwin')
23
+ cacheDirectory = path.join(os.homedir(), 'Library', 'Caches');
24
+ else if (process.platform === 'win32')
25
+ cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
26
+ else
27
+ throw new Error('Unsupported platform: ' + process.platform);
28
+ return path.join(cacheDirectory, 'ms-playwright');
29
+ }
30
+ export async function userDataDir(browserConfig) {
31
+ return path.join(cacheDir(), 'ms-playwright', `mcp-${browserConfig.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
32
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import fs from 'fs';
17
+ import path from 'path';
18
+ import http from 'http';
19
+ import mime from 'mime';
20
+ import { ManualPromise } from './manualPromise.js';
21
+ export class HttpServer {
22
+ _server;
23
+ _urlPrefixPrecise = '';
24
+ _urlPrefixHumanReadable = '';
25
+ _port = 0;
26
+ _routes = [];
27
+ constructor() {
28
+ this._server = http.createServer(this._onRequest.bind(this));
29
+ decorateServer(this._server);
30
+ }
31
+ server() {
32
+ return this._server;
33
+ }
34
+ routePrefix(prefix, handler) {
35
+ this._routes.push({ prefix, handler });
36
+ }
37
+ routePath(path, handler) {
38
+ this._routes.push({ exact: path, handler });
39
+ }
40
+ port() {
41
+ return this._port;
42
+ }
43
+ async _tryStart(port, host) {
44
+ const errorPromise = new ManualPromise();
45
+ const errorListener = (error) => errorPromise.reject(error);
46
+ this._server.on('error', errorListener);
47
+ try {
48
+ this._server.listen(port, host);
49
+ await Promise.race([
50
+ new Promise(cb => this._server.once('listening', cb)),
51
+ errorPromise,
52
+ ]);
53
+ }
54
+ finally {
55
+ this._server.removeListener('error', errorListener);
56
+ }
57
+ }
58
+ async start(options = {}) {
59
+ const host = options.host || 'localhost';
60
+ if (options.preferredPort) {
61
+ try {
62
+ await this._tryStart(options.preferredPort, host);
63
+ }
64
+ catch (e) {
65
+ if (!e || !e.message || !e.message.includes('EADDRINUSE'))
66
+ throw e;
67
+ await this._tryStart(undefined, host);
68
+ }
69
+ }
70
+ else {
71
+ await this._tryStart(options.port, host);
72
+ }
73
+ const address = this._server.address();
74
+ if (typeof address === 'string') {
75
+ this._urlPrefixPrecise = address;
76
+ this._urlPrefixHumanReadable = address;
77
+ }
78
+ else {
79
+ this._port = address.port;
80
+ const resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
81
+ this._urlPrefixPrecise = `http://${resolvedHost}:${address.port}`;
82
+ this._urlPrefixHumanReadable = `http://${host}:${address.port}`;
83
+ }
84
+ }
85
+ async stop() {
86
+ await new Promise(cb => this._server.close(cb));
87
+ }
88
+ urlPrefix(purpose) {
89
+ return purpose === 'human-readable' ? this._urlPrefixHumanReadable : this._urlPrefixPrecise;
90
+ }
91
+ serveFile(request, response, absoluteFilePath, headers) {
92
+ try {
93
+ for (const [name, value] of Object.entries(headers || {}))
94
+ response.setHeader(name, value);
95
+ if (request.headers.range)
96
+ this._serveRangeFile(request, response, absoluteFilePath);
97
+ else
98
+ this._serveFile(response, absoluteFilePath);
99
+ return true;
100
+ }
101
+ catch (e) {
102
+ return false;
103
+ }
104
+ }
105
+ _serveFile(response, absoluteFilePath) {
106
+ const content = fs.readFileSync(absoluteFilePath);
107
+ response.statusCode = 200;
108
+ const contentType = mime.getType(path.extname(absoluteFilePath)) || 'application/octet-stream';
109
+ response.setHeader('Content-Type', contentType);
110
+ response.setHeader('Content-Length', content.byteLength);
111
+ response.end(content);
112
+ }
113
+ _serveRangeFile(request, response, absoluteFilePath) {
114
+ const range = request.headers.range;
115
+ if (!range || !range.startsWith('bytes=') || range.includes(', ') || [...range].filter(char => char === '-').length !== 1) {
116
+ response.statusCode = 400;
117
+ return response.end('Bad request');
118
+ }
119
+ // Parse the range header: https://datatracker.ietf.org/doc/html/rfc7233#section-2.1
120
+ const [startStr, endStr] = range.replace(/bytes=/, '').split('-');
121
+ // Both start and end (when passing to fs.createReadStream) and the range header are inclusive and start counting at 0.
122
+ let start;
123
+ let end;
124
+ const size = fs.statSync(absoluteFilePath).size;
125
+ if (startStr !== '' && endStr === '') {
126
+ // No end specified: use the whole file
127
+ start = +startStr;
128
+ end = size - 1;
129
+ }
130
+ else if (startStr === '' && endStr !== '') {
131
+ // No start specified: calculate start manually
132
+ start = size - +endStr;
133
+ end = size - 1;
134
+ }
135
+ else {
136
+ start = +startStr;
137
+ end = +endStr;
138
+ }
139
+ // Handle unavailable range request
140
+ if (Number.isNaN(start) || Number.isNaN(end) || start >= size || end >= size || start > end) {
141
+ // Return the 416 Range Not Satisfiable: https://datatracker.ietf.org/doc/html/rfc7233#section-4.4
142
+ response.writeHead(416, {
143
+ 'Content-Range': `bytes */${size}`
144
+ });
145
+ return response.end();
146
+ }
147
+ // Sending Partial Content: https://datatracker.ietf.org/doc/html/rfc7233#section-4.1
148
+ response.writeHead(206, {
149
+ 'Content-Range': `bytes ${start}-${end}/${size}`,
150
+ 'Accept-Ranges': 'bytes',
151
+ 'Content-Length': end - start + 1,
152
+ 'Content-Type': mime.getType(path.extname(absoluteFilePath)),
153
+ });
154
+ const readable = fs.createReadStream(absoluteFilePath, { start, end });
155
+ readable.pipe(response);
156
+ }
157
+ _onRequest(request, response) {
158
+ if (request.method === 'OPTIONS') {
159
+ response.writeHead(200);
160
+ response.end();
161
+ return;
162
+ }
163
+ request.on('error', () => response.end());
164
+ try {
165
+ if (!request.url) {
166
+ response.end();
167
+ return;
168
+ }
169
+ const url = new URL('http://localhost' + request.url);
170
+ for (const route of this._routes) {
171
+ if (route.exact && url.pathname === route.exact) {
172
+ route.handler(request, response);
173
+ return;
174
+ }
175
+ if (route.prefix && url.pathname.startsWith(route.prefix)) {
176
+ route.handler(request, response);
177
+ return;
178
+ }
179
+ }
180
+ response.statusCode = 404;
181
+ response.end();
182
+ }
183
+ catch (e) {
184
+ response.end();
185
+ }
186
+ }
187
+ }
188
+ function decorateServer(server) {
189
+ const sockets = new Set();
190
+ server.on('connection', socket => {
191
+ sockets.add(socket);
192
+ socket.once('close', () => sockets.delete(socket));
193
+ });
194
+ const close = server.close;
195
+ server.close = (callback) => {
196
+ for (const socket of sockets)
197
+ socket.destroy();
198
+ sockets.clear();
199
+ return close.call(server, callback);
200
+ };
201
+ }
package/lib/program.js CHANGED
@@ -27,6 +27,7 @@ program
27
27
  .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.', semicolonSeparatedList)
28
28
  .option('--block-service-workers', 'block service workers')
29
29
  .option('--browser <browser>', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
30
+ .option('--browser-agent <endpoint>', 'Use browser agent (experimental).')
30
31
  .option('--caps <caps>', 'comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.')
31
32
  .option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
32
33
  .option('--config <path>', 'path to the configuration file.')
package/lib/tab.js CHANGED
@@ -73,7 +73,7 @@ export class Tab {
73
73
  // on chromium, the download event is fired *after* page.goto rejects, so we wait a lil bit
74
74
  const download = await Promise.race([
75
75
  downloadEvent,
76
- new Promise(resolve => setTimeout(resolve, 500)),
76
+ new Promise(resolve => setTimeout(resolve, 1000)),
77
77
  ]);
78
78
  if (!download)
79
79
  throw e;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playwright/mcp",
3
- "version": "0.0.28",
3
+ "version": "0.0.29",
4
4
  "description": "Playwright Tools for MCP",
5
5
  "type": "module",
6
6
  "repository": {
@@ -24,6 +24,7 @@
24
24
  "ctest": "playwright test --project=chrome",
25
25
  "ftest": "playwright test --project=firefox",
26
26
  "wtest": "playwright test --project=webkit",
27
+ "run-server": "node lib/browserServer.js",
27
28
  "clean": "rm -rf lib",
28
29
  "npm-publish": "npm run clean && npm run build && npm run test && npm publish"
29
30
  },
@@ -38,13 +39,14 @@
38
39
  "@modelcontextprotocol/sdk": "^1.11.0",
39
40
  "commander": "^13.1.0",
40
41
  "debug": "^4.4.1",
41
- "playwright": "1.53.0-alpha-2025-05-27",
42
+ "mime": "^4.0.7",
43
+ "playwright": "1.53.0",
42
44
  "zod-to-json-schema": "^3.24.4"
43
45
  },
44
46
  "devDependencies": {
45
47
  "@eslint/eslintrc": "^3.2.0",
46
48
  "@eslint/js": "^9.19.0",
47
- "@playwright/test": "1.53.0-alpha-2025-05-27",
49
+ "@playwright/test": "1.53.0",
48
50
  "@stylistic/eslint-plugin": "^3.0.1",
49
51
  "@types/debug": "^4.1.12",
50
52
  "@types/node": "^22.13.10",