@playwright/mcp 0.0.32 → 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.
@@ -13,19 +13,26 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
+ /**
17
+ * WebSocket server that bridges Playwright MCP and Chrome Extension
18
+ *
19
+ * Endpoints:
20
+ * - /cdp/guid - Full CDP interface for Playwright MCP
21
+ * - /extension/guid - Extension connection for chrome.debugger forwarding
22
+ */
16
23
  import { spawn } from 'child_process';
17
- import { WebSocket, WebSocketServer } from 'ws';
18
24
  import debug from 'debug';
19
- import * as playwright from 'playwright';
25
+ import { WebSocket, WebSocketServer } from 'ws';
26
+ import { httpAddressToString } from '../utils/httpServer.js';
27
+ import { logUnhandledError } from '../utils/log.js';
28
+ import { ManualPromise } from '../utils/manualPromise.js';
20
29
  // @ts-ignore
21
30
  const { registry } = await import('playwright-core/lib/server/registry/index');
22
- import { httpAddressToString, startHttpServer } from '../httpServer.js';
23
- import { logUnhandledError } from '../log.js';
24
- import { ManualPromise } from '../manualPromise.js';
25
31
  const debugLogger = debug('pw:mcp:relay');
26
32
  export class CDPRelayServer {
27
33
  _wsHost;
28
34
  _browserChannel;
35
+ _userDataDir;
29
36
  _cdpPath;
30
37
  _extensionPath;
31
38
  _wss;
@@ -34,9 +41,10 @@ export class CDPRelayServer {
34
41
  _connectedTabInfo;
35
42
  _nextSessionId = 1;
36
43
  _extensionConnectionPromise;
37
- constructor(server, browserChannel) {
44
+ constructor(server, browserChannel, userDataDir) {
38
45
  this._wsHost = httpAddressToString(server.address()).replace(/^http/, 'ws');
39
46
  this._browserChannel = browserChannel;
47
+ this._userDataDir = userDataDir;
40
48
  const uuid = crypto.randomUUID();
41
49
  this._cdpPath = `/cdp/${uuid}`;
42
50
  this._extensionPath = `/extension/${uuid}`;
@@ -50,16 +58,22 @@ export class CDPRelayServer {
50
58
  extensionEndpoint() {
51
59
  return `${this._wsHost}${this._extensionPath}`;
52
60
  }
53
- async ensureExtensionConnectionForMCPContext(clientInfo) {
61
+ async ensureExtensionConnectionForMCPContext(clientInfo, abortSignal) {
54
62
  debugLogger('Ensuring extension connection for MCP context');
55
63
  if (this._extensionConnection)
56
64
  return;
57
- await this._connectBrowser(clientInfo);
65
+ this._connectBrowser(clientInfo);
58
66
  debugLogger('Waiting for incoming extension connection');
59
- await this._extensionConnectionPromise;
67
+ await Promise.race([
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)),
72
+ new Promise((_, reject) => abortSignal.addEventListener('abort', reject))
73
+ ]);
60
74
  debugLogger('Extension connection established');
61
75
  }
62
- async _connectBrowser(clientInfo) {
76
+ _connectBrowser(clientInfo) {
63
77
  const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`;
64
78
  // Need to specify "key" in the manifest.json to make the id stable when loading from file.
65
79
  const url = new URL('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html');
@@ -72,7 +86,11 @@ export class CDPRelayServer {
72
86
  const executablePath = executableInfo.executablePath();
73
87
  if (!executablePath)
74
88
  throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`);
75
- spawn(executablePath, [href], {
89
+ const args = [];
90
+ if (this._userDataDir)
91
+ args.push(`--user-data-dir=${this._userDataDir}`);
92
+ args.push(href);
93
+ spawn(executablePath, args, {
76
94
  windowsHide: true,
77
95
  detached: true,
78
96
  shell: false,
@@ -248,45 +266,6 @@ export class CDPRelayServer {
248
266
  this._playwrightConnection?.send(JSON.stringify(message));
249
267
  }
250
268
  }
251
- class ExtensionContextFactory {
252
- _relay;
253
- _browserPromise;
254
- constructor(relay) {
255
- this._relay = relay;
256
- }
257
- async createContext(clientInfo) {
258
- // First call will establish the connection to the extension.
259
- if (!this._browserPromise)
260
- this._browserPromise = this._obtainBrowser(clientInfo);
261
- const browser = await this._browserPromise;
262
- return {
263
- browserContext: browser.contexts()[0],
264
- close: async () => {
265
- debugLogger('close() called for browser context, ignoring');
266
- }
267
- };
268
- }
269
- clientDisconnected() {
270
- this._relay.closeConnections('MCP client disconnected');
271
- this._browserPromise = undefined;
272
- }
273
- async _obtainBrowser(clientInfo) {
274
- await this._relay.ensureExtensionConnectionForMCPContext(clientInfo);
275
- const browser = await playwright.chromium.connectOverCDP(this._relay.cdpEndpoint());
276
- browser.on('disconnected', () => {
277
- this._browserPromise = undefined;
278
- debugLogger('Browser disconnected');
279
- });
280
- return browser;
281
- }
282
- }
283
- export async function startCDPRelayServer(browserChannel, abortController) {
284
- const httpServer = await startHttpServer({});
285
- const cdpRelayServer = new CDPRelayServer(httpServer, browserChannel);
286
- abortController.signal.addEventListener('abort', () => cdpRelayServer.stop());
287
- debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`);
288
- return new ExtensionContextFactory(cdpRelayServer);
289
- }
290
269
  class ExtensionConnection {
291
270
  _ws;
292
271
  _callbacks = new Map();
@@ -0,0 +1,56 @@
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 debug from 'debug';
17
+ import * as playwright from 'playwright';
18
+ import { startHttpServer } from '../utils/httpServer.js';
19
+ import { CDPRelayServer } from './cdpRelay.js';
20
+ const debugLogger = debug('pw:mcp:relay');
21
+ export class ExtensionContextFactory {
22
+ name = 'extension';
23
+ description = 'Connect to a browser using the Playwright MCP extension';
24
+ _browserChannel;
25
+ _userDataDir;
26
+ constructor(browserChannel, userDataDir) {
27
+ this._browserChannel = browserChannel;
28
+ this._userDataDir = userDataDir;
29
+ }
30
+ async createContext(clientInfo, abortSignal) {
31
+ const browser = await this._obtainBrowser(clientInfo, abortSignal);
32
+ return {
33
+ browserContext: browser.contexts()[0],
34
+ close: async () => {
35
+ debugLogger('close() called for browser context');
36
+ await browser.close();
37
+ }
38
+ };
39
+ }
40
+ async _obtainBrowser(clientInfo, abortSignal) {
41
+ const relay = await this._startRelay(abortSignal);
42
+ await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal);
43
+ return await playwright.chromium.connectOverCDP(relay.cdpEndpoint());
44
+ }
45
+ async _startRelay(abortSignal) {
46
+ const httpServer = await startHttpServer({});
47
+ if (abortSignal.aborted) {
48
+ httpServer.close();
49
+ throw new Error(abortSignal.reason);
50
+ }
51
+ const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel, this._userDataDir);
52
+ abortSignal.addEventListener('abort', () => cdpRelayServer.stop());
53
+ debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`);
54
+ return cdpRelayServer;
55
+ }
56
+ }
package/lib/index.js CHANGED
@@ -19,10 +19,12 @@ import { contextFactory } from './browserContextFactory.js';
19
19
  import * as mcpServer from './mcp/server.js';
20
20
  export async function createConnection(userConfig = {}, contextGetter) {
21
21
  const config = await resolveConfig(userConfig);
22
- const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config.browser);
22
+ const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config);
23
23
  return mcpServer.createServer(new BrowserServerBackend(config, factory), false);
24
24
  }
25
25
  class SimpleBrowserContextFactory {
26
+ name = 'custom';
27
+ description = 'Connect to a browser using a custom context getter';
26
28
  _contextGetter;
27
29
  constructor(contextGetter) {
28
30
  this._contextGetter = contextGetter;
@@ -38,7 +38,7 @@ export class Context {
38
38
  }
39
39
  static async create(config) {
40
40
  const client = new Client({ name: 'Playwright Proxy', version: '1.0.0' });
41
- const browserContextFactory = contextFactory(config.browser);
41
+ const browserContextFactory = contextFactory(config);
42
42
  const server = mcpServer.createServer(new BrowserServerBackend(config, browserContextFactory), false);
43
43
  await client.connect(new InProcessTransport(server));
44
44
  await client.ping();
@@ -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,56 +13,64 @@
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 '../utils/manualPromise.js';
20
+ import { logUnhandledError } from '../utils/log.js';
21
+ const serverDebug = debug('pw:mcp:server');
19
22
  export async function connect(serverBackendFactory, transport, runHeartbeat) {
20
23
  const backend = serverBackendFactory();
21
- await backend.initialize?.();
22
24
  const server = createServer(backend, runHeartbeat);
23
25
  await server.connect(transport);
24
26
  }
25
27
  export function createServer(backend, runHeartbeat) {
28
+ const initializedPromise = new ManualPromise();
26
29
  const server = new Server({ name: backend.name, version: backend.version }, {
27
30
  capabilities: {
28
31
  tools: {},
29
32
  }
30
33
  });
31
- const tools = backend.tools();
32
34
  server.setRequestHandler(ListToolsRequestSchema, async () => {
33
- return { tools: tools.map(tool => ({
34
- name: tool.name,
35
- description: tool.description,
36
- inputSchema: zodToJsonSchema(tool.inputSchema),
37
- annotations: {
38
- title: tool.title,
39
- readOnlyHint: tool.type === 'readOnly',
40
- destructiveHint: tool.type === 'destructive',
41
- openWorldHint: true,
42
- },
43
- })) };
35
+ serverDebug('listTools');
36
+ await initializedPromise;
37
+ const tools = await backend.listTools();
38
+ return { tools };
44
39
  });
45
40
  let heartbeatRunning = false;
46
41
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
42
+ serverDebug('callTool', request);
43
+ await initializedPromise;
47
44
  if (runHeartbeat && !heartbeatRunning) {
48
45
  heartbeatRunning = true;
49
46
  startHeartbeat(server);
50
47
  }
51
- const errorResult = (...messages) => ({
52
- content: [{ type: 'text', text: messages.join('\n') }],
53
- isError: true,
54
- });
55
- const tool = tools.find(tool => tool.name === request.params.name);
56
- if (!tool)
57
- return errorResult(`Tool "${request.params.name}" not found`);
58
48
  try {
59
- return await backend.callTool(tool, tool.inputSchema.parse(request.params.arguments || {}));
49
+ return await backend.callTool(request.params.name, request.params.arguments || {});
60
50
  }
61
51
  catch (error) {
62
- return errorResult(String(error));
52
+ return {
53
+ content: [{ type: 'text', text: '### Result\n' + String(error) }],
54
+ isError: true,
55
+ };
56
+ }
57
+ });
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);
63
72
  }
64
73
  });
65
- addServerListener(server, 'initialized', () => backend.serverInitialized?.(server.getClientVersion()));
66
74
  addServerListener(server, 'close', () => backend.serverClosed?.());
67
75
  return server;
68
76
  }
@@ -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 { 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,11 +54,11 @@ 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())
57
+ .addOption(new Option('--connect-tool', 'Allow to switch between different browser connection methods.').hideHelp())
56
58
  .addOption(new Option('--loop-tools', 'Run loop tools').hideHelp())
57
59
  .addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
58
60
  .action(async (options) => {
59
- const abortController = setupExitWatchdog();
61
+ setupExitWatchdog();
60
62
  if (options.vision) {
61
63
  // eslint-disable-next-line no-console
62
64
  console.error('The --vision option is deprecated, use --caps=vision instead');
@@ -64,39 +66,46 @@ program
64
66
  }
65
67
  const config = await resolveCLIConfig(options);
66
68
  if (options.extension) {
67
- await runWithExtension(config, abortController);
69
+ const contextFactory = createExtensionContextFactory(config);
70
+ const serverBackendFactory = () => new BrowserServerBackend(config, contextFactory);
71
+ await mcpTransport.start(serverBackendFactory, config.server);
68
72
  return;
69
73
  }
70
74
  if (options.loopTools) {
71
75
  await runLoopTools(config);
72
76
  return;
73
77
  }
74
- const browserContextFactory = contextFactory(config.browser);
75
- const serverBackendFactory = () => new BrowserServerBackend(config, browserContextFactory);
76
- await mcpTransport.start(serverBackendFactory, config.server);
77
- if (config.saveTrace) {
78
- const server = await startTraceViewerServer();
79
- const urlPrefix = server.urlPrefix('human-readable');
80
- const url = urlPrefix + '/trace/index.html?trace=' + config.browser.launchOptions.tracesDir + '/trace.json';
81
- // eslint-disable-next-line no-console
82
- console.error('\nTrace viewer listening on ' + url);
83
- }
78
+ const browserContextFactory = contextFactory(config);
79
+ const providers = [mcpProviderForBrowserContextFactory(config, browserContextFactory)];
80
+ if (options.connectTool)
81
+ providers.push(mcpProviderForBrowserContextFactory(config, createExtensionContextFactory(config)));
82
+ await mcpTransport.start(() => new ProxyBackend(providers), config.server);
84
83
  });
85
84
  function setupExitWatchdog() {
86
- const abortController = new AbortController();
87
85
  let isExiting = false;
88
86
  const handleExit = async () => {
89
87
  if (isExiting)
90
88
  return;
91
89
  isExiting = true;
92
90
  setTimeout(() => process.exit(0), 15000);
93
- abortController.abort('Process exiting');
94
91
  await Context.disposeAll();
95
92
  process.exit(0);
96
93
  };
97
94
  process.stdin.on('close', handleExit);
98
95
  process.on('SIGINT', handleExit);
99
96
  process.on('SIGTERM', handleExit);
100
- return abortController;
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
+ };
101
110
  }
102
111
  void program.parseAsync(process.argv);