@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.
- package/README.md +33 -3
- package/lib/browserContextFactory.js +78 -60
- package/lib/browserServerBackend.js +47 -21
- package/lib/config.js +8 -8
- package/lib/context.js +76 -28
- package/lib/extension/cdpRelay.js +29 -50
- package/lib/extension/extensionContextFactory.js +56 -0
- package/lib/index.js +3 -1
- package/lib/loopTools/context.js +1 -1
- package/lib/loopTools/main.js +7 -5
- package/lib/mcp/proxyBackend.js +106 -0
- package/lib/mcp/server.js +32 -24
- package/lib/mcp/tool.js +29 -0
- package/lib/mcp/transport.js +1 -1
- package/lib/program.js +30 -21
- package/lib/response.js +81 -14
- package/lib/sessionLog.js +85 -34
- package/lib/tab.js +55 -52
- package/lib/tools/common.js +0 -1
- package/lib/tools/evaluate.js +1 -1
- package/lib/tools/files.js +0 -1
- package/lib/tools/keyboard.js +1 -3
- package/lib/tools/navigate.js +0 -3
- package/lib/tools/pdf.js +2 -4
- package/lib/tools/screenshot.js +13 -10
- package/lib/tools/snapshot.js +3 -8
- package/lib/tools/tool.js +5 -4
- package/lib/tools/utils.js +0 -7
- package/lib/tools/wait.js +3 -4
- package/lib/{javascript.js → utils/codegen.js} +1 -1
- package/lib/{fileUtils.js → utils/fileUtils.js} +6 -2
- package/lib/utils/guid.js +22 -0
- package/lib/{package.js → utils/package.js} +1 -1
- package/package.json +9 -9
- package/lib/extension/main.js +0 -33
- /package/lib/{httpServer.js → utils/httpServer.js} +0 -0
- /package/lib/{log.js → utils/log.js} +0 -0
- /package/lib/{manualPromise.js → utils/manualPromise.js} +0 -0
|
@@ -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
|
|
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
|
-
|
|
65
|
+
this._connectBrowser(clientInfo);
|
|
58
66
|
debugLogger('Waiting for incoming extension connection');
|
|
59
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
package/lib/loopTools/context.js
CHANGED
|
@@ -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
|
|
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();
|
package/lib/loopTools/main.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
43
|
-
const tool = this._tools.find(tool => tool.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 {
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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(
|
|
49
|
+
return await backend.callTool(request.params.name, request.params.arguments || {});
|
|
60
50
|
}
|
|
61
51
|
catch (error) {
|
|
62
|
-
return
|
|
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
|
}
|
package/lib/mcp/tool.js
ADDED
|
@@ -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
|
+
}
|
package/lib/mcp/transport.js
CHANGED
|
@@ -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
|
-
|
|
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('--
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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);
|