@roxybrowser/playwright-mcp 0.0.5 → 0.0.6-beta.7
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 +832 -742
- package/dist/cli.mjs +250 -0
- package/dist/cli.mjs.LICENSE.txt +42 -0
- package/dist/index.mjs +250 -0
- package/dist/index.mjs.LICENSE.txt +42 -0
- package/dist/index.mjs.map +1 -0
- package/index.d.ts +86 -23
- package/package.json +27 -41
- package/cli.js +0 -18
- package/config.d.ts +0 -119
- package/index.js +0 -19
- package/lib/browserContextFactory.js +0 -264
- package/lib/browserServerBackend.js +0 -77
- package/lib/config.js +0 -246
- package/lib/context.js +0 -242
- package/lib/extension/cdpRelay.js +0 -355
- package/lib/extension/extensionContextFactory.js +0 -54
- package/lib/index.js +0 -40
- package/lib/loop/loop.js +0 -69
- package/lib/loop/loopClaude.js +0 -152
- package/lib/loop/loopOpenAI.js +0 -141
- package/lib/loop/main.js +0 -60
- package/lib/loopTools/context.js +0 -67
- package/lib/loopTools/main.js +0 -54
- package/lib/loopTools/perform.js +0 -32
- package/lib/loopTools/snapshot.js +0 -29
- package/lib/loopTools/tool.js +0 -18
- package/lib/mcp/http.js +0 -120
- package/lib/mcp/inProcessTransport.js +0 -72
- package/lib/mcp/proxyBackend.js +0 -104
- package/lib/mcp/server.js +0 -123
- package/lib/mcp/tool.js +0 -29
- package/lib/program.js +0 -145
- package/lib/response.js +0 -165
- package/lib/sessionLog.js +0 -121
- package/lib/tab.js +0 -249
- package/lib/tools/common.js +0 -55
- package/lib/tools/console.js +0 -33
- package/lib/tools/dialogs.js +0 -47
- package/lib/tools/evaluate.js +0 -53
- package/lib/tools/files.js +0 -44
- package/lib/tools/install.js +0 -53
- package/lib/tools/keyboard.js +0 -78
- package/lib/tools/mouse.js +0 -99
- package/lib/tools/navigate.js +0 -70
- package/lib/tools/network.js +0 -41
- package/lib/tools/pdf.js +0 -40
- package/lib/tools/roxy.js +0 -50
- package/lib/tools/screenshot.js +0 -79
- package/lib/tools/snapshot.js +0 -139
- package/lib/tools/tabs.js +0 -87
- package/lib/tools/tool.js +0 -33
- package/lib/tools/utils.js +0 -74
- package/lib/tools/wait.js +0 -55
- package/lib/tools.js +0 -52
- package/lib/utils/codegen.js +0 -49
- package/lib/utils/fileUtils.js +0 -36
- package/lib/utils/guid.js +0 -22
- package/lib/utils/log.js +0 -21
- package/lib/utils/manualPromise.js +0 -111
- package/lib/utils/package.js +0 -20
- package/lib/vscode/host.js +0 -128
- package/lib/vscode/main.js +0 -62
|
@@ -1,355 +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
|
-
/**
|
|
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
|
-
*/
|
|
23
|
-
import { spawn } from 'child_process';
|
|
24
|
-
import debug from 'debug';
|
|
25
|
-
import { WebSocket, WebSocketServer } from 'ws';
|
|
26
|
-
import { httpAddressToString } from '../mcp/http.js';
|
|
27
|
-
import { logUnhandledError } from '../utils/log.js';
|
|
28
|
-
import { ManualPromise } from '../utils/manualPromise.js';
|
|
29
|
-
import { packageJSON } from '../utils/package.js';
|
|
30
|
-
// @ts-ignore
|
|
31
|
-
const { registry } = await import('playwright-core/lib/server/registry/index');
|
|
32
|
-
const debugLogger = debug('pw:mcp:relay');
|
|
33
|
-
export class CDPRelayServer {
|
|
34
|
-
_wsHost;
|
|
35
|
-
_browserChannel;
|
|
36
|
-
_userDataDir;
|
|
37
|
-
_cdpPath;
|
|
38
|
-
_extensionPath;
|
|
39
|
-
_wss;
|
|
40
|
-
_playwrightConnection = null;
|
|
41
|
-
_extensionConnection = null;
|
|
42
|
-
_connectedTabInfo;
|
|
43
|
-
_nextSessionId = 1;
|
|
44
|
-
_extensionConnectionPromise;
|
|
45
|
-
constructor(server, browserChannel, userDataDir) {
|
|
46
|
-
this._wsHost = httpAddressToString(server.address()).replace(/^http/, 'ws');
|
|
47
|
-
this._browserChannel = browserChannel;
|
|
48
|
-
this._userDataDir = userDataDir;
|
|
49
|
-
const uuid = crypto.randomUUID();
|
|
50
|
-
this._cdpPath = `/cdp/${uuid}`;
|
|
51
|
-
this._extensionPath = `/extension/${uuid}`;
|
|
52
|
-
this._resetExtensionConnection();
|
|
53
|
-
this._wss = new WebSocketServer({ server });
|
|
54
|
-
this._wss.on('connection', this._onConnection.bind(this));
|
|
55
|
-
}
|
|
56
|
-
cdpEndpoint() {
|
|
57
|
-
return `${this._wsHost}${this._cdpPath}`;
|
|
58
|
-
}
|
|
59
|
-
extensionEndpoint() {
|
|
60
|
-
return `${this._wsHost}${this._extensionPath}`;
|
|
61
|
-
}
|
|
62
|
-
async ensureExtensionConnectionForMCPContext(clientInfo, abortSignal) {
|
|
63
|
-
debugLogger('Ensuring extension connection for MCP context');
|
|
64
|
-
if (this._extensionConnection)
|
|
65
|
-
return;
|
|
66
|
-
this._connectBrowser(clientInfo);
|
|
67
|
-
debugLogger('Waiting for incoming extension connection');
|
|
68
|
-
await Promise.race([
|
|
69
|
-
this._extensionConnectionPromise,
|
|
70
|
-
new Promise((_, reject) => setTimeout(() => {
|
|
71
|
-
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.`));
|
|
72
|
-
}, process.env.PWMCP_TEST_CONNECTION_TIMEOUT ? parseInt(process.env.PWMCP_TEST_CONNECTION_TIMEOUT, 10) : 5_000)),
|
|
73
|
-
new Promise((_, reject) => abortSignal.addEventListener('abort', reject))
|
|
74
|
-
]);
|
|
75
|
-
debugLogger('Extension connection established');
|
|
76
|
-
}
|
|
77
|
-
_connectBrowser(clientInfo) {
|
|
78
|
-
const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`;
|
|
79
|
-
// Need to specify "key" in the manifest.json to make the id stable when loading from file.
|
|
80
|
-
const url = new URL('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html');
|
|
81
|
-
url.searchParams.set('mcpRelayUrl', mcpRelayEndpoint);
|
|
82
|
-
const client = {
|
|
83
|
-
name: clientInfo.name,
|
|
84
|
-
version: clientInfo.version,
|
|
85
|
-
};
|
|
86
|
-
url.searchParams.set('client', JSON.stringify(client));
|
|
87
|
-
url.searchParams.set('pwMcpVersion', packageJSON.version);
|
|
88
|
-
const href = url.toString();
|
|
89
|
-
const executableInfo = registry.findExecutable(this._browserChannel);
|
|
90
|
-
if (!executableInfo)
|
|
91
|
-
throw new Error(`Unsupported channel: "${this._browserChannel}"`);
|
|
92
|
-
const executablePath = executableInfo.executablePath();
|
|
93
|
-
if (!executablePath)
|
|
94
|
-
throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`);
|
|
95
|
-
const args = [];
|
|
96
|
-
if (this._userDataDir)
|
|
97
|
-
args.push(`--user-data-dir=${this._userDataDir}`);
|
|
98
|
-
args.push(href);
|
|
99
|
-
spawn(executablePath, args, {
|
|
100
|
-
windowsHide: true,
|
|
101
|
-
detached: true,
|
|
102
|
-
shell: false,
|
|
103
|
-
stdio: 'ignore',
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
stop() {
|
|
107
|
-
this.closeConnections('Server stopped');
|
|
108
|
-
this._wss.close();
|
|
109
|
-
}
|
|
110
|
-
closeConnections(reason) {
|
|
111
|
-
this._closePlaywrightConnection(reason);
|
|
112
|
-
this._closeExtensionConnection(reason);
|
|
113
|
-
}
|
|
114
|
-
_onConnection(ws, request) {
|
|
115
|
-
const url = new URL(`http://localhost${request.url}`);
|
|
116
|
-
debugLogger(`New connection to ${url.pathname}`);
|
|
117
|
-
if (url.pathname === this._cdpPath) {
|
|
118
|
-
this._handlePlaywrightConnection(ws);
|
|
119
|
-
}
|
|
120
|
-
else if (url.pathname === this._extensionPath) {
|
|
121
|
-
this._handleExtensionConnection(ws);
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
debugLogger(`Invalid path: ${url.pathname}`);
|
|
125
|
-
ws.close(4004, 'Invalid path');
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
_handlePlaywrightConnection(ws) {
|
|
129
|
-
if (this._playwrightConnection) {
|
|
130
|
-
debugLogger('Rejecting second Playwright connection');
|
|
131
|
-
ws.close(1000, 'Another CDP client already connected');
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
this._playwrightConnection = ws;
|
|
135
|
-
ws.on('message', async (data) => {
|
|
136
|
-
try {
|
|
137
|
-
const message = JSON.parse(data.toString());
|
|
138
|
-
await this._handlePlaywrightMessage(message);
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
debugLogger(`Error while handling Playwright message\n${data.toString()}\n`, error);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
ws.on('close', () => {
|
|
145
|
-
if (this._playwrightConnection !== ws)
|
|
146
|
-
return;
|
|
147
|
-
this._playwrightConnection = null;
|
|
148
|
-
this._closeExtensionConnection('Playwright client disconnected');
|
|
149
|
-
debugLogger('Playwright WebSocket closed');
|
|
150
|
-
});
|
|
151
|
-
ws.on('error', error => {
|
|
152
|
-
debugLogger('Playwright WebSocket error:', error);
|
|
153
|
-
});
|
|
154
|
-
debugLogger('Playwright MCP connected');
|
|
155
|
-
}
|
|
156
|
-
_closeExtensionConnection(reason) {
|
|
157
|
-
this._extensionConnection?.close(reason);
|
|
158
|
-
this._extensionConnectionPromise.reject(new Error(reason));
|
|
159
|
-
this._resetExtensionConnection();
|
|
160
|
-
}
|
|
161
|
-
_resetExtensionConnection() {
|
|
162
|
-
this._connectedTabInfo = undefined;
|
|
163
|
-
this._extensionConnection = null;
|
|
164
|
-
this._extensionConnectionPromise = new ManualPromise();
|
|
165
|
-
void this._extensionConnectionPromise.catch(logUnhandledError);
|
|
166
|
-
}
|
|
167
|
-
_closePlaywrightConnection(reason) {
|
|
168
|
-
if (this._playwrightConnection?.readyState === WebSocket.OPEN)
|
|
169
|
-
this._playwrightConnection.close(1000, reason);
|
|
170
|
-
this._playwrightConnection = null;
|
|
171
|
-
}
|
|
172
|
-
_handleExtensionConnection(ws) {
|
|
173
|
-
if (this._extensionConnection) {
|
|
174
|
-
ws.close(1000, 'Another extension connection already established');
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
this._extensionConnection = new ExtensionConnection(ws);
|
|
178
|
-
this._extensionConnection.onclose = (c, reason) => {
|
|
179
|
-
debugLogger('Extension WebSocket closed:', reason, c === this._extensionConnection);
|
|
180
|
-
if (this._extensionConnection !== c)
|
|
181
|
-
return;
|
|
182
|
-
this._resetExtensionConnection();
|
|
183
|
-
this._closePlaywrightConnection(`Extension disconnected: ${reason}`);
|
|
184
|
-
};
|
|
185
|
-
this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this);
|
|
186
|
-
this._extensionConnectionPromise.resolve();
|
|
187
|
-
}
|
|
188
|
-
_handleExtensionMessage(method, params) {
|
|
189
|
-
switch (method) {
|
|
190
|
-
case 'forwardCDPEvent':
|
|
191
|
-
const sessionId = params.sessionId || this._connectedTabInfo?.sessionId;
|
|
192
|
-
this._sendToPlaywright({
|
|
193
|
-
sessionId,
|
|
194
|
-
method: params.method,
|
|
195
|
-
params: params.params
|
|
196
|
-
});
|
|
197
|
-
break;
|
|
198
|
-
case 'detachedFromTab':
|
|
199
|
-
debugLogger('← Debugger detached from tab:', params);
|
|
200
|
-
this._connectedTabInfo = undefined;
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
async _handlePlaywrightMessage(message) {
|
|
205
|
-
debugLogger('← Playwright:', `${message.method} (id=${message.id})`);
|
|
206
|
-
const { id, sessionId, method, params } = message;
|
|
207
|
-
try {
|
|
208
|
-
const result = await this._handleCDPCommand(method, params, sessionId);
|
|
209
|
-
this._sendToPlaywright({ id, sessionId, result });
|
|
210
|
-
}
|
|
211
|
-
catch (e) {
|
|
212
|
-
debugLogger('Error in the extension:', e);
|
|
213
|
-
this._sendToPlaywright({
|
|
214
|
-
id,
|
|
215
|
-
sessionId,
|
|
216
|
-
error: { message: e.message }
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
async _handleCDPCommand(method, params, sessionId) {
|
|
221
|
-
switch (method) {
|
|
222
|
-
case 'Browser.getVersion': {
|
|
223
|
-
return {
|
|
224
|
-
protocolVersion: '1.3',
|
|
225
|
-
product: 'Chrome/Extension-Bridge',
|
|
226
|
-
userAgent: 'CDP-Bridge-Server/1.0.0',
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
case 'Browser.setDownloadBehavior': {
|
|
230
|
-
return {};
|
|
231
|
-
}
|
|
232
|
-
case 'Target.setAutoAttach': {
|
|
233
|
-
// Forward child session handling.
|
|
234
|
-
if (sessionId)
|
|
235
|
-
break;
|
|
236
|
-
// Simulate auto-attach behavior with real target info
|
|
237
|
-
const { targetInfo } = await this._extensionConnection.send('attachToTab');
|
|
238
|
-
this._connectedTabInfo = {
|
|
239
|
-
targetInfo,
|
|
240
|
-
sessionId: `pw-tab-${this._nextSessionId++}`,
|
|
241
|
-
};
|
|
242
|
-
debugLogger('Simulating auto-attach');
|
|
243
|
-
this._sendToPlaywright({
|
|
244
|
-
method: 'Target.attachedToTarget',
|
|
245
|
-
params: {
|
|
246
|
-
sessionId: this._connectedTabInfo.sessionId,
|
|
247
|
-
targetInfo: {
|
|
248
|
-
...this._connectedTabInfo.targetInfo,
|
|
249
|
-
attached: true,
|
|
250
|
-
},
|
|
251
|
-
waitingForDebugger: false
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
return {};
|
|
255
|
-
}
|
|
256
|
-
case 'Target.getTargetInfo': {
|
|
257
|
-
return this._connectedTabInfo?.targetInfo;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return await this._forwardToExtension(method, params, sessionId);
|
|
261
|
-
}
|
|
262
|
-
async _forwardToExtension(method, params, sessionId) {
|
|
263
|
-
if (!this._extensionConnection)
|
|
264
|
-
throw new Error('Extension not connected');
|
|
265
|
-
// Top level sessionId is only passed between the relay and the client.
|
|
266
|
-
if (this._connectedTabInfo?.sessionId === sessionId)
|
|
267
|
-
sessionId = undefined;
|
|
268
|
-
return await this._extensionConnection.send('forwardCDPCommand', { sessionId, method, params });
|
|
269
|
-
}
|
|
270
|
-
_sendToPlaywright(message) {
|
|
271
|
-
debugLogger('→ Playwright:', `${message.method ?? `response(id=${message.id})`}`);
|
|
272
|
-
this._playwrightConnection?.send(JSON.stringify(message));
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
class ExtensionConnection {
|
|
276
|
-
_ws;
|
|
277
|
-
_callbacks = new Map();
|
|
278
|
-
_lastId = 0;
|
|
279
|
-
onmessage;
|
|
280
|
-
onclose;
|
|
281
|
-
constructor(ws) {
|
|
282
|
-
this._ws = ws;
|
|
283
|
-
this._ws.on('message', this._onMessage.bind(this));
|
|
284
|
-
this._ws.on('close', this._onClose.bind(this));
|
|
285
|
-
this._ws.on('error', this._onError.bind(this));
|
|
286
|
-
}
|
|
287
|
-
async send(method, params, sessionId) {
|
|
288
|
-
if (this._ws.readyState !== WebSocket.OPEN)
|
|
289
|
-
throw new Error(`Unexpected WebSocket state: ${this._ws.readyState}`);
|
|
290
|
-
const id = ++this._lastId;
|
|
291
|
-
this._ws.send(JSON.stringify({ id, method, params, sessionId }));
|
|
292
|
-
const error = new Error(`Protocol error: ${method}`);
|
|
293
|
-
return new Promise((resolve, reject) => {
|
|
294
|
-
this._callbacks.set(id, { resolve, reject, error });
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
close(message) {
|
|
298
|
-
debugLogger('closing extension connection:', message);
|
|
299
|
-
if (this._ws.readyState === WebSocket.OPEN)
|
|
300
|
-
this._ws.close(1000, message);
|
|
301
|
-
}
|
|
302
|
-
_onMessage(event) {
|
|
303
|
-
const eventData = event.toString();
|
|
304
|
-
let parsedJson;
|
|
305
|
-
try {
|
|
306
|
-
parsedJson = JSON.parse(eventData);
|
|
307
|
-
}
|
|
308
|
-
catch (e) {
|
|
309
|
-
debugLogger(`<closing ws> Closing websocket due to malformed JSON. eventData=${eventData} e=${e?.message}`);
|
|
310
|
-
this._ws.close();
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
try {
|
|
314
|
-
this._handleParsedMessage(parsedJson);
|
|
315
|
-
}
|
|
316
|
-
catch (e) {
|
|
317
|
-
debugLogger(`<closing ws> Closing websocket due to failed onmessage callback. eventData=${eventData} e=${e?.message}`);
|
|
318
|
-
this._ws.close();
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
_handleParsedMessage(object) {
|
|
322
|
-
if (object.id && this._callbacks.has(object.id)) {
|
|
323
|
-
const callback = this._callbacks.get(object.id);
|
|
324
|
-
this._callbacks.delete(object.id);
|
|
325
|
-
if (object.error) {
|
|
326
|
-
const error = callback.error;
|
|
327
|
-
error.message = object.error;
|
|
328
|
-
callback.reject(error);
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
callback.resolve(object.result);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
else if (object.id) {
|
|
335
|
-
debugLogger('← Extension: unexpected response', object);
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
this.onmessage?.(object.method, object.params);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
_onClose(event) {
|
|
342
|
-
debugLogger(`<ws closed> code=${event.code} reason=${event.reason}`);
|
|
343
|
-
this._dispose();
|
|
344
|
-
this.onclose?.(this, event.reason);
|
|
345
|
-
}
|
|
346
|
-
_onError(event) {
|
|
347
|
-
debugLogger(`<ws error> message=${event.message} type=${event.type} target=${event.target}`);
|
|
348
|
-
this._dispose();
|
|
349
|
-
}
|
|
350
|
-
_dispose() {
|
|
351
|
-
for (const callback of this._callbacks.values())
|
|
352
|
-
callback.reject(new Error('WebSocket closed'));
|
|
353
|
-
this._callbacks.clear();
|
|
354
|
-
}
|
|
355
|
-
}
|
|
@@ -1,54 +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 debug from 'debug';
|
|
17
|
-
import * as playwright from 'playwright';
|
|
18
|
-
import { startHttpServer } from '../mcp/http.js';
|
|
19
|
-
import { CDPRelayServer } from './cdpRelay.js';
|
|
20
|
-
const debugLogger = debug('pw:mcp:relay');
|
|
21
|
-
export class ExtensionContextFactory {
|
|
22
|
-
_browserChannel;
|
|
23
|
-
_userDataDir;
|
|
24
|
-
constructor(browserChannel, userDataDir) {
|
|
25
|
-
this._browserChannel = browserChannel;
|
|
26
|
-
this._userDataDir = userDataDir;
|
|
27
|
-
}
|
|
28
|
-
async createContext(clientInfo, abortSignal) {
|
|
29
|
-
const browser = await this._obtainBrowser(clientInfo, abortSignal);
|
|
30
|
-
return {
|
|
31
|
-
browserContext: browser.contexts()[0],
|
|
32
|
-
close: async () => {
|
|
33
|
-
debugLogger('close() called for browser context');
|
|
34
|
-
await browser.close();
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
async _obtainBrowser(clientInfo, abortSignal) {
|
|
39
|
-
const relay = await this._startRelay(abortSignal);
|
|
40
|
-
await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal);
|
|
41
|
-
return await playwright.chromium.connectOverCDP(relay.cdpEndpoint());
|
|
42
|
-
}
|
|
43
|
-
async _startRelay(abortSignal) {
|
|
44
|
-
const httpServer = await startHttpServer({});
|
|
45
|
-
if (abortSignal.aborted) {
|
|
46
|
-
httpServer.close();
|
|
47
|
-
throw new Error(abortSignal.reason);
|
|
48
|
-
}
|
|
49
|
-
const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel, this._userDataDir);
|
|
50
|
-
abortSignal.addEventListener('abort', () => cdpRelayServer.stop());
|
|
51
|
-
debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`);
|
|
52
|
-
return cdpRelayServer;
|
|
53
|
-
}
|
|
54
|
-
}
|
package/lib/index.js
DELETED
|
@@ -1,40 +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 { BrowserServerBackend } from './browserServerBackend.js';
|
|
17
|
-
import { resolveConfig } from './config.js';
|
|
18
|
-
import { contextFactory } from './browserContextFactory.js';
|
|
19
|
-
import * as mcpServer from './mcp/server.js';
|
|
20
|
-
import { packageJSON } from './utils/package.js';
|
|
21
|
-
export async function createConnection(userConfig = {}, contextGetter) {
|
|
22
|
-
const config = await resolveConfig(userConfig);
|
|
23
|
-
const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config);
|
|
24
|
-
return mcpServer.createServer('Playwright', packageJSON.version, new BrowserServerBackend(config, factory), false);
|
|
25
|
-
}
|
|
26
|
-
class SimpleBrowserContextFactory {
|
|
27
|
-
name = 'custom';
|
|
28
|
-
description = 'Connect to a browser using a custom context getter';
|
|
29
|
-
_contextGetter;
|
|
30
|
-
constructor(contextGetter) {
|
|
31
|
-
this._contextGetter = contextGetter;
|
|
32
|
-
}
|
|
33
|
-
async createContext() {
|
|
34
|
-
const browserContext = await this._contextGetter();
|
|
35
|
-
return {
|
|
36
|
-
browserContext,
|
|
37
|
-
close: () => browserContext.close()
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
}
|
package/lib/loop/loop.js
DELETED
|
@@ -1,69 +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 debug from 'debug';
|
|
17
|
-
export async function runTask(delegate, client, task, oneShot = false) {
|
|
18
|
-
const { tools } = await client.listTools();
|
|
19
|
-
const taskContent = oneShot ? `Perform following task: ${task}.` : `Perform following task: ${task}. Once the task is complete, call the "done" tool.`;
|
|
20
|
-
const conversation = delegate.createConversation(taskContent, tools, oneShot);
|
|
21
|
-
for (let iteration = 0; iteration < 5; ++iteration) {
|
|
22
|
-
debug('history')('Making API call for iteration', iteration);
|
|
23
|
-
const toolCalls = await delegate.makeApiCall(conversation);
|
|
24
|
-
if (toolCalls.length === 0)
|
|
25
|
-
throw new Error('Call the "done" tool when the task is complete.');
|
|
26
|
-
const toolResults = [];
|
|
27
|
-
for (const toolCall of toolCalls) {
|
|
28
|
-
const doneResult = delegate.checkDoneToolCall(toolCall);
|
|
29
|
-
if (doneResult !== null)
|
|
30
|
-
return conversation.messages;
|
|
31
|
-
const { name, arguments: args, id } = toolCall;
|
|
32
|
-
try {
|
|
33
|
-
debug('tool')(name, args);
|
|
34
|
-
const response = await client.callTool({
|
|
35
|
-
name,
|
|
36
|
-
arguments: args,
|
|
37
|
-
});
|
|
38
|
-
const responseContent = (response.content || []);
|
|
39
|
-
debug('tool')(responseContent);
|
|
40
|
-
const text = responseContent.filter(part => part.type === 'text').map(part => part.text).join('\n');
|
|
41
|
-
toolResults.push({
|
|
42
|
-
toolCallId: id,
|
|
43
|
-
content: text,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
debug('tool')(error);
|
|
48
|
-
toolResults.push({
|
|
49
|
-
toolCallId: id,
|
|
50
|
-
content: `Error while executing tool "${name}": ${error instanceof Error ? error.message : String(error)}\n\nPlease try to recover and complete the task.`,
|
|
51
|
-
isError: true,
|
|
52
|
-
});
|
|
53
|
-
// Skip remaining tool calls for this iteration
|
|
54
|
-
for (const remainingToolCall of toolCalls.slice(toolCalls.indexOf(toolCall) + 1)) {
|
|
55
|
-
toolResults.push({
|
|
56
|
-
toolCallId: remainingToolCall.id,
|
|
57
|
-
content: `This tool call is skipped due to previous error.`,
|
|
58
|
-
isError: true,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
delegate.addToolResults(conversation, toolResults);
|
|
65
|
-
if (oneShot)
|
|
66
|
-
return conversation.messages;
|
|
67
|
-
}
|
|
68
|
-
throw new Error('Failed to perform step, max attempts reached');
|
|
69
|
-
}
|
package/lib/loop/loopClaude.js
DELETED
|
@@ -1,152 +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
|
-
const model = 'claude-sonnet-4-20250514';
|
|
17
|
-
export class ClaudeDelegate {
|
|
18
|
-
_anthropic;
|
|
19
|
-
async anthropic() {
|
|
20
|
-
if (!this._anthropic) {
|
|
21
|
-
const anthropic = await import('@anthropic-ai/sdk');
|
|
22
|
-
this._anthropic = new anthropic.Anthropic();
|
|
23
|
-
}
|
|
24
|
-
return this._anthropic;
|
|
25
|
-
}
|
|
26
|
-
createConversation(task, tools, oneShot) {
|
|
27
|
-
const llmTools = tools.map(tool => ({
|
|
28
|
-
name: tool.name,
|
|
29
|
-
description: tool.description || '',
|
|
30
|
-
inputSchema: tool.inputSchema,
|
|
31
|
-
}));
|
|
32
|
-
if (!oneShot) {
|
|
33
|
-
llmTools.push({
|
|
34
|
-
name: 'done',
|
|
35
|
-
description: 'Call this tool when the task is complete.',
|
|
36
|
-
inputSchema: {
|
|
37
|
-
type: 'object',
|
|
38
|
-
properties: {},
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
messages: [{
|
|
44
|
-
role: 'user',
|
|
45
|
-
content: task
|
|
46
|
-
}],
|
|
47
|
-
tools: llmTools,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
async makeApiCall(conversation) {
|
|
51
|
-
// Convert generic messages to Claude format
|
|
52
|
-
const claudeMessages = [];
|
|
53
|
-
for (const message of conversation.messages) {
|
|
54
|
-
if (message.role === 'user') {
|
|
55
|
-
claudeMessages.push({
|
|
56
|
-
role: 'user',
|
|
57
|
-
content: message.content
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
else if (message.role === 'assistant') {
|
|
61
|
-
const content = [];
|
|
62
|
-
// Add text content
|
|
63
|
-
if (message.content) {
|
|
64
|
-
content.push({
|
|
65
|
-
type: 'text',
|
|
66
|
-
text: message.content,
|
|
67
|
-
citations: []
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
// Add tool calls
|
|
71
|
-
if (message.toolCalls) {
|
|
72
|
-
for (const toolCall of message.toolCalls) {
|
|
73
|
-
content.push({
|
|
74
|
-
type: 'tool_use',
|
|
75
|
-
id: toolCall.id,
|
|
76
|
-
name: toolCall.name,
|
|
77
|
-
input: toolCall.arguments
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
claudeMessages.push({
|
|
82
|
-
role: 'assistant',
|
|
83
|
-
content
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
else if (message.role === 'tool') {
|
|
87
|
-
// Tool results are added differently - we need to find if there's already a user message with tool results
|
|
88
|
-
const lastMessage = claudeMessages[claudeMessages.length - 1];
|
|
89
|
-
const toolResult = {
|
|
90
|
-
type: 'tool_result',
|
|
91
|
-
tool_use_id: message.toolCallId,
|
|
92
|
-
content: message.content,
|
|
93
|
-
is_error: message.isError,
|
|
94
|
-
};
|
|
95
|
-
if (lastMessage && lastMessage.role === 'user' && Array.isArray(lastMessage.content)) {
|
|
96
|
-
// Add to existing tool results message
|
|
97
|
-
lastMessage.content.push(toolResult);
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
// Create new tool results message
|
|
101
|
-
claudeMessages.push({
|
|
102
|
-
role: 'user',
|
|
103
|
-
content: [toolResult]
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
// Convert generic tools to Claude format
|
|
109
|
-
const claudeTools = conversation.tools.map(tool => ({
|
|
110
|
-
name: tool.name,
|
|
111
|
-
description: tool.description,
|
|
112
|
-
input_schema: tool.inputSchema,
|
|
113
|
-
}));
|
|
114
|
-
const anthropic = await this.anthropic();
|
|
115
|
-
const response = await anthropic.messages.create({
|
|
116
|
-
model,
|
|
117
|
-
max_tokens: 10000,
|
|
118
|
-
messages: claudeMessages,
|
|
119
|
-
tools: claudeTools,
|
|
120
|
-
});
|
|
121
|
-
// Extract tool calls and add assistant message to generic conversation
|
|
122
|
-
const toolCalls = response.content.filter(block => block.type === 'tool_use');
|
|
123
|
-
const textContent = response.content.filter(block => block.type === 'text').map(block => block.text).join('');
|
|
124
|
-
const llmToolCalls = toolCalls.map(toolCall => ({
|
|
125
|
-
name: toolCall.name,
|
|
126
|
-
arguments: toolCall.input,
|
|
127
|
-
id: toolCall.id,
|
|
128
|
-
}));
|
|
129
|
-
// Add assistant message to generic conversation
|
|
130
|
-
conversation.messages.push({
|
|
131
|
-
role: 'assistant',
|
|
132
|
-
content: textContent,
|
|
133
|
-
toolCalls: llmToolCalls.length > 0 ? llmToolCalls : undefined
|
|
134
|
-
});
|
|
135
|
-
return llmToolCalls;
|
|
136
|
-
}
|
|
137
|
-
addToolResults(conversation, results) {
|
|
138
|
-
for (const result of results) {
|
|
139
|
-
conversation.messages.push({
|
|
140
|
-
role: 'tool',
|
|
141
|
-
toolCallId: result.toolCallId,
|
|
142
|
-
content: result.content,
|
|
143
|
-
isError: result.isError,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
checkDoneToolCall(toolCall) {
|
|
148
|
-
if (toolCall.name === 'done')
|
|
149
|
-
return toolCall.arguments.result;
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
}
|