@playwright/mcp 0.0.13 → 0.0.15
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 +134 -30
- package/index.d.ts +37 -24
- package/lib/context.js +169 -85
- package/lib/index.js +64 -9
- package/lib/manualPromise.js +116 -0
- package/lib/program.js +4 -53
- package/lib/server.js +23 -5
- package/lib/tools/common.js +34 -38
- package/lib/{resources → tools}/console.js +23 -13
- package/lib/tools/dialogs.js +52 -0
- package/lib/tools/files.js +21 -17
- package/lib/tools/install.js +7 -8
- package/lib/tools/keyboard.js +16 -19
- package/lib/tools/navigate.js +41 -43
- package/lib/tools/network.js +51 -0
- package/lib/tools/pdf.js +47 -11
- package/lib/tools/screen.js +140 -76
- package/lib/tools/snapshot.js +145 -100
- package/lib/tools/tabs.js +52 -56
- package/lib/tools/tool.js +4 -0
- package/lib/tools/utils.js +2 -2
- package/package.json +6 -3
package/lib/context.js
CHANGED
|
@@ -51,24 +51,46 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
51
51
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
52
52
|
};
|
|
53
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
-
exports.Context = void 0;
|
|
54
|
+
exports.Tab = exports.Context = void 0;
|
|
55
55
|
exports.generateLocator = generateLocator;
|
|
56
56
|
const playwright = __importStar(require("playwright"));
|
|
57
57
|
const yaml_1 = __importDefault(require("yaml"));
|
|
58
58
|
const utils_1 = require("./tools/utils");
|
|
59
|
+
const manualPromise_1 = require("./manualPromise");
|
|
59
60
|
class Context {
|
|
61
|
+
tools;
|
|
60
62
|
options;
|
|
61
63
|
_browser;
|
|
62
64
|
_browserContext;
|
|
63
65
|
_tabs = [];
|
|
64
66
|
_currentTab;
|
|
65
|
-
|
|
67
|
+
_modalStates = [];
|
|
68
|
+
_pendingAction;
|
|
69
|
+
constructor(tools, options) {
|
|
70
|
+
this.tools = tools;
|
|
66
71
|
this.options = options;
|
|
67
72
|
}
|
|
73
|
+
modalStates() {
|
|
74
|
+
return this._modalStates;
|
|
75
|
+
}
|
|
76
|
+
setModalState(modalState, inTab) {
|
|
77
|
+
this._modalStates.push({ ...modalState, tab: inTab });
|
|
78
|
+
}
|
|
79
|
+
clearModalState(modalState) {
|
|
80
|
+
this._modalStates = this._modalStates.filter(state => state !== modalState);
|
|
81
|
+
}
|
|
82
|
+
modalStatesMarkdown() {
|
|
83
|
+
const result = ['### Modal state'];
|
|
84
|
+
for (const state of this._modalStates) {
|
|
85
|
+
const tool = this.tools.find(tool => tool.clearsModalState === state.type);
|
|
86
|
+
result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`);
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
68
90
|
tabs() {
|
|
69
91
|
return this._tabs;
|
|
70
92
|
}
|
|
71
|
-
|
|
93
|
+
currentTabOrDie() {
|
|
72
94
|
if (!this._currentTab)
|
|
73
95
|
throw new Error('No current snapshot available. Capture a snapshot of navigate to a new location first.');
|
|
74
96
|
return this._currentTab;
|
|
@@ -89,7 +111,7 @@ class Context {
|
|
|
89
111
|
await context.newPage();
|
|
90
112
|
return this._currentTab;
|
|
91
113
|
}
|
|
92
|
-
async
|
|
114
|
+
async listTabsMarkdown() {
|
|
93
115
|
if (!this._tabs.length)
|
|
94
116
|
return '### No tabs open';
|
|
95
117
|
const lines = ['### Open tabs'];
|
|
@@ -103,9 +125,103 @@ class Context {
|
|
|
103
125
|
return lines.join('\n');
|
|
104
126
|
}
|
|
105
127
|
async closeTab(index) {
|
|
106
|
-
const tab = index === undefined ? this.
|
|
107
|
-
await tab
|
|
108
|
-
return await this.
|
|
128
|
+
const tab = index === undefined ? this._currentTab : this._tabs[index - 1];
|
|
129
|
+
await tab?.page.close();
|
|
130
|
+
return await this.listTabsMarkdown();
|
|
131
|
+
}
|
|
132
|
+
async run(tool, params) {
|
|
133
|
+
// Tab management is done outside of the action() call.
|
|
134
|
+
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params));
|
|
135
|
+
const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
|
|
136
|
+
const racingAction = action ? () => this._raceAgainstModalDialogs(action) : undefined;
|
|
137
|
+
if (resultOverride)
|
|
138
|
+
return resultOverride;
|
|
139
|
+
if (!this._currentTab) {
|
|
140
|
+
return {
|
|
141
|
+
content: [{
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: 'No open pages available. Use the "browser_navigate" tool to navigate to a page first.',
|
|
144
|
+
}],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const tab = this.currentTabOrDie();
|
|
148
|
+
// TODO: race against modal dialogs to resolve clicks.
|
|
149
|
+
let actionResult;
|
|
150
|
+
try {
|
|
151
|
+
if (waitForNetwork)
|
|
152
|
+
actionResult = await (0, utils_1.waitForCompletion)(this, tab.page, async () => racingAction?.()) ?? undefined;
|
|
153
|
+
else
|
|
154
|
+
actionResult = await racingAction?.() ?? undefined;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
if (captureSnapshot && !this._javaScriptBlocked())
|
|
158
|
+
await tab.captureSnapshot();
|
|
159
|
+
}
|
|
160
|
+
const result = [];
|
|
161
|
+
result.push(`- Ran Playwright code:
|
|
162
|
+
\`\`\`js
|
|
163
|
+
${code.join('\n')}
|
|
164
|
+
\`\`\`
|
|
165
|
+
`);
|
|
166
|
+
if (this.modalStates().length) {
|
|
167
|
+
result.push(...this.modalStatesMarkdown());
|
|
168
|
+
return {
|
|
169
|
+
content: [{
|
|
170
|
+
type: 'text',
|
|
171
|
+
text: result.join('\n'),
|
|
172
|
+
}],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (this.tabs().length > 1)
|
|
176
|
+
result.push(await this.listTabsMarkdown(), '');
|
|
177
|
+
if (this.tabs().length > 1)
|
|
178
|
+
result.push('### Current tab');
|
|
179
|
+
result.push(`- Page URL: ${tab.page.url()}`, `- Page Title: ${await tab.page.title()}`);
|
|
180
|
+
if (captureSnapshot && tab.hasSnapshot())
|
|
181
|
+
result.push(tab.snapshotOrDie().text());
|
|
182
|
+
const content = actionResult?.content ?? [];
|
|
183
|
+
return {
|
|
184
|
+
content: [
|
|
185
|
+
...content,
|
|
186
|
+
{
|
|
187
|
+
type: 'text',
|
|
188
|
+
text: result.join('\n'),
|
|
189
|
+
}
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
async waitForTimeout(time) {
|
|
194
|
+
if (this._currentTab && !this._javaScriptBlocked())
|
|
195
|
+
await this._currentTab.page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
|
|
196
|
+
else
|
|
197
|
+
await new Promise(f => setTimeout(f, time));
|
|
198
|
+
}
|
|
199
|
+
async _raceAgainstModalDialogs(action) {
|
|
200
|
+
this._pendingAction = {
|
|
201
|
+
dialogShown: new manualPromise_1.ManualPromise(),
|
|
202
|
+
};
|
|
203
|
+
let result;
|
|
204
|
+
try {
|
|
205
|
+
await Promise.race([
|
|
206
|
+
action().then(r => result = r),
|
|
207
|
+
this._pendingAction.dialogShown,
|
|
208
|
+
]);
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
this._pendingAction = undefined;
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
_javaScriptBlocked() {
|
|
216
|
+
return this._modalStates.some(state => state.type === 'dialog');
|
|
217
|
+
}
|
|
218
|
+
dialogShown(tab, dialog) {
|
|
219
|
+
this.setModalState({
|
|
220
|
+
type: 'dialog',
|
|
221
|
+
description: `"${dialog.type()}" dialog with message "${dialog.message()}"`,
|
|
222
|
+
dialog,
|
|
223
|
+
}, tab);
|
|
224
|
+
this._pendingAction?.dialogShown.resolve();
|
|
109
225
|
}
|
|
110
226
|
_onPageCreated(page) {
|
|
111
227
|
const tab = new Tab(this, page, tab => this._onPageClosed(tab));
|
|
@@ -114,23 +230,26 @@ class Context {
|
|
|
114
230
|
this._currentTab = tab;
|
|
115
231
|
}
|
|
116
232
|
_onPageClosed(tab) {
|
|
233
|
+
this._modalStates = this._modalStates.filter(state => state.tab !== tab);
|
|
117
234
|
const index = this._tabs.indexOf(tab);
|
|
118
235
|
if (index === -1)
|
|
119
236
|
return;
|
|
120
237
|
this._tabs.splice(index, 1);
|
|
121
238
|
if (this._currentTab === tab)
|
|
122
239
|
this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)];
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
void this._browserContext.close().then(() => browser?.close()).catch(() => { });
|
|
126
|
-
this._browser = undefined;
|
|
127
|
-
this._browserContext = undefined;
|
|
128
|
-
}
|
|
240
|
+
if (this._browserContext && !this._tabs.length)
|
|
241
|
+
void this.close();
|
|
129
242
|
}
|
|
130
243
|
async close() {
|
|
131
244
|
if (!this._browserContext)
|
|
132
245
|
return;
|
|
133
|
-
|
|
246
|
+
const browserContext = this._browserContext;
|
|
247
|
+
const browser = this._browser;
|
|
248
|
+
this._browserContext = undefined;
|
|
249
|
+
this._browser = undefined;
|
|
250
|
+
await browserContext?.close().then(async () => {
|
|
251
|
+
await browser?.close();
|
|
252
|
+
}).catch(() => { });
|
|
134
253
|
}
|
|
135
254
|
async _ensureBrowserContext() {
|
|
136
255
|
if (!this._browserContext) {
|
|
@@ -179,7 +298,7 @@ class Tab {
|
|
|
179
298
|
context;
|
|
180
299
|
page;
|
|
181
300
|
_console = [];
|
|
182
|
-
|
|
301
|
+
_requests = new Map();
|
|
183
302
|
_snapshot;
|
|
184
303
|
_onPageClose;
|
|
185
304
|
constructor(context, page, onPageClose) {
|
|
@@ -187,18 +306,30 @@ class Tab {
|
|
|
187
306
|
this.page = page;
|
|
188
307
|
this._onPageClose = onPageClose;
|
|
189
308
|
page.on('console', event => this._console.push(event));
|
|
309
|
+
page.on('request', request => this._requests.set(request, null));
|
|
310
|
+
page.on('response', response => this._requests.set(response.request(), response));
|
|
190
311
|
page.on('framenavigated', frame => {
|
|
191
312
|
if (!frame.parentFrame())
|
|
192
|
-
this.
|
|
313
|
+
this._clearCollectedArtifacts();
|
|
193
314
|
});
|
|
194
315
|
page.on('close', () => this._onClose());
|
|
195
|
-
page.on('filechooser', chooser =>
|
|
316
|
+
page.on('filechooser', chooser => {
|
|
317
|
+
this.context.setModalState({
|
|
318
|
+
type: 'fileChooser',
|
|
319
|
+
description: 'File chooser',
|
|
320
|
+
fileChooser: chooser,
|
|
321
|
+
}, this);
|
|
322
|
+
});
|
|
323
|
+
page.on('dialog', dialog => this.context.dialogShown(this, dialog));
|
|
196
324
|
page.setDefaultNavigationTimeout(60000);
|
|
197
325
|
page.setDefaultTimeout(5000);
|
|
198
326
|
}
|
|
199
|
-
|
|
200
|
-
this._fileChooser = undefined;
|
|
327
|
+
_clearCollectedArtifacts() {
|
|
201
328
|
this._console.length = 0;
|
|
329
|
+
this._requests.clear();
|
|
330
|
+
}
|
|
331
|
+
_onClose() {
|
|
332
|
+
this._clearCollectedArtifacts();
|
|
202
333
|
this._onPageClose(this);
|
|
203
334
|
}
|
|
204
335
|
async navigate(url) {
|
|
@@ -206,68 +337,25 @@ class Tab {
|
|
|
206
337
|
// Cap load event to 5 seconds, the page is operational at this point.
|
|
207
338
|
await this.page.waitForLoadState('load', { timeout: 5000 }).catch(() => { });
|
|
208
339
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
if (!options?.noClearFileChooser)
|
|
213
|
-
this._fileChooser = undefined;
|
|
214
|
-
if (options?.waitForCompletion)
|
|
215
|
-
runResult = await (0, utils_1.waitForCompletion)(this.page, () => callback(this)) ?? undefined;
|
|
216
|
-
else
|
|
217
|
-
runResult = await callback(this) ?? undefined;
|
|
218
|
-
}
|
|
219
|
-
finally {
|
|
220
|
-
if (options?.captureSnapshot)
|
|
221
|
-
this._snapshot = await PageSnapshot.create(this.page);
|
|
222
|
-
}
|
|
223
|
-
const result = [];
|
|
224
|
-
result.push(`- Ran code:
|
|
225
|
-
\`\`\`js
|
|
226
|
-
${runResult.code.join('\n')}
|
|
227
|
-
\`\`\`
|
|
228
|
-
`);
|
|
229
|
-
if (this.context.tabs().length > 1)
|
|
230
|
-
result.push(await this.context.listTabs(), '');
|
|
231
|
-
if (this._snapshot) {
|
|
232
|
-
if (this.context.tabs().length > 1)
|
|
233
|
-
result.push('### Current tab');
|
|
234
|
-
result.push(this._snapshot.text({ hasFileChooser: !!this._fileChooser }));
|
|
235
|
-
}
|
|
236
|
-
return {
|
|
237
|
-
content: [{
|
|
238
|
-
type: 'text',
|
|
239
|
-
text: result.join('\n'),
|
|
240
|
-
}],
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
async runAndWait(callback, options) {
|
|
244
|
-
return await this.run(callback, {
|
|
245
|
-
waitForCompletion: true,
|
|
246
|
-
...options,
|
|
247
|
-
});
|
|
340
|
+
hasSnapshot() {
|
|
341
|
+
return !!this._snapshot;
|
|
248
342
|
}
|
|
249
|
-
|
|
250
|
-
return await this.run(tab => callback(tab.lastSnapshot()), {
|
|
251
|
-
captureSnapshot: true,
|
|
252
|
-
waitForCompletion: true,
|
|
253
|
-
...options,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
lastSnapshot() {
|
|
343
|
+
snapshotOrDie() {
|
|
257
344
|
if (!this._snapshot)
|
|
258
345
|
throw new Error('No snapshot available');
|
|
259
346
|
return this._snapshot;
|
|
260
347
|
}
|
|
261
|
-
|
|
348
|
+
console() {
|
|
262
349
|
return this._console;
|
|
263
350
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
this.
|
|
351
|
+
requests() {
|
|
352
|
+
return this._requests;
|
|
353
|
+
}
|
|
354
|
+
async captureSnapshot() {
|
|
355
|
+
this._snapshot = await PageSnapshot.create(this.page);
|
|
269
356
|
}
|
|
270
357
|
}
|
|
358
|
+
exports.Tab = Tab;
|
|
271
359
|
class PageSnapshot {
|
|
272
360
|
_frameLocators = [];
|
|
273
361
|
_text;
|
|
@@ -278,25 +366,21 @@ class PageSnapshot {
|
|
|
278
366
|
await snapshot._build(page);
|
|
279
367
|
return snapshot;
|
|
280
368
|
}
|
|
281
|
-
text(
|
|
282
|
-
|
|
283
|
-
if (options.hasFileChooser) {
|
|
284
|
-
results.push('- There is a file chooser visible that requires browser_file_upload to be called');
|
|
285
|
-
results.push('');
|
|
286
|
-
}
|
|
287
|
-
results.push(this._text);
|
|
288
|
-
return results.join('\n');
|
|
369
|
+
text() {
|
|
370
|
+
return this._text;
|
|
289
371
|
}
|
|
290
372
|
async _build(page) {
|
|
291
373
|
const yamlDocument = await this._snapshotFrame(page);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
374
|
+
this._text = [
|
|
375
|
+
`- Page Snapshot`,
|
|
376
|
+
'```yaml',
|
|
377
|
+
yamlDocument.toString({ indentSeq: false }).trim(),
|
|
378
|
+
'```',
|
|
379
|
+
].join('\n');
|
|
296
380
|
}
|
|
297
381
|
async _snapshotFrame(frame) {
|
|
298
382
|
const frameIndex = this._frameLocators.push(frame) - 1;
|
|
299
|
-
const snapshotString = await frame.locator('body').ariaSnapshot({ ref: true });
|
|
383
|
+
const snapshotString = await frame.locator('body').ariaSnapshot({ ref: true, emitGeneric: true });
|
|
300
384
|
const snapshot = yaml_1.default.parseDocument(snapshotString);
|
|
301
385
|
const visit = async (node) => {
|
|
302
386
|
if (yaml_1.default.isPair(node)) {
|
package/lib/index.js
CHANGED
|
@@ -19,52 +19,107 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
19
19
|
};
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
21
|
exports.createServer = createServer;
|
|
22
|
+
const path_1 = __importDefault(require("path"));
|
|
23
|
+
const os_1 = __importDefault(require("os"));
|
|
24
|
+
const fs_1 = __importDefault(require("fs"));
|
|
22
25
|
const server_1 = require("./server");
|
|
23
26
|
const common_1 = __importDefault(require("./tools/common"));
|
|
27
|
+
const console_1 = __importDefault(require("./tools/console"));
|
|
28
|
+
const dialogs_1 = __importDefault(require("./tools/dialogs"));
|
|
24
29
|
const files_1 = __importDefault(require("./tools/files"));
|
|
25
30
|
const install_1 = __importDefault(require("./tools/install"));
|
|
26
31
|
const keyboard_1 = __importDefault(require("./tools/keyboard"));
|
|
27
32
|
const navigate_1 = __importDefault(require("./tools/navigate"));
|
|
33
|
+
const network_1 = __importDefault(require("./tools/network"));
|
|
28
34
|
const pdf_1 = __importDefault(require("./tools/pdf"));
|
|
29
35
|
const snapshot_1 = __importDefault(require("./tools/snapshot"));
|
|
30
36
|
const tabs_1 = __importDefault(require("./tools/tabs"));
|
|
31
37
|
const screen_1 = __importDefault(require("./tools/screen"));
|
|
32
|
-
const console_1 = require("./resources/console");
|
|
33
38
|
const snapshotTools = [
|
|
34
39
|
...(0, common_1.default)(true),
|
|
40
|
+
...console_1.default,
|
|
41
|
+
...(0, dialogs_1.default)(true),
|
|
35
42
|
...(0, files_1.default)(true),
|
|
36
43
|
...install_1.default,
|
|
37
44
|
...(0, keyboard_1.default)(true),
|
|
38
45
|
...(0, navigate_1.default)(true),
|
|
46
|
+
...network_1.default,
|
|
39
47
|
...pdf_1.default,
|
|
40
48
|
...snapshot_1.default,
|
|
41
49
|
...(0, tabs_1.default)(true),
|
|
42
50
|
];
|
|
43
51
|
const screenshotTools = [
|
|
44
52
|
...(0, common_1.default)(false),
|
|
53
|
+
...console_1.default,
|
|
54
|
+
...(0, dialogs_1.default)(false),
|
|
45
55
|
...(0, files_1.default)(false),
|
|
46
56
|
...install_1.default,
|
|
47
57
|
...(0, keyboard_1.default)(false),
|
|
48
58
|
...(0, navigate_1.default)(false),
|
|
59
|
+
...network_1.default,
|
|
49
60
|
...pdf_1.default,
|
|
50
61
|
...screen_1.default,
|
|
51
62
|
...(0, tabs_1.default)(false),
|
|
52
63
|
];
|
|
53
|
-
const resources = [
|
|
54
|
-
console_1.console,
|
|
55
|
-
];
|
|
56
64
|
const packageJSON = require('../package.json');
|
|
57
|
-
function createServer(options) {
|
|
65
|
+
async function createServer(options) {
|
|
66
|
+
let browserName;
|
|
67
|
+
let channel;
|
|
68
|
+
switch (options?.browser) {
|
|
69
|
+
case 'chrome':
|
|
70
|
+
case 'chrome-beta':
|
|
71
|
+
case 'chrome-canary':
|
|
72
|
+
case 'chrome-dev':
|
|
73
|
+
case 'msedge':
|
|
74
|
+
case 'msedge-beta':
|
|
75
|
+
case 'msedge-canary':
|
|
76
|
+
case 'msedge-dev':
|
|
77
|
+
browserName = 'chromium';
|
|
78
|
+
channel = options.browser;
|
|
79
|
+
break;
|
|
80
|
+
case 'chromium':
|
|
81
|
+
browserName = 'chromium';
|
|
82
|
+
break;
|
|
83
|
+
case 'firefox':
|
|
84
|
+
browserName = 'firefox';
|
|
85
|
+
break;
|
|
86
|
+
case 'webkit':
|
|
87
|
+
browserName = 'webkit';
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
browserName = 'chromium';
|
|
91
|
+
channel = 'chrome';
|
|
92
|
+
}
|
|
93
|
+
const userDataDir = options?.userDataDir ?? await createUserDataDir(browserName);
|
|
94
|
+
const launchOptions = {
|
|
95
|
+
headless: !!(options?.headless ?? (os_1.default.platform() === 'linux' && !process.env.DISPLAY)),
|
|
96
|
+
channel,
|
|
97
|
+
executablePath: options?.executablePath,
|
|
98
|
+
};
|
|
58
99
|
const allTools = options?.vision ? screenshotTools : snapshotTools;
|
|
59
100
|
const tools = allTools.filter(tool => !options?.capabilities || tool.capability === 'core' || options.capabilities.includes(tool.capability));
|
|
60
101
|
return (0, server_1.createServerWithTools)({
|
|
61
102
|
name: 'Playwright',
|
|
62
103
|
version: packageJSON.version,
|
|
63
104
|
tools,
|
|
64
|
-
resources,
|
|
65
|
-
browserName
|
|
66
|
-
userDataDir
|
|
67
|
-
launchOptions
|
|
105
|
+
resources: [],
|
|
106
|
+
browserName,
|
|
107
|
+
userDataDir,
|
|
108
|
+
launchOptions,
|
|
68
109
|
cdpEndpoint: options?.cdpEndpoint,
|
|
69
110
|
});
|
|
70
111
|
}
|
|
112
|
+
async function createUserDataDir(browserName) {
|
|
113
|
+
let cacheDirectory;
|
|
114
|
+
if (process.platform === 'linux')
|
|
115
|
+
cacheDirectory = process.env.XDG_CACHE_HOME || path_1.default.join(os_1.default.homedir(), '.cache');
|
|
116
|
+
else if (process.platform === 'darwin')
|
|
117
|
+
cacheDirectory = path_1.default.join(os_1.default.homedir(), 'Library', 'Caches');
|
|
118
|
+
else if (process.platform === 'win32')
|
|
119
|
+
cacheDirectory = process.env.LOCALAPPDATA || path_1.default.join(os_1.default.homedir(), 'AppData', 'Local');
|
|
120
|
+
else
|
|
121
|
+
throw new Error('Unsupported platform: ' + process.platform);
|
|
122
|
+
const result = path_1.default.join(cacheDirectory, 'ms-playwright', `mcp-${browserName}-profile`);
|
|
123
|
+
await fs_1.default.promises.mkdir(result, { recursive: true });
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Microsoft Corporation.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.LongStandingScope = exports.ManualPromise = void 0;
|
|
19
|
+
class ManualPromise extends Promise {
|
|
20
|
+
_resolve;
|
|
21
|
+
_reject;
|
|
22
|
+
_isDone;
|
|
23
|
+
constructor() {
|
|
24
|
+
let resolve;
|
|
25
|
+
let reject;
|
|
26
|
+
super((f, r) => {
|
|
27
|
+
resolve = f;
|
|
28
|
+
reject = r;
|
|
29
|
+
});
|
|
30
|
+
this._isDone = false;
|
|
31
|
+
this._resolve = resolve;
|
|
32
|
+
this._reject = reject;
|
|
33
|
+
}
|
|
34
|
+
isDone() {
|
|
35
|
+
return this._isDone;
|
|
36
|
+
}
|
|
37
|
+
resolve(t) {
|
|
38
|
+
this._isDone = true;
|
|
39
|
+
this._resolve(t);
|
|
40
|
+
}
|
|
41
|
+
reject(e) {
|
|
42
|
+
this._isDone = true;
|
|
43
|
+
this._reject(e);
|
|
44
|
+
}
|
|
45
|
+
static get [Symbol.species]() {
|
|
46
|
+
return Promise;
|
|
47
|
+
}
|
|
48
|
+
get [Symbol.toStringTag]() {
|
|
49
|
+
return 'ManualPromise';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.ManualPromise = ManualPromise;
|
|
53
|
+
class LongStandingScope {
|
|
54
|
+
_terminateError;
|
|
55
|
+
_closeError;
|
|
56
|
+
_terminatePromises = new Map();
|
|
57
|
+
_isClosed = false;
|
|
58
|
+
reject(error) {
|
|
59
|
+
this._isClosed = true;
|
|
60
|
+
this._terminateError = error;
|
|
61
|
+
for (const p of this._terminatePromises.keys())
|
|
62
|
+
p.resolve(error);
|
|
63
|
+
}
|
|
64
|
+
close(error) {
|
|
65
|
+
this._isClosed = true;
|
|
66
|
+
this._closeError = error;
|
|
67
|
+
for (const [p, frames] of this._terminatePromises)
|
|
68
|
+
p.resolve(cloneError(error, frames));
|
|
69
|
+
}
|
|
70
|
+
isClosed() {
|
|
71
|
+
return this._isClosed;
|
|
72
|
+
}
|
|
73
|
+
static async raceMultiple(scopes, promise) {
|
|
74
|
+
return Promise.race(scopes.map(s => s.race(promise)));
|
|
75
|
+
}
|
|
76
|
+
async race(promise) {
|
|
77
|
+
return this._race(Array.isArray(promise) ? promise : [promise], false);
|
|
78
|
+
}
|
|
79
|
+
async safeRace(promise, defaultValue) {
|
|
80
|
+
return this._race([promise], true, defaultValue);
|
|
81
|
+
}
|
|
82
|
+
async _race(promises, safe, defaultValue) {
|
|
83
|
+
const terminatePromise = new ManualPromise();
|
|
84
|
+
const frames = captureRawStack();
|
|
85
|
+
if (this._terminateError)
|
|
86
|
+
terminatePromise.resolve(this._terminateError);
|
|
87
|
+
if (this._closeError)
|
|
88
|
+
terminatePromise.resolve(cloneError(this._closeError, frames));
|
|
89
|
+
this._terminatePromises.set(terminatePromise, frames);
|
|
90
|
+
try {
|
|
91
|
+
return await Promise.race([
|
|
92
|
+
terminatePromise.then(e => safe ? defaultValue : Promise.reject(e)),
|
|
93
|
+
...promises
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
this._terminatePromises.delete(terminatePromise);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.LongStandingScope = LongStandingScope;
|
|
102
|
+
function cloneError(error, frames) {
|
|
103
|
+
const clone = new Error();
|
|
104
|
+
clone.name = error.name;
|
|
105
|
+
clone.message = error.message;
|
|
106
|
+
clone.stack = [error.name + ':' + error.message, ...frames].join('\n');
|
|
107
|
+
return clone;
|
|
108
|
+
}
|
|
109
|
+
function captureRawStack() {
|
|
110
|
+
const stackTraceLimit = Error.stackTraceLimit;
|
|
111
|
+
Error.stackTraceLimit = 50;
|
|
112
|
+
const error = new Error();
|
|
113
|
+
const stack = error.stack || '';
|
|
114
|
+
Error.stackTraceLimit = stackTraceLimit;
|
|
115
|
+
return stack.split('\n');
|
|
116
|
+
}
|
package/lib/program.js
CHANGED
|
@@ -19,9 +19,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
19
19
|
};
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
21
|
const http_1 = __importDefault(require("http"));
|
|
22
|
-
const fs_1 = __importDefault(require("fs"));
|
|
23
|
-
const os_1 = __importDefault(require("os"));
|
|
24
|
-
const path_1 = __importDefault(require("path"));
|
|
25
22
|
const commander_1 = require("commander");
|
|
26
23
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
27
24
|
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
@@ -41,43 +38,11 @@ commander_1.program
|
|
|
41
38
|
.option('--user-data-dir <path>', 'Path to the user data directory')
|
|
42
39
|
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
|
|
43
40
|
.action(async (options) => {
|
|
44
|
-
let browserName;
|
|
45
|
-
let channel;
|
|
46
|
-
switch (options.browser) {
|
|
47
|
-
case 'chrome':
|
|
48
|
-
case 'chrome-beta':
|
|
49
|
-
case 'chrome-canary':
|
|
50
|
-
case 'chrome-dev':
|
|
51
|
-
case 'msedge':
|
|
52
|
-
case 'msedge-beta':
|
|
53
|
-
case 'msedge-canary':
|
|
54
|
-
case 'msedge-dev':
|
|
55
|
-
browserName = 'chromium';
|
|
56
|
-
channel = options.browser;
|
|
57
|
-
break;
|
|
58
|
-
case 'chromium':
|
|
59
|
-
browserName = 'chromium';
|
|
60
|
-
break;
|
|
61
|
-
case 'firefox':
|
|
62
|
-
browserName = 'firefox';
|
|
63
|
-
break;
|
|
64
|
-
case 'webkit':
|
|
65
|
-
browserName = 'webkit';
|
|
66
|
-
break;
|
|
67
|
-
default:
|
|
68
|
-
browserName = 'chromium';
|
|
69
|
-
channel = 'chrome';
|
|
70
|
-
}
|
|
71
|
-
const launchOptions = {
|
|
72
|
-
headless: !!(options.headless ?? (os_1.default.platform() === 'linux' && !process.env.DISPLAY)),
|
|
73
|
-
channel,
|
|
74
|
-
executablePath: options.executablePath,
|
|
75
|
-
};
|
|
76
|
-
const userDataDir = options.userDataDir ?? await createUserDataDir(browserName);
|
|
77
41
|
const serverList = new server_1.ServerList(() => (0, index_1.createServer)({
|
|
78
|
-
|
|
79
|
-
userDataDir,
|
|
80
|
-
|
|
42
|
+
browser: options.browser,
|
|
43
|
+
userDataDir: options.userDataDir,
|
|
44
|
+
headless: options.headless,
|
|
45
|
+
executablePath: options.executablePath,
|
|
81
46
|
vision: !!options.vision,
|
|
82
47
|
cdpEndpoint: options.cdpEndpoint,
|
|
83
48
|
capabilities: options.caps?.split(',').map((c) => c.trim()),
|
|
@@ -102,20 +67,6 @@ function setupExitWatchdog(serverList) {
|
|
|
102
67
|
process.on('SIGTERM', handleExit);
|
|
103
68
|
}
|
|
104
69
|
commander_1.program.parse(process.argv);
|
|
105
|
-
async function createUserDataDir(browserName) {
|
|
106
|
-
let cacheDirectory;
|
|
107
|
-
if (process.platform === 'linux')
|
|
108
|
-
cacheDirectory = process.env.XDG_CACHE_HOME || path_1.default.join(os_1.default.homedir(), '.cache');
|
|
109
|
-
else if (process.platform === 'darwin')
|
|
110
|
-
cacheDirectory = path_1.default.join(os_1.default.homedir(), 'Library', 'Caches');
|
|
111
|
-
else if (process.platform === 'win32')
|
|
112
|
-
cacheDirectory = process.env.LOCALAPPDATA || path_1.default.join(os_1.default.homedir(), 'AppData', 'Local');
|
|
113
|
-
else
|
|
114
|
-
throw new Error('Unsupported platform: ' + process.platform);
|
|
115
|
-
const result = path_1.default.join(cacheDirectory, 'ms-playwright', `mcp-${browserName}-profile`);
|
|
116
|
-
await fs_1.default.promises.mkdir(result, { recursive: true });
|
|
117
|
-
return result;
|
|
118
|
-
}
|
|
119
70
|
async function startSSEServer(port, serverList) {
|
|
120
71
|
const sessions = new Map();
|
|
121
72
|
const httpServer = http_1.default.createServer(async (req, res) => {
|