@playwright/mcp 0.0.33 → 0.0.34

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
@@ -100,6 +100,29 @@ Go to `Advanced settings` -> `Extensions` -> `Add custom extension`. Name to you
100
100
  Go to `Program` in the right sidebar -> `Install` -> `Edit mcp.json`. Use the standard config above.
101
101
  </details>
102
102
 
103
+ <details>
104
+ <summary>opencode</summary>
105
+
106
+ Follow the MCP Servers [documentation](https://opencode.ai/docs/mcp-servers/). For example in `~/.config/opencode/opencode.json`:
107
+
108
+ ```json
109
+ {
110
+ "$schema": "https://opencode.ai/config.json",
111
+ "mcp": {
112
+ "playwright": {
113
+ "type": "local",
114
+ "command": [
115
+ "npx",
116
+ "@playwright/mcp@latest"
117
+ ],
118
+ "enabled": true
119
+ }
120
+ }
121
+ }
122
+
123
+ ```
124
+ </details>
125
+
103
126
  <details>
104
127
  <summary>Qodo Gen</summary>
105
128
 
@@ -158,6 +181,9 @@ Playwright MCP server supports following arguments. They can be provided in the
158
181
  --config <path> path to the configuration file.
159
182
  --device <device> device to emulate, for example: "iPhone 15"
160
183
  --executable-path <path> path to the browser executable.
184
+ --extension Connect to a running browser instance
185
+ (Edge/Chrome only). Requires the "Playwright MCP
186
+ Bridge" browser extension to be installed.
161
187
  --headless run browser in headless mode, headed by default
162
188
  --host <host> host to bind server to. Default is localhost. Use
163
189
  0.0.0.0 to bind to all interfaces.
@@ -191,7 +217,7 @@ Playwright MCP server supports following arguments. They can be provided in the
191
217
 
192
218
  ### User profile
193
219
 
194
- You can run Playwright MCP with persistent profile like a regular browser (default), or in the isolated contexts for the testing sessions.
220
+ You can run Playwright MCP with persistent profile like a regular browser (default), in isolated contexts for testing sessions, or connect to your existing browser using the browser extension.
195
221
 
196
222
  **Persistent profile**
197
223
 
@@ -231,6 +257,10 @@ state [here](https://playwright.dev/docs/auth).
231
257
  }
232
258
  ```
233
259
 
260
+ **Browser Extension**
261
+
262
+ The Playwright MCP Chrome Extension allows you to connect to existing browser tabs and leverage your logged-in sessions and browser state. See [extension/README.md](extension/README.md) for installation and setup instructions.
263
+
234
264
  ### Configuration file
235
265
 
236
266
  The Playwright MCP server can be configured using a JSON configuration file. You can specify the configuration file
@@ -19,8 +19,10 @@ import path from 'path';
19
19
  import * as playwright from 'playwright';
20
20
  // @ts-ignore
21
21
  import { registryDirectory } from 'playwright-core/lib/server/registry/index';
22
- import { logUnhandledError, testDebug } from './log.js';
23
- import { createHash } from './utils.js';
22
+ // @ts-ignore
23
+ import { startTraceViewerServer } from 'playwright-core/lib/server';
24
+ import { logUnhandledError, testDebug } from './utils/log.js';
25
+ import { createHash } from './utils/guid.js';
24
26
  import { outputFile } from './config.js';
25
27
  export function contextFactory(config) {
26
28
  if (config.browser.remoteEndpoint)
@@ -36,17 +38,16 @@ class BaseContextFactory {
36
38
  description;
37
39
  config;
38
40
  _browserPromise;
39
- _tracesDir;
40
41
  constructor(name, description, config) {
41
42
  this.name = name;
42
43
  this.description = description;
43
44
  this.config = config;
44
45
  }
45
- async _obtainBrowser() {
46
+ async _obtainBrowser(clientInfo) {
46
47
  if (this._browserPromise)
47
48
  return this._browserPromise;
48
49
  testDebug(`obtain browser (${this.name})`);
49
- this._browserPromise = this._doObtainBrowser();
50
+ this._browserPromise = this._doObtainBrowser(clientInfo);
50
51
  void this._browserPromise.then(browser => {
51
52
  browser.on('disconnected', () => {
52
53
  this._browserPromise = undefined;
@@ -56,14 +57,12 @@ class BaseContextFactory {
56
57
  });
57
58
  return this._browserPromise;
58
59
  }
59
- async _doObtainBrowser() {
60
+ async _doObtainBrowser(clientInfo) {
60
61
  throw new Error('Not implemented');
61
62
  }
62
63
  async createContext(clientInfo) {
63
- if (this.config.saveTrace)
64
- this._tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces-${Date.now()}`);
65
64
  testDebug(`create browser context (${this.name})`);
66
- const browser = await this._obtainBrowser();
65
+ const browser = await this._obtainBrowser(clientInfo);
67
66
  const browserContext = await this._doCreateContext(browser);
68
67
  return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) };
69
68
  }
@@ -85,11 +84,11 @@ class IsolatedContextFactory extends BaseContextFactory {
85
84
  constructor(config) {
86
85
  super('isolated', 'Create a new isolated browser context', config);
87
86
  }
88
- async _doObtainBrowser() {
87
+ async _doObtainBrowser(clientInfo) {
89
88
  await injectCdpPort(this.config.browser);
90
89
  const browserType = playwright[this.config.browser.browserName];
91
90
  return browserType.launch({
92
- tracesDir: this._tracesDir,
91
+ tracesDir: await startTraceServer(this.config, clientInfo.rootPath),
93
92
  ...this.config.browser.launchOptions,
94
93
  handleSIGINT: false,
95
94
  handleSIGTERM: false,
@@ -141,9 +140,7 @@ class PersistentContextFactory {
141
140
  await injectCdpPort(this.config.browser);
142
141
  testDebug('create browser context (persistent)');
143
142
  const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo.rootPath);
144
- let tracesDir;
145
- if (this.config.saveTrace)
146
- tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces-${Date.now()}`);
143
+ const tracesDir = await startTraceServer(this.config, clientInfo.rootPath);
147
144
  this._userDataDirs.add(userDataDir);
148
145
  testDebug('lock user data dir', userDataDir);
149
146
  const browserType = playwright[this.config.browser.browserName];
@@ -203,3 +200,14 @@ async function findFreePort() {
203
200
  server.on('error', reject);
204
201
  });
205
202
  }
203
+ async function startTraceServer(config, rootPath) {
204
+ if (!config.saveTrace)
205
+ return undefined;
206
+ const tracesDir = await outputFile(config, rootPath, `traces-${Date.now()}`);
207
+ const server = await startTraceViewerServer();
208
+ const urlPrefix = server.urlPrefix('human-readable');
209
+ const url = urlPrefix + '/trace/index.html?trace=' + tracesDir + '/trace.json';
210
+ // eslint-disable-next-line no-console
211
+ console.error('\nTrace viewer listening on ' + url);
212
+ return tracesDir;
213
+ }
@@ -14,14 +14,13 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { fileURLToPath } from 'url';
17
- import { z } from 'zod';
18
17
  import { Context } from './context.js';
19
- import { logUnhandledError } from './log.js';
18
+ import { logUnhandledError } from './utils/log.js';
20
19
  import { Response } from './response.js';
21
20
  import { SessionLog } from './sessionLog.js';
22
21
  import { filteredTools } from './tools.js';
23
- import { packageJSON } from './package.js';
24
- import { defineTool } from './tools/tool.js';
22
+ import { packageJSON } from './utils/package.js';
23
+ import { toMcpTool } from './mcp/tool.js';
25
24
  export class BrowserServerBackend {
26
25
  name = 'Playwright';
27
26
  version = packageJSON.version;
@@ -30,19 +29,14 @@ export class BrowserServerBackend {
30
29
  _sessionLog;
31
30
  _config;
32
31
  _browserContextFactory;
33
- constructor(config, factories) {
32
+ constructor(config, factory) {
34
33
  this._config = config;
35
- this._browserContextFactory = factories[0];
34
+ this._browserContextFactory = factory;
36
35
  this._tools = filteredTools(config);
37
- if (factories.length > 1)
38
- this._tools.push(this._defineContextSwitchTool(factories));
39
36
  }
40
- async initialize(server) {
41
- const capabilities = server.getClientCapabilities();
37
+ async initialize(clientVersion, roots) {
42
38
  let rootPath;
43
- if (capabilities.roots && (server.getClientVersion()?.name === 'Visual Studio Code' ||
44
- server.getClientVersion()?.name === 'Visual Studio Code - Insiders')) {
45
- const { roots } = await server.listRoots();
39
+ if (roots.length > 0) {
46
40
  const firstRootUri = roots[0]?.uri;
47
41
  const url = firstRootUri ? new URL(firstRootUri) : undefined;
48
42
  rootPath = url ? fileURLToPath(url) : undefined;
@@ -53,16 +47,19 @@ export class BrowserServerBackend {
53
47
  config: this._config,
54
48
  browserContextFactory: this._browserContextFactory,
55
49
  sessionLog: this._sessionLog,
56
- clientInfo: { ...server.getClientVersion(), rootPath },
50
+ clientInfo: { ...clientVersion, rootPath },
57
51
  });
58
52
  }
59
- tools() {
60
- return this._tools.map(tool => tool.schema);
53
+ async listTools() {
54
+ return this._tools.map(tool => toMcpTool(tool.schema));
61
55
  }
62
- async callTool(schema, parsedArguments) {
56
+ async callTool(name, rawArguments) {
57
+ const tool = this._tools.find(tool => tool.schema.name === name);
58
+ if (!tool)
59
+ throw new Error(`Tool "${name}" not found`);
60
+ const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
63
61
  const context = this._context;
64
- const response = new Response(context, schema.name, parsedArguments);
65
- const tool = this._tools.find(tool => tool.schema.name === schema.name);
62
+ const response = new Response(context, name, parsedArguments);
66
63
  context.setRunningTool(true);
67
64
  try {
68
65
  await tool.handle(context, parsedArguments, response);
@@ -78,44 +75,6 @@ export class BrowserServerBackend {
78
75
  return response.serialize();
79
76
  }
80
77
  serverClosed() {
81
- void this._context.dispose().catch(logUnhandledError);
82
- }
83
- _defineContextSwitchTool(factories) {
84
- const self = this;
85
- return defineTool({
86
- capability: 'core',
87
- schema: {
88
- name: 'browser_connect',
89
- title: 'Connect to a browser context',
90
- description: [
91
- 'Connect to a browser using one of the available methods:',
92
- ...factories.map(factory => `- "${factory.name}": ${factory.description}`),
93
- ].join('\n'),
94
- inputSchema: z.object({
95
- method: z.enum(factories.map(factory => factory.name)).default(factories[0].name).describe('The method to use to connect to the browser'),
96
- }),
97
- type: 'readOnly',
98
- },
99
- async handle(context, params, response) {
100
- const factory = factories.find(factory => factory.name === params.method);
101
- if (!factory) {
102
- response.addError('Unknown connection method: ' + params.method);
103
- return;
104
- }
105
- await self._setContextFactory(factory);
106
- response.addResult('Successfully changed connection method.');
107
- }
108
- });
109
- }
110
- async _setContextFactory(newFactory) {
111
- if (this._context) {
112
- const options = {
113
- ...this._context.options,
114
- browserContextFactory: newFactory,
115
- };
116
- await this._context.dispose();
117
- this._context = new Context(options);
118
- }
119
- this._browserContextFactory = newFactory;
78
+ void this._context?.dispose().catch(logUnhandledError);
120
79
  }
121
80
  }
package/lib/config.js CHANGED
@@ -17,7 +17,7 @@ import fs from 'fs';
17
17
  import os from 'os';
18
18
  import path from 'path';
19
19
  import { devices } from 'playwright';
20
- import { sanitizeForFilePath } from './utils.js';
20
+ import { sanitizeForFilePath } from './utils/fileUtils.js';
21
21
  const defaultConfig = {
22
22
  browser: {
23
23
  browserName: 'chromium',
package/lib/context.js CHANGED
@@ -14,7 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import debug from 'debug';
17
- import { logUnhandledError } from './log.js';
17
+ import { logUnhandledError } from './utils/log.js';
18
18
  import { Tab } from './tab.js';
19
19
  import { outputFile } from './config.js';
20
20
  const testDebug = debug('pw:mcp:test');
@@ -23,9 +23,9 @@
23
23
  import { spawn } from 'child_process';
24
24
  import debug from 'debug';
25
25
  import { WebSocket, WebSocketServer } from 'ws';
26
- import { httpAddressToString } from '../httpServer.js';
27
- import { logUnhandledError } from '../log.js';
28
- import { ManualPromise } from '../manualPromise.js';
26
+ import { httpAddressToString } from '../utils/httpServer.js';
27
+ import { logUnhandledError } from '../utils/log.js';
28
+ import { ManualPromise } from '../utils/manualPromise.js';
29
29
  // @ts-ignore
30
30
  const { registry } = await import('playwright-core/lib/server/registry/index');
31
31
  const debugLogger = debug('pw:mcp:relay');
@@ -66,6 +66,9 @@ export class CDPRelayServer {
66
66
  debugLogger('Waiting for incoming extension connection');
67
67
  await Promise.race([
68
68
  this._extensionConnectionPromise,
69
+ new Promise((_, reject) => setTimeout(() => {
70
+ reject(new Error(`Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed. See https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md for installation instructions.`));
71
+ }, process.env.PWMCP_TEST_CONNECTION_TIMEOUT ? parseInt(process.env.PWMCP_TEST_CONNECTION_TIMEOUT, 10) : 5_000)),
69
72
  new Promise((_, reject) => abortSignal.addEventListener('abort', reject))
70
73
  ]);
71
74
  debugLogger('Extension connection established');
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import debug from 'debug';
17
17
  import * as playwright from 'playwright';
18
- import { startHttpServer } from '../httpServer.js';
18
+ import { startHttpServer } from '../utils/httpServer.js';
19
19
  import { CDPRelayServer } from './cdpRelay.js';
20
20
  const debugLogger = debug('pw:mcp:relay');
21
21
  export class ExtensionContextFactory {
package/lib/index.js CHANGED
@@ -20,7 +20,7 @@ import * as mcpServer from './mcp/server.js';
20
20
  export async function createConnection(userConfig = {}, contextGetter) {
21
21
  const config = await resolveConfig(userConfig);
22
22
  const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config);
23
- return mcpServer.createServer(new BrowserServerBackend(config, [factory]), false);
23
+ return mcpServer.createServer(new BrowserServerBackend(config, factory), false);
24
24
  }
25
25
  class SimpleBrowserContextFactory {
26
26
  name = 'custom';
@@ -39,7 +39,7 @@ export class Context {
39
39
  static async create(config) {
40
40
  const client = new Client({ name: 'Playwright Proxy', version: '1.0.0' });
41
41
  const browserContextFactory = contextFactory(config);
42
- const server = mcpServer.createServer(new BrowserServerBackend(config, [browserContextFactory]), false);
42
+ const server = mcpServer.createServer(new BrowserServerBackend(config, browserContextFactory), false);
43
43
  await client.connect(new InProcessTransport(server));
44
44
  await client.ping();
45
45
  return new Context(config, client);
@@ -15,10 +15,11 @@
15
15
  */
16
16
  import dotenv from 'dotenv';
17
17
  import * as mcpTransport from '../mcp/transport.js';
18
- import { packageJSON } from '../package.js';
18
+ import { packageJSON } from '../utils/package.js';
19
19
  import { Context } from './context.js';
20
20
  import { perform } from './perform.js';
21
21
  import { snapshot } from './snapshot.js';
22
+ import { toMcpTool } from '../mcp/tool.js';
22
23
  export async function runLoopTools(config) {
23
24
  dotenv.config();
24
25
  const serverBackendFactory = () => new LoopToolsServerBackend(config);
@@ -36,11 +37,12 @@ class LoopToolsServerBackend {
36
37
  async initialize() {
37
38
  this._context = await Context.create(this._config);
38
39
  }
39
- tools() {
40
- return this._tools.map(tool => tool.schema);
40
+ async listTools() {
41
+ return this._tools.map(tool => toMcpTool(tool.schema));
41
42
  }
42
- async callTool(schema, parsedArguments) {
43
- const tool = this._tools.find(tool => tool.schema.name === schema.name);
43
+ async callTool(name, args) {
44
+ const tool = this._tools.find(tool => tool.schema.name === name);
45
+ const parsedArguments = tool.schema.inputSchema.parse(args || {});
44
46
  return await tool.handle(this._context, parsedArguments);
45
47
  }
46
48
  serverClosed() {
@@ -0,0 +1,106 @@
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 { z } from 'zod';
17
+ import { zodToJsonSchema } from 'zod-to-json-schema';
18
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
19
+ import { ListRootsRequestSchema, PingRequestSchema } from '@modelcontextprotocol/sdk/types.js';
20
+ import { logUnhandledError } from '../utils/log.js';
21
+ import { packageJSON } from '../utils/package.js';
22
+ export class ProxyBackend {
23
+ name = 'Playwright MCP Client Switcher';
24
+ version = packageJSON.version;
25
+ _mcpProviders;
26
+ _currentClient;
27
+ _contextSwitchTool;
28
+ _roots = [];
29
+ constructor(mcpProviders) {
30
+ this._mcpProviders = mcpProviders;
31
+ this._contextSwitchTool = this._defineContextSwitchTool();
32
+ }
33
+ async initialize(clientVersion, roots) {
34
+ this._roots = roots;
35
+ await this._setCurrentClient(this._mcpProviders[0]);
36
+ }
37
+ async listTools() {
38
+ const response = await this._currentClient.listTools();
39
+ if (this._mcpProviders.length === 1)
40
+ return response.tools;
41
+ return [
42
+ ...response.tools,
43
+ this._contextSwitchTool,
44
+ ];
45
+ }
46
+ async callTool(name, args) {
47
+ if (name === this._contextSwitchTool.name)
48
+ return this._callContextSwitchTool(args);
49
+ return await this._currentClient.callTool({
50
+ name,
51
+ arguments: args,
52
+ });
53
+ }
54
+ serverClosed() {
55
+ void this._currentClient?.close().catch(logUnhandledError);
56
+ }
57
+ async _callContextSwitchTool(params) {
58
+ try {
59
+ const factory = this._mcpProviders.find(factory => factory.name === params.name);
60
+ if (!factory)
61
+ throw new Error('Unknown connection method: ' + params.name);
62
+ await this._setCurrentClient(factory);
63
+ return {
64
+ content: [{ type: 'text', text: '### Result\nSuccessfully changed connection method.\n' }],
65
+ };
66
+ }
67
+ catch (error) {
68
+ return {
69
+ content: [{ type: 'text', text: `### Result\nError: ${error}\n` }],
70
+ isError: true,
71
+ };
72
+ }
73
+ }
74
+ _defineContextSwitchTool() {
75
+ return {
76
+ name: 'browser_connect',
77
+ description: [
78
+ 'Connect to a browser using one of the available methods:',
79
+ ...this._mcpProviders.map(factory => `- "${factory.name}": ${factory.description}`),
80
+ ].join('\n'),
81
+ inputSchema: zodToJsonSchema(z.object({
82
+ name: z.enum(this._mcpProviders.map(factory => factory.name)).default(this._mcpProviders[0].name).describe('The method to use to connect to the browser'),
83
+ }), { strictUnions: true }),
84
+ annotations: {
85
+ title: 'Connect to a browser context',
86
+ readOnlyHint: true,
87
+ openWorldHint: false,
88
+ },
89
+ };
90
+ }
91
+ async _setCurrentClient(factory) {
92
+ await this._currentClient?.close();
93
+ this._currentClient = undefined;
94
+ const client = new Client({ name: 'Playwright MCP Proxy', version: packageJSON.version });
95
+ client.registerCapabilities({
96
+ roots: {
97
+ listRoots: true,
98
+ },
99
+ });
100
+ client.setRequestHandler(ListRootsRequestSchema, () => ({ roots: this._roots }));
101
+ client.setRequestHandler(PingRequestSchema, () => ({}));
102
+ const transport = await factory.connect();
103
+ await client.connect(transport);
104
+ this._currentClient = client;
105
+ }
106
+ }
package/lib/mcp/server.js CHANGED
@@ -13,11 +13,12 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
+ import debug from 'debug';
16
17
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
17
18
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
18
- import { zodToJsonSchema } from 'zod-to-json-schema';
19
- import { ManualPromise } from '../manualPromise.js';
20
- import { logUnhandledError } from '../log.js';
19
+ import { ManualPromise } from '../utils/manualPromise.js';
20
+ import { logUnhandledError } from '../utils/log.js';
21
+ const serverDebug = debug('pw:mcp:server');
21
22
  export async function connect(serverBackendFactory, transport, runHeartbeat) {
22
23
  const backend = serverBackendFactory();
23
24
  const server = createServer(backend, runHeartbeat);
@@ -30,43 +31,45 @@ export function createServer(backend, runHeartbeat) {
30
31
  tools: {},
31
32
  }
32
33
  });
33
- const tools = backend.tools();
34
34
  server.setRequestHandler(ListToolsRequestSchema, async () => {
35
- return { tools: tools.map(tool => ({
36
- name: tool.name,
37
- description: tool.description,
38
- inputSchema: zodToJsonSchema(tool.inputSchema),
39
- annotations: {
40
- title: tool.title,
41
- readOnlyHint: tool.type === 'readOnly',
42
- destructiveHint: tool.type === 'destructive',
43
- openWorldHint: true,
44
- },
45
- })) };
35
+ serverDebug('listTools');
36
+ await initializedPromise;
37
+ const tools = await backend.listTools();
38
+ return { tools };
46
39
  });
47
40
  let heartbeatRunning = false;
48
41
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
42
+ serverDebug('callTool', request);
49
43
  await initializedPromise;
50
44
  if (runHeartbeat && !heartbeatRunning) {
51
45
  heartbeatRunning = true;
52
46
  startHeartbeat(server);
53
47
  }
54
- const errorResult = (...messages) => ({
55
- content: [{ type: 'text', text: '### Result\n' + messages.join('\n') }],
56
- isError: true,
57
- });
58
- const tool = tools.find(tool => tool.name === request.params.name);
59
- if (!tool)
60
- return errorResult(`Error: Tool "${request.params.name}" not found`);
61
48
  try {
62
- return await backend.callTool(tool, tool.inputSchema.parse(request.params.arguments || {}));
49
+ return await backend.callTool(request.params.name, request.params.arguments || {});
63
50
  }
64
51
  catch (error) {
65
- return errorResult(String(error));
52
+ return {
53
+ content: [{ type: 'text', text: '### Result\n' + String(error) }],
54
+ isError: true,
55
+ };
66
56
  }
67
57
  });
68
- addServerListener(server, 'initialized', () => {
69
- backend.initialize?.(server).then(() => initializedPromise.resolve()).catch(logUnhandledError);
58
+ addServerListener(server, 'initialized', async () => {
59
+ try {
60
+ const capabilities = server.getClientCapabilities();
61
+ let clientRoots = [];
62
+ if (capabilities?.roots) {
63
+ const { roots } = await server.listRoots(undefined, { timeout: 2_000 }).catch(() => ({ roots: [] }));
64
+ clientRoots = roots;
65
+ }
66
+ const clientVersion = server.getClientVersion() ?? { name: 'unknown', version: 'unknown' };
67
+ await backend.initialize?.(clientVersion, clientRoots);
68
+ initializedPromise.resolve();
69
+ }
70
+ catch (e) {
71
+ logUnhandledError(e);
72
+ }
70
73
  });
71
74
  addServerListener(server, 'close', () => backend.serverClosed?.());
72
75
  return server;
@@ -0,0 +1,29 @@
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 { zodToJsonSchema } from 'zod-to-json-schema';
17
+ export function toMcpTool(tool) {
18
+ return {
19
+ name: tool.name,
20
+ description: tool.description,
21
+ inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }),
22
+ annotations: {
23
+ title: tool.title,
24
+ readOnlyHint: tool.type === 'readOnly',
25
+ destructiveHint: tool.type === 'destructive',
26
+ openWorldHint: true,
27
+ },
28
+ };
29
+ }
@@ -18,7 +18,7 @@ import debug from 'debug';
18
18
  import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
19
19
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
20
20
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
21
- import { httpAddressToString, startHttpServer } from '../httpServer.js';
21
+ import { httpAddressToString, startHttpServer } from '../utils/httpServer.js';
22
22
  import * as mcpServer from './server.js';
23
23
  export async function start(serverBackendFactory, options) {
24
24
  if (options.port !== undefined) {
package/lib/program.js CHANGED
@@ -14,16 +14,17 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { program, Option } from 'commander';
17
- // @ts-ignore
18
- import { startTraceViewerServer } from 'playwright-core/lib/server';
17
+ import * as mcpServer from './mcp/server.js';
19
18
  import * as mcpTransport from './mcp/transport.js';
20
19
  import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js';
21
- import { packageJSON } from './package.js';
22
- import { createExtensionContextFactory, runWithExtension } from './extension/main.js';
23
- import { BrowserServerBackend } from './browserServerBackend.js';
20
+ import { packageJSON } from './utils/package.js';
24
21
  import { Context } from './context.js';
25
22
  import { contextFactory } from './browserContextFactory.js';
26
23
  import { runLoopTools } from './loopTools/main.js';
24
+ import { ProxyBackend } from './mcp/proxyBackend.js';
25
+ import { BrowserServerBackend } from './browserServerBackend.js';
26
+ import { ExtensionContextFactory } from './extension/extensionContextFactory.js';
27
+ import { InProcessTransport } from './mcp/inProcessTransport.js';
27
28
  program
28
29
  .version('Version ' + packageJSON.version)
29
30
  .name(packageJSON.name)
@@ -36,6 +37,7 @@ program
36
37
  .option('--config <path>', 'path to the configuration file.')
37
38
  .option('--device <device>', 'device to emulate, for example: "iPhone 15"')
38
39
  .option('--executable-path <path>', 'path to the browser executable.')
40
+ .option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.')
39
41
  .option('--headless', 'run browser in headless mode, headed by default')
40
42
  .option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
41
43
  .option('--ignore-https-errors', 'ignore https errors')
@@ -52,7 +54,6 @@ program
52
54
  .option('--user-agent <ua string>', 'specify user agent string')
53
55
  .option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
54
56
  .option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"')
55
- .addOption(new Option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').hideHelp())
56
57
  .addOption(new Option('--connect-tool', 'Allow to switch between different browser connection methods.').hideHelp())
57
58
  .addOption(new Option('--loop-tools', 'Run loop tools').hideHelp())
58
59
  .addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
@@ -65,7 +66,9 @@ program
65
66
  }
66
67
  const config = await resolveCLIConfig(options);
67
68
  if (options.extension) {
68
- await runWithExtension(config);
69
+ const contextFactory = createExtensionContextFactory(config);
70
+ const serverBackendFactory = () => new BrowserServerBackend(config, contextFactory);
71
+ await mcpTransport.start(serverBackendFactory, config.server);
69
72
  return;
70
73
  }
71
74
  if (options.loopTools) {
@@ -73,18 +76,10 @@ program
73
76
  return;
74
77
  }
75
78
  const browserContextFactory = contextFactory(config);
76
- const factories = [browserContextFactory];
79
+ const providers = [mcpProviderForBrowserContextFactory(config, browserContextFactory)];
77
80
  if (options.connectTool)
78
- factories.push(createExtensionContextFactory(config));
79
- const serverBackendFactory = () => new BrowserServerBackend(config, factories);
80
- await mcpTransport.start(serverBackendFactory, config.server);
81
- if (config.saveTrace) {
82
- const server = await startTraceViewerServer();
83
- const urlPrefix = server.urlPrefix('human-readable');
84
- const url = urlPrefix + '/trace/index.html?trace=' + config.browser.launchOptions.tracesDir + '/trace.json';
85
- // eslint-disable-next-line no-console
86
- console.error('\nTrace viewer listening on ' + url);
87
- }
81
+ providers.push(mcpProviderForBrowserContextFactory(config, createExtensionContextFactory(config)));
82
+ await mcpTransport.start(() => new ProxyBackend(providers), config.server);
88
83
  });
89
84
  function setupExitWatchdog() {
90
85
  let isExiting = false;
@@ -100,4 +95,17 @@ function setupExitWatchdog() {
100
95
  process.on('SIGINT', handleExit);
101
96
  process.on('SIGTERM', handleExit);
102
97
  }
98
+ function createExtensionContextFactory(config) {
99
+ return new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir);
100
+ }
101
+ function mcpProviderForBrowserContextFactory(config, browserContextFactory) {
102
+ return {
103
+ name: browserContextFactory.name,
104
+ description: browserContextFactory.description,
105
+ connect: async () => {
106
+ const server = mcpServer.createServer(new BrowserServerBackend(config, browserContextFactory), false);
107
+ return new InProcessTransport(server);
108
+ },
109
+ };
110
+ }
103
111
  void program.parseAsync(process.argv);
package/lib/sessionLog.js CHANGED
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import fs from 'fs';
17
17
  import path from 'path';
18
- import { logUnhandledError } from './log.js';
18
+ import { logUnhandledError } from './utils/log.js';
19
19
  import { outputFile } from './config.js';
20
20
  export class SessionLog {
21
21
  _folder;
package/lib/tab.js CHANGED
@@ -15,8 +15,8 @@
15
15
  */
16
16
  import { EventEmitter } from 'events';
17
17
  import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
18
- import { logUnhandledError } from './log.js';
19
- import { ManualPromise } from './manualPromise.js';
18
+ import { logUnhandledError } from './utils/log.js';
19
+ import { ManualPromise } from './utils/manualPromise.js';
20
20
  export const TabEvents = {
21
21
  modalState: 'modalState'
22
22
  };
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { z } from 'zod';
17
17
  import { defineTabTool } from './tool.js';
18
- import * as javascript from '../javascript.js';
18
+ import * as javascript from '../utils/codegen.js';
19
19
  import { generateLocator } from './utils.js';
20
20
  const evaluateSchema = z.object({
21
21
  function: z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'),
@@ -17,7 +17,7 @@ import { z } from 'zod';
17
17
  import { defineTabTool } from './tool.js';
18
18
  import { elementSchema } from './snapshot.js';
19
19
  import { generateLocator } from './utils.js';
20
- import * as javascript from '../javascript.js';
20
+ import * as javascript from '../utils/codegen.js';
21
21
  const pressKey = defineTabTool({
22
22
  capability: 'core',
23
23
  schema: {
package/lib/tools/pdf.js CHANGED
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { z } from 'zod';
17
17
  import { defineTabTool } from './tool.js';
18
- import * as javascript from '../javascript.js';
18
+ import * as javascript from '../utils/codegen.js';
19
19
  const pdfSchema = z.object({
20
20
  filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
21
21
  });
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { z } from 'zod';
17
17
  import { defineTabTool } from './tool.js';
18
- import * as javascript from '../javascript.js';
18
+ import * as javascript from '../utils/codegen.js';
19
19
  import { generateLocator } from './utils.js';
20
20
  const screenshotSchema = z.object({
21
21
  type: z.enum(['png', 'jpeg']).default('png').describe('Image format for the screenshot. Default is png.'),
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { z } from 'zod';
17
17
  import { defineTabTool, defineTool } from './tool.js';
18
- import * as javascript from '../javascript.js';
18
+ import * as javascript from '../utils/codegen.js';
19
19
  import { generateLocator } from './utils.js';
20
20
  const snapshot = defineTool({
21
21
  capability: 'core',
package/lib/tools/wait.js CHANGED
@@ -31,20 +31,19 @@ const wait = defineTool({
31
31
  handle: async (context, params, response) => {
32
32
  if (!params.text && !params.textGone && !params.time)
33
33
  throw new Error('Either time, text or textGone must be provided');
34
- const code = [];
35
34
  if (params.time) {
36
- code.push(`await new Promise(f => setTimeout(f, ${params.time} * 1000));`);
35
+ response.addCode(`await new Promise(f => setTimeout(f, ${params.time} * 1000));`);
37
36
  await new Promise(f => setTimeout(f, Math.min(30000, params.time * 1000)));
38
37
  }
39
38
  const tab = context.currentTabOrDie();
40
39
  const locator = params.text ? tab.page.getByText(params.text).first() : undefined;
41
40
  const goneLocator = params.textGone ? tab.page.getByText(params.textGone).first() : undefined;
42
41
  if (goneLocator) {
43
- code.push(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`);
42
+ response.addCode(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`);
44
43
  await goneLocator.waitFor({ state: 'hidden' });
45
44
  }
46
45
  if (locator) {
47
- code.push(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`);
46
+ response.addCode(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`);
48
47
  await locator.waitFor({ state: 'visible' });
49
48
  }
50
49
  response.addResult(`Waited for ${params.text || params.textGone || params.time}`);
@@ -25,7 +25,7 @@ export function escapeWithQuotes(text, char = '\'') {
25
25
  if (char === '"')
26
26
  return char + escapedText.replace(/["]/g, '\\"') + char;
27
27
  if (char === '`')
28
- return char + escapedText.replace(/[`]/g, '`') + char;
28
+ return char + escapedText.replace(/[`]/g, '\\`') + char;
29
29
  throw new Error('Invalid escape char');
30
30
  }
31
31
  export function quote(text) {
@@ -27,6 +27,10 @@ export function cacheDir() {
27
27
  throw new Error('Unsupported platform: ' + process.platform);
28
28
  return path.join(cacheDirectory, 'ms-playwright');
29
29
  }
30
- export async function userDataDir(browserConfig) {
31
- return path.join(cacheDir(), 'ms-playwright', `mcp-${browserConfig.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
30
+ export function sanitizeForFilePath(s) {
31
+ const sanitize = (s) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
32
+ const separator = s.lastIndexOf('.');
33
+ if (separator === -1)
34
+ return sanitize(s);
35
+ return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1));
32
36
  }
@@ -14,13 +14,9 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import crypto from 'crypto';
17
+ export function createGuid() {
18
+ return crypto.randomBytes(16).toString('hex');
19
+ }
17
20
  export function createHash(data) {
18
21
  return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7);
19
22
  }
20
- export function sanitizeForFilePath(s) {
21
- const sanitize = (s) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
22
- const separator = s.lastIndexOf('.');
23
- if (separator === -1)
24
- return sanitize(s);
25
- return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1));
26
- }
@@ -17,4 +17,4 @@ import fs from 'fs';
17
17
  import path from 'path';
18
18
  import url from 'url';
19
19
  const __filename = url.fileURLToPath(import.meta.url);
20
- export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', 'package.json'), 'utf8'));
20
+ export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', '..', 'package.json'), 'utf8'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playwright/mcp",
3
- "version": "0.0.33",
3
+ "version": "0.0.34",
4
4
  "description": "Playwright Tools for MCP",
5
5
  "type": "module",
6
6
  "repository": {
@@ -17,8 +17,9 @@
17
17
  "license": "Apache-2.0",
18
18
  "scripts": {
19
19
  "build": "tsc",
20
- "lint": "npm run update-readme && eslint . && tsc --noEmit",
20
+ "lint": "npm run update-readme && npm run check-deps && eslint . && tsc --noEmit",
21
21
  "lint-fix": "eslint . --fix",
22
+ "check-deps": "node utils/check-deps.js",
22
23
  "update-readme": "node utils/update-readme.js",
23
24
  "watch": "tsc --watch",
24
25
  "test": "playwright test",
@@ -42,8 +43,8 @@
42
43
  "debug": "^4.4.1",
43
44
  "dotenv": "^17.2.0",
44
45
  "mime": "^4.0.7",
45
- "playwright": "1.55.0-alpha-2025-08-07",
46
- "playwright-core": "1.55.0-alpha-2025-08-07",
46
+ "playwright": "1.55.0-alpha-2025-08-12",
47
+ "playwright-core": "1.55.0-alpha-2025-08-12",
47
48
  "ws": "^8.18.1",
48
49
  "zod": "^3.24.1",
49
50
  "zod-to-json-schema": "^3.24.4"
@@ -52,7 +53,7 @@
52
53
  "@anthropic-ai/sdk": "^0.57.0",
53
54
  "@eslint/eslintrc": "^3.2.0",
54
55
  "@eslint/js": "^9.19.0",
55
- "@playwright/test": "1.55.0-alpha-2025-08-07",
56
+ "@playwright/test": "1.55.0-alpha-2025-08-12",
56
57
  "@stylistic/eslint-plugin": "^3.0.1",
57
58
  "@types/debug": "^4.1.12",
58
59
  "@types/node": "^22.13.10",
@@ -1,26 +0,0 @@
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 { ExtensionContextFactory } from './extensionContextFactory.js';
17
- import { BrowserServerBackend } from '../browserServerBackend.js';
18
- import * as mcpTransport from '../mcp/transport.js';
19
- export async function runWithExtension(config) {
20
- const contextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir);
21
- const serverBackendFactory = () => new BrowserServerBackend(config, [contextFactory]);
22
- await mcpTransport.start(serverBackendFactory, config.server);
23
- }
24
- export function createExtensionContextFactory(config) {
25
- return new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir);
26
- }
File without changes
File without changes