@playwright/mcp 0.0.15 → 0.0.16
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 +124 -65
- package/config.d.ts +113 -0
- package/index.d.ts +2 -39
- package/lib/config.js +157 -0
- package/lib/context.js +34 -163
- package/lib/index.js +4 -59
- package/lib/pageSnapshot.js +96 -0
- package/lib/program.js +12 -85
- package/lib/server.js +14 -35
- package/lib/tab.js +81 -0
- package/lib/tools/install.js +1 -1
- package/lib/tools/pdf.js +2 -7
- package/lib/tools/snapshot.js +18 -19
- package/lib/transport.js +124 -0
- package/package.json +5 -5
package/lib/context.js
CHANGED
|
@@ -47,28 +47,26 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
47
47
|
return result;
|
|
48
48
|
};
|
|
49
49
|
})();
|
|
50
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
51
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
52
|
-
};
|
|
53
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
-
exports.
|
|
51
|
+
exports.Context = void 0;
|
|
55
52
|
exports.generateLocator = generateLocator;
|
|
56
53
|
const playwright = __importStar(require("playwright"));
|
|
57
|
-
const yaml_1 = __importDefault(require("yaml"));
|
|
58
54
|
const utils_1 = require("./tools/utils");
|
|
59
55
|
const manualPromise_1 = require("./manualPromise");
|
|
56
|
+
const tab_1 = require("./tab");
|
|
60
57
|
class Context {
|
|
61
58
|
tools;
|
|
62
|
-
|
|
59
|
+
config;
|
|
63
60
|
_browser;
|
|
64
61
|
_browserContext;
|
|
62
|
+
_createBrowserContextPromise;
|
|
65
63
|
_tabs = [];
|
|
66
64
|
_currentTab;
|
|
67
65
|
_modalStates = [];
|
|
68
66
|
_pendingAction;
|
|
69
|
-
constructor(tools,
|
|
67
|
+
constructor(tools, config) {
|
|
70
68
|
this.tools = tools;
|
|
71
|
-
this.
|
|
69
|
+
this.config = config;
|
|
72
70
|
}
|
|
73
71
|
modalStates() {
|
|
74
72
|
return this._modalStates;
|
|
@@ -81,6 +79,8 @@ class Context {
|
|
|
81
79
|
}
|
|
82
80
|
modalStatesMarkdown() {
|
|
83
81
|
const result = ['### Modal state'];
|
|
82
|
+
if (this._modalStates.length === 0)
|
|
83
|
+
result.push('- There is no modal state present');
|
|
84
84
|
for (const state of this._modalStates) {
|
|
85
85
|
const tool = this.tools.find(tool => tool.clearsModalState === state.type);
|
|
86
86
|
result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`);
|
|
@@ -224,7 +224,7 @@ ${code.join('\n')}
|
|
|
224
224
|
this._pendingAction?.dialogShown.resolve();
|
|
225
225
|
}
|
|
226
226
|
_onPageCreated(page) {
|
|
227
|
-
const tab = new Tab(this, page, tab => this._onPageClosed(tab));
|
|
227
|
+
const tab = new tab_1.Tab(this, page, tab => this._onPageClosed(tab));
|
|
228
228
|
this._tabs.push(tab);
|
|
229
229
|
if (!this._currentTab)
|
|
230
230
|
this._currentTab = tab;
|
|
@@ -245,6 +245,7 @@ ${code.join('\n')}
|
|
|
245
245
|
return;
|
|
246
246
|
const browserContext = this._browserContext;
|
|
247
247
|
const browser = this._browser;
|
|
248
|
+
this._createBrowserContextPromise = undefined;
|
|
248
249
|
this._browserContext = undefined;
|
|
249
250
|
this._browser = undefined;
|
|
250
251
|
await browserContext?.close().then(async () => {
|
|
@@ -263,170 +264,40 @@ ${code.join('\n')}
|
|
|
263
264
|
return this._browserContext;
|
|
264
265
|
}
|
|
265
266
|
async _createBrowserContext() {
|
|
266
|
-
if (this.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
267
|
+
if (!this._createBrowserContextPromise)
|
|
268
|
+
this._createBrowserContextPromise = this._innerCreateBrowserContext();
|
|
269
|
+
return this._createBrowserContextPromise;
|
|
270
|
+
}
|
|
271
|
+
async _innerCreateBrowserContext() {
|
|
272
|
+
if (this.config.browser?.remoteEndpoint) {
|
|
273
|
+
const url = new URL(this.config.browser?.remoteEndpoint);
|
|
274
|
+
if (this.config.browser.browserName)
|
|
275
|
+
url.searchParams.set('browser', this.config.browser.browserName);
|
|
276
|
+
if (this.config.browser.launchOptions)
|
|
277
|
+
url.searchParams.set('launch-options', JSON.stringify(this.config.browser.launchOptions));
|
|
278
|
+
const browser = await playwright[this.config.browser?.browserName ?? 'chromium'].connect(String(url));
|
|
273
279
|
const browserContext = await browser.newContext();
|
|
274
280
|
return { browser, browserContext };
|
|
275
281
|
}
|
|
276
|
-
if (this.
|
|
277
|
-
const browser = await playwright.chromium.connectOverCDP(this.
|
|
282
|
+
if (this.config.browser?.cdpEndpoint) {
|
|
283
|
+
const browser = await playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint);
|
|
278
284
|
const browserContext = browser.contexts()[0];
|
|
279
285
|
return { browser, browserContext };
|
|
280
286
|
}
|
|
281
|
-
const browserContext = await this.
|
|
287
|
+
const browserContext = await launchPersistentContext(this.config.browser);
|
|
282
288
|
return { browserContext };
|
|
283
289
|
}
|
|
284
|
-
async _launchPersistentContext() {
|
|
285
|
-
try {
|
|
286
|
-
const browserType = this.options.browserName ? playwright[this.options.browserName] : playwright.chromium;
|
|
287
|
-
return await browserType.launchPersistentContext(this.options.userDataDir, this.options.launchOptions);
|
|
288
|
-
}
|
|
289
|
-
catch (error) {
|
|
290
|
-
if (error.message.includes('Executable doesn\'t exist'))
|
|
291
|
-
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
|
|
292
|
-
throw error;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
290
|
}
|
|
296
291
|
exports.Context = Context;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
this.page = page;
|
|
307
|
-
this._onPageClose = onPageClose;
|
|
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));
|
|
311
|
-
page.on('framenavigated', frame => {
|
|
312
|
-
if (!frame.parentFrame())
|
|
313
|
-
this._clearCollectedArtifacts();
|
|
314
|
-
});
|
|
315
|
-
page.on('close', () => this._onClose());
|
|
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));
|
|
324
|
-
page.setDefaultNavigationTimeout(60000);
|
|
325
|
-
page.setDefaultTimeout(5000);
|
|
326
|
-
}
|
|
327
|
-
_clearCollectedArtifacts() {
|
|
328
|
-
this._console.length = 0;
|
|
329
|
-
this._requests.clear();
|
|
330
|
-
}
|
|
331
|
-
_onClose() {
|
|
332
|
-
this._clearCollectedArtifacts();
|
|
333
|
-
this._onPageClose(this);
|
|
334
|
-
}
|
|
335
|
-
async navigate(url) {
|
|
336
|
-
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
337
|
-
// Cap load event to 5 seconds, the page is operational at this point.
|
|
338
|
-
await this.page.waitForLoadState('load', { timeout: 5000 }).catch(() => { });
|
|
339
|
-
}
|
|
340
|
-
hasSnapshot() {
|
|
341
|
-
return !!this._snapshot;
|
|
342
|
-
}
|
|
343
|
-
snapshotOrDie() {
|
|
344
|
-
if (!this._snapshot)
|
|
345
|
-
throw new Error('No snapshot available');
|
|
346
|
-
return this._snapshot;
|
|
347
|
-
}
|
|
348
|
-
console() {
|
|
349
|
-
return this._console;
|
|
350
|
-
}
|
|
351
|
-
requests() {
|
|
352
|
-
return this._requests;
|
|
353
|
-
}
|
|
354
|
-
async captureSnapshot() {
|
|
355
|
-
this._snapshot = await PageSnapshot.create(this.page);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
exports.Tab = Tab;
|
|
359
|
-
class PageSnapshot {
|
|
360
|
-
_frameLocators = [];
|
|
361
|
-
_text;
|
|
362
|
-
constructor() {
|
|
363
|
-
}
|
|
364
|
-
static async create(page) {
|
|
365
|
-
const snapshot = new PageSnapshot();
|
|
366
|
-
await snapshot._build(page);
|
|
367
|
-
return snapshot;
|
|
368
|
-
}
|
|
369
|
-
text() {
|
|
370
|
-
return this._text;
|
|
371
|
-
}
|
|
372
|
-
async _build(page) {
|
|
373
|
-
const yamlDocument = await this._snapshotFrame(page);
|
|
374
|
-
this._text = [
|
|
375
|
-
`- Page Snapshot`,
|
|
376
|
-
'```yaml',
|
|
377
|
-
yamlDocument.toString({ indentSeq: false }).trim(),
|
|
378
|
-
'```',
|
|
379
|
-
].join('\n');
|
|
380
|
-
}
|
|
381
|
-
async _snapshotFrame(frame) {
|
|
382
|
-
const frameIndex = this._frameLocators.push(frame) - 1;
|
|
383
|
-
const snapshotString = await frame.locator('body').ariaSnapshot({ ref: true, emitGeneric: true });
|
|
384
|
-
const snapshot = yaml_1.default.parseDocument(snapshotString);
|
|
385
|
-
const visit = async (node) => {
|
|
386
|
-
if (yaml_1.default.isPair(node)) {
|
|
387
|
-
await Promise.all([
|
|
388
|
-
visit(node.key).then(k => node.key = k),
|
|
389
|
-
visit(node.value).then(v => node.value = v)
|
|
390
|
-
]);
|
|
391
|
-
}
|
|
392
|
-
else if (yaml_1.default.isSeq(node) || yaml_1.default.isMap(node)) {
|
|
393
|
-
node.items = await Promise.all(node.items.map(visit));
|
|
394
|
-
}
|
|
395
|
-
else if (yaml_1.default.isScalar(node)) {
|
|
396
|
-
if (typeof node.value === 'string') {
|
|
397
|
-
const value = node.value;
|
|
398
|
-
if (frameIndex > 0)
|
|
399
|
-
node.value = value.replace('[ref=', `[ref=f${frameIndex}`);
|
|
400
|
-
if (value.startsWith('iframe ')) {
|
|
401
|
-
const ref = value.match(/\[ref=(.*)\]/)?.[1];
|
|
402
|
-
if (ref) {
|
|
403
|
-
try {
|
|
404
|
-
const childSnapshot = await this._snapshotFrame(frame.frameLocator(`aria-ref=${ref}`));
|
|
405
|
-
return snapshot.createPair(node.value, childSnapshot);
|
|
406
|
-
}
|
|
407
|
-
catch (error) {
|
|
408
|
-
return snapshot.createPair(node.value, '<could not take iframe snapshot>');
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return node;
|
|
415
|
-
};
|
|
416
|
-
await visit(snapshot.contents);
|
|
417
|
-
return snapshot;
|
|
418
|
-
}
|
|
419
|
-
refLocator(ref) {
|
|
420
|
-
let frame = this._frameLocators[0];
|
|
421
|
-
const match = ref.match(/^f(\d+)(.*)/);
|
|
422
|
-
if (match) {
|
|
423
|
-
const frameIndex = parseInt(match[1], 10);
|
|
424
|
-
frame = this._frameLocators[frameIndex];
|
|
425
|
-
ref = match[2];
|
|
426
|
-
}
|
|
427
|
-
if (!frame)
|
|
428
|
-
throw new Error(`Frame does not exist. Provide ref from the most current snapshot.`);
|
|
429
|
-
return frame.locator(`aria-ref=${ref}`);
|
|
292
|
+
async function launchPersistentContext(browserConfig) {
|
|
293
|
+
try {
|
|
294
|
+
const browserType = browserConfig?.browserName ? playwright[browserConfig.browserName] : playwright.chromium;
|
|
295
|
+
return await browserType.launchPersistentContext(browserConfig?.userDataDir || '', { ...browserConfig?.launchOptions, ...browserConfig?.contextOptions });
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
if (error.message.includes('Executable doesn\'t exist'))
|
|
299
|
+
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
|
|
300
|
+
throw error;
|
|
430
301
|
}
|
|
431
302
|
}
|
|
432
303
|
async function generateLocator(locator) {
|
package/lib/index.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
|
exports.createServer = createServer;
|
|
22
|
-
const path_1 = __importDefault(require("path"));
|
|
23
|
-
const os_1 = __importDefault(require("os"));
|
|
24
|
-
const fs_1 = __importDefault(require("fs"));
|
|
25
22
|
const server_1 = require("./server");
|
|
26
23
|
const common_1 = __importDefault(require("./tools/common"));
|
|
27
24
|
const console_1 = __importDefault(require("./tools/console"));
|
|
@@ -62,64 +59,12 @@ const screenshotTools = [
|
|
|
62
59
|
...(0, tabs_1.default)(false),
|
|
63
60
|
];
|
|
64
61
|
const packageJSON = require('../package.json');
|
|
65
|
-
async function createServer(
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
};
|
|
99
|
-
const allTools = options?.vision ? screenshotTools : snapshotTools;
|
|
100
|
-
const tools = allTools.filter(tool => !options?.capabilities || tool.capability === 'core' || options.capabilities.includes(tool.capability));
|
|
62
|
+
async function createServer(config = {}) {
|
|
63
|
+
const allTools = config.vision ? screenshotTools : snapshotTools;
|
|
64
|
+
const tools = allTools.filter(tool => !config.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability));
|
|
101
65
|
return (0, server_1.createServerWithTools)({
|
|
102
66
|
name: 'Playwright',
|
|
103
67
|
version: packageJSON.version,
|
|
104
68
|
tools,
|
|
105
|
-
|
|
106
|
-
browserName,
|
|
107
|
-
userDataDir,
|
|
108
|
-
launchOptions,
|
|
109
|
-
cdpEndpoint: options?.cdpEndpoint,
|
|
110
|
-
});
|
|
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;
|
|
69
|
+
}, config);
|
|
125
70
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.PageSnapshot = void 0;
|
|
22
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
23
|
+
class PageSnapshot {
|
|
24
|
+
_frameLocators = [];
|
|
25
|
+
_text;
|
|
26
|
+
constructor() {
|
|
27
|
+
}
|
|
28
|
+
static async create(page) {
|
|
29
|
+
const snapshot = new PageSnapshot();
|
|
30
|
+
await snapshot._build(page);
|
|
31
|
+
return snapshot;
|
|
32
|
+
}
|
|
33
|
+
text() {
|
|
34
|
+
return this._text;
|
|
35
|
+
}
|
|
36
|
+
async _build(page) {
|
|
37
|
+
const yamlDocument = await this._snapshotFrame(page);
|
|
38
|
+
this._text = [
|
|
39
|
+
`- Page Snapshot`,
|
|
40
|
+
'```yaml',
|
|
41
|
+
yamlDocument.toString({ indentSeq: false }).trim(),
|
|
42
|
+
'```',
|
|
43
|
+
].join('\n');
|
|
44
|
+
}
|
|
45
|
+
async _snapshotFrame(frame) {
|
|
46
|
+
const frameIndex = this._frameLocators.push(frame) - 1;
|
|
47
|
+
const snapshotString = await frame.locator('body').ariaSnapshot({ ref: true, emitGeneric: true });
|
|
48
|
+
const snapshot = yaml_1.default.parseDocument(snapshotString);
|
|
49
|
+
const visit = async (node) => {
|
|
50
|
+
if (yaml_1.default.isPair(node)) {
|
|
51
|
+
await Promise.all([
|
|
52
|
+
visit(node.key).then(k => node.key = k),
|
|
53
|
+
visit(node.value).then(v => node.value = v)
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
else if (yaml_1.default.isSeq(node) || yaml_1.default.isMap(node)) {
|
|
57
|
+
node.items = await Promise.all(node.items.map(visit));
|
|
58
|
+
}
|
|
59
|
+
else if (yaml_1.default.isScalar(node)) {
|
|
60
|
+
if (typeof node.value === 'string') {
|
|
61
|
+
const value = node.value;
|
|
62
|
+
if (frameIndex > 0)
|
|
63
|
+
node.value = value.replace('[ref=', `[ref=f${frameIndex}`);
|
|
64
|
+
if (value.startsWith('iframe ')) {
|
|
65
|
+
const ref = value.match(/\[ref=(.*)\]/)?.[1];
|
|
66
|
+
if (ref) {
|
|
67
|
+
try {
|
|
68
|
+
const childSnapshot = await this._snapshotFrame(frame.frameLocator(`aria-ref=${ref}`));
|
|
69
|
+
return snapshot.createPair(node.value, childSnapshot);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return snapshot.createPair(node.value, '<could not take iframe snapshot>');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return node;
|
|
79
|
+
};
|
|
80
|
+
await visit(snapshot.contents);
|
|
81
|
+
return snapshot;
|
|
82
|
+
}
|
|
83
|
+
refLocator(ref) {
|
|
84
|
+
let frame = this._frameLocators[0];
|
|
85
|
+
const match = ref.match(/^f(\d+)(.*)/);
|
|
86
|
+
if (match) {
|
|
87
|
+
const frameIndex = parseInt(match[1], 10);
|
|
88
|
+
frame = this._frameLocators[frameIndex];
|
|
89
|
+
ref = match[2];
|
|
90
|
+
}
|
|
91
|
+
if (!frame)
|
|
92
|
+
throw new Error(`Frame does not exist. Provide ref from the most current snapshot.`);
|
|
93
|
+
return frame.locator(`aria-ref=${ref}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.PageSnapshot = PageSnapshot;
|
package/lib/program.js
CHANGED
|
@@ -14,17 +14,12 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
-
};
|
|
20
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
const http_1 = __importDefault(require("http"));
|
|
22
18
|
const commander_1 = require("commander");
|
|
23
|
-
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
24
|
-
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
25
19
|
const index_1 = require("./index");
|
|
26
20
|
const server_1 = require("./server");
|
|
27
|
-
const
|
|
21
|
+
const transport_1 = require("./transport");
|
|
22
|
+
const config_1 = require("./config");
|
|
28
23
|
const packageJSON = require('../package.json');
|
|
29
24
|
commander_1.program
|
|
30
25
|
.version('Version ' + packageJSON.version)
|
|
@@ -34,27 +29,20 @@ commander_1.program
|
|
|
34
29
|
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
|
|
35
30
|
.option('--executable-path <path>', 'Path to the browser executable.')
|
|
36
31
|
.option('--headless', 'Run browser in headless mode, headed by default')
|
|
37
|
-
.option('--
|
|
32
|
+
.option('--device <device>', 'Device to emulate, for example: "iPhone 15"')
|
|
38
33
|
.option('--user-data-dir <path>', 'Path to the user data directory')
|
|
34
|
+
.option('--port <port>', 'Port to listen on for SSE transport.')
|
|
35
|
+
.option('--host <host>', 'Host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
|
|
39
36
|
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
|
|
37
|
+
.option('--config <path>', 'Path to the configuration file.')
|
|
40
38
|
.action(async (options) => {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
userDataDir: options.userDataDir,
|
|
44
|
-
headless: options.headless,
|
|
45
|
-
executablePath: options.executablePath,
|
|
46
|
-
vision: !!options.vision,
|
|
47
|
-
cdpEndpoint: options.cdpEndpoint,
|
|
48
|
-
capabilities: options.caps?.split(',').map((c) => c.trim()),
|
|
49
|
-
}));
|
|
39
|
+
const config = await (0, config_1.resolveConfig)(options);
|
|
40
|
+
const serverList = new server_1.ServerList(() => (0, index_1.createServer)(config));
|
|
50
41
|
setupExitWatchdog(serverList);
|
|
51
|
-
if (options.port)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const server = await serverList.create();
|
|
56
|
-
await server.connect(new stdio_js_1.StdioServerTransport());
|
|
57
|
-
}
|
|
42
|
+
if (options.port)
|
|
43
|
+
(0, transport_1.startHttpTransport)(+options.port, options.host, serverList);
|
|
44
|
+
else
|
|
45
|
+
await (0, transport_1.startStdioTransport)(serverList);
|
|
58
46
|
});
|
|
59
47
|
function setupExitWatchdog(serverList) {
|
|
60
48
|
const handleExit = async () => {
|
|
@@ -67,64 +55,3 @@ function setupExitWatchdog(serverList) {
|
|
|
67
55
|
process.on('SIGTERM', handleExit);
|
|
68
56
|
}
|
|
69
57
|
commander_1.program.parse(process.argv);
|
|
70
|
-
async function startSSEServer(port, serverList) {
|
|
71
|
-
const sessions = new Map();
|
|
72
|
-
const httpServer = http_1.default.createServer(async (req, res) => {
|
|
73
|
-
if (req.method === 'POST') {
|
|
74
|
-
const searchParams = new URL(`http://localhost${req.url}`).searchParams;
|
|
75
|
-
const sessionId = searchParams.get('sessionId');
|
|
76
|
-
if (!sessionId) {
|
|
77
|
-
res.statusCode = 400;
|
|
78
|
-
res.end('Missing sessionId');
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const transport = sessions.get(sessionId);
|
|
82
|
-
if (!transport) {
|
|
83
|
-
res.statusCode = 404;
|
|
84
|
-
res.end('Session not found');
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
await transport.handlePostMessage(req, res);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
else if (req.method === 'GET') {
|
|
91
|
-
const transport = new sse_js_1.SSEServerTransport('/sse', res);
|
|
92
|
-
sessions.set(transport.sessionId, transport);
|
|
93
|
-
const server = await serverList.create();
|
|
94
|
-
res.on('close', () => {
|
|
95
|
-
sessions.delete(transport.sessionId);
|
|
96
|
-
serverList.close(server).catch(e => console.error(e));
|
|
97
|
-
});
|
|
98
|
-
await server.connect(transport);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
res.statusCode = 405;
|
|
103
|
-
res.end('Method not allowed');
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
httpServer.listen(port, () => {
|
|
107
|
-
const address = httpServer.address();
|
|
108
|
-
(0, assert_1.default)(address, 'Could not bind server socket');
|
|
109
|
-
let url;
|
|
110
|
-
if (typeof address === 'string') {
|
|
111
|
-
url = address;
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
const resolvedPort = address.port;
|
|
115
|
-
let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
|
|
116
|
-
if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]')
|
|
117
|
-
resolvedHost = 'localhost';
|
|
118
|
-
url = `http://${resolvedHost}:${resolvedPort}`;
|
|
119
|
-
}
|
|
120
|
-
console.log(`Listening on ${url}`);
|
|
121
|
-
console.log('Put this in your client config:');
|
|
122
|
-
console.log(JSON.stringify({
|
|
123
|
-
'mcpServers': {
|
|
124
|
-
'playwright': {
|
|
125
|
-
'url': `${url}/sse`
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}, undefined, 2));
|
|
129
|
-
});
|
|
130
|
-
}
|
package/lib/server.js
CHANGED
|
@@ -21,13 +21,12 @@ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
|
21
21
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
22
22
|
const zod_to_json_schema_1 = require("zod-to-json-schema");
|
|
23
23
|
const context_1 = require("./context");
|
|
24
|
-
function createServerWithTools(
|
|
25
|
-
const { name, version, tools
|
|
26
|
-
const context = new context_1.Context(tools,
|
|
24
|
+
function createServerWithTools(serverOptions, config) {
|
|
25
|
+
const { name, version, tools } = serverOptions;
|
|
26
|
+
const context = new context_1.Context(tools, config);
|
|
27
27
|
const server = new index_js_1.Server({ name, version }, {
|
|
28
28
|
capabilities: {
|
|
29
29
|
tools: {},
|
|
30
|
-
resources: {},
|
|
31
30
|
}
|
|
32
31
|
});
|
|
33
32
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
@@ -39,46 +38,26 @@ function createServerWithTools(options) {
|
|
|
39
38
|
})),
|
|
40
39
|
};
|
|
41
40
|
});
|
|
42
|
-
server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
|
|
43
|
-
return { resources: resources.map(resource => resource.schema) };
|
|
44
|
-
});
|
|
45
41
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
42
|
+
const errorResult = (...messages) => ({
|
|
43
|
+
content: [{ type: 'text', text: messages.join('\n') }],
|
|
44
|
+
isError: true,
|
|
45
|
+
});
|
|
46
46
|
const tool = tools.find(tool => tool.schema.name === request.params.name);
|
|
47
|
-
if (!tool)
|
|
48
|
-
return {
|
|
49
|
-
content: [{ type: 'text', text: `Tool "${request.params.name}" not found` }],
|
|
50
|
-
isError: true,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
47
|
+
if (!tool)
|
|
48
|
+
return errorResult(`Tool "${request.params.name}" not found`);
|
|
53
49
|
const modalStates = context.modalStates().map(state => state.type);
|
|
54
|
-
if (
|
|
55
|
-
(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
...context.modalStatesMarkdown(),
|
|
59
|
-
].join('\n');
|
|
60
|
-
return {
|
|
61
|
-
content: [{ type: 'text', text }],
|
|
62
|
-
isError: true,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
50
|
+
if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState))
|
|
51
|
+
return errorResult(`The tool "${request.params.name}" can only be used when there is related modal state present.`, ...context.modalStatesMarkdown());
|
|
52
|
+
if (!tool.clearsModalState && modalStates.length)
|
|
53
|
+
return errorResult(`Tool "${request.params.name}" does not handle the modal state.`, ...context.modalStatesMarkdown());
|
|
65
54
|
try {
|
|
66
55
|
return await context.run(tool, request.params.arguments);
|
|
67
56
|
}
|
|
68
57
|
catch (error) {
|
|
69
|
-
return
|
|
70
|
-
content: [{ type: 'text', text: String(error) }],
|
|
71
|
-
isError: true,
|
|
72
|
-
};
|
|
58
|
+
return errorResult(String(error));
|
|
73
59
|
}
|
|
74
60
|
});
|
|
75
|
-
server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) => {
|
|
76
|
-
const resource = resources.find(resource => resource.schema.uri === request.params.uri);
|
|
77
|
-
if (!resource)
|
|
78
|
-
return { contents: [] };
|
|
79
|
-
const contents = await resource.read(context, request.params.uri);
|
|
80
|
-
return { contents };
|
|
81
|
-
});
|
|
82
61
|
const oldClose = server.close.bind(server);
|
|
83
62
|
server.close = async () => {
|
|
84
63
|
await oldClose();
|