@playwright/mcp 0.0.6 → 0.0.9

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 CHANGED
@@ -59,9 +59,25 @@ code-insiders --add-mcp '{"name":"playwright","command":"npx","args":["@playwrig
59
59
 
60
60
  After installation, the Playwright MCP server will be available for use with your GitHub Copilot agent in VS Code.
61
61
 
62
+ ### CLI Options
63
+
64
+ The Playwright MCP server supports the following command-line options:
65
+
66
+ - `--browser <browser>`: Browser or chrome channel to use. Possible values:
67
+ - `chrome`, `firefox`, `webkit`, `msedge`
68
+ - Chrome channels: `chrome-beta`, `chrome-canary`, `chrome-dev`
69
+ - Edge channels: `msedge-beta`, `msedge-canary`, `msedge-dev`
70
+ - Default: `chrome`
71
+ - `--cdp-endpoint <endpoint>`: CDP endpoint to connect to
72
+ - `--executable-path <path>`: Path to the browser executable
73
+ - `--headless`: Run browser in headless mode (headed by default)
74
+ - `--port <port>`: Port to listen on for SSE transport
75
+ - `--user-data-dir <path>`: Path to the user data directory
76
+ - `--vision`: Run server that uses screenshots (Aria snapshots are used by default)
77
+
62
78
  ### User data directory
63
79
 
64
- Playwright MCP will launch Chrome browser with the new profile, located at
80
+ Playwright MCP will launch the browser with the new profile, located at
65
81
 
66
82
  ```
67
83
  - `%USERPROFILE%\AppData\Local\ms-playwright\mcp-chrome-profile` on Windows
@@ -69,7 +85,7 @@ Playwright MCP will launch Chrome browser with the new profile, located at
69
85
  - `~/.cache/ms-playwright/mcp-chrome-profile` on Linux
70
86
  ```
71
87
 
72
- All the logged in information will be stored in that profile, you can delete it between sessions if you'dlike to clear the offline state.
88
+ All the logged in information will be stored in that profile, you can delete it between sessions if you'd like to clear the offline state.
73
89
 
74
90
 
75
91
  ### Running headless browser (Browser without GUI).
package/lib/context.js CHANGED
@@ -47,21 +47,25 @@ 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
+ };
50
53
  Object.defineProperty(exports, "__esModule", { value: true });
51
54
  exports.Context = void 0;
55
+ const child_process_1 = require("child_process");
56
+ const path_1 = __importDefault(require("path"));
52
57
  const playwright = __importStar(require("playwright"));
58
+ const yaml_1 = __importDefault(require("yaml"));
53
59
  class Context {
54
- _userDataDir;
55
- _launchOptions;
60
+ _options;
56
61
  _browser;
57
62
  _page;
58
63
  _console = [];
59
64
  _createPagePromise;
60
65
  _fileChooser;
61
66
  _lastSnapshotFrames = [];
62
- constructor(userDataDir, launchOptions) {
63
- this._userDataDir = userDataDir;
64
- this._launchOptions = launchOptions;
67
+ constructor(options) {
68
+ this._options = options;
65
69
  }
66
70
  async createPage() {
67
71
  if (this._createPagePromise)
@@ -93,6 +97,24 @@ class Context {
93
97
  this._fileChooser = undefined;
94
98
  this._console.length = 0;
95
99
  }
100
+ async install() {
101
+ const channel = this._options.launchOptions?.channel ?? this._options.browserName ?? 'chrome';
102
+ const cli = path_1.default.join(require.resolve('playwright/package.json'), '..', 'cli.js');
103
+ const child = (0, child_process_1.fork)(cli, ['install', channel], {
104
+ stdio: 'pipe',
105
+ });
106
+ const output = [];
107
+ child.stdout?.on('data', data => output.push(data.toString()));
108
+ child.stderr?.on('data', data => output.push(data.toString()));
109
+ return new Promise((resolve, reject) => {
110
+ child.on('close', code => {
111
+ if (code === 0)
112
+ resolve(channel);
113
+ else
114
+ reject(new Error(`Failed to install browser: ${output.join('')}`));
115
+ });
116
+ });
117
+ }
96
118
  existingPage() {
97
119
  if (!this._page)
98
120
  throw new Error('Navigate to a location to create a page');
@@ -119,49 +141,92 @@ class Context {
119
141
  this._fileChooser = undefined;
120
142
  }
121
143
  async _createPage() {
122
- if (process.env.PLAYWRIGHT_WS_ENDPOINT) {
123
- const url = new URL(process.env.PLAYWRIGHT_WS_ENDPOINT);
124
- if (this._launchOptions)
125
- url.searchParams.set('launch-options', JSON.stringify(this._launchOptions));
126
- const browser = await playwright.chromium.connect(String(url));
144
+ if (this._options.remoteEndpoint) {
145
+ const url = new URL(this._options.remoteEndpoint);
146
+ if (this._options.browserName)
147
+ url.searchParams.set('browser', this._options.browserName);
148
+ if (this._options.launchOptions)
149
+ url.searchParams.set('launch-options', JSON.stringify(this._options.launchOptions));
150
+ const browser = await playwright[this._options.browserName ?? 'chromium'].connect(String(url));
127
151
  const page = await browser.newPage();
128
152
  return { browser, page };
129
153
  }
130
- const context = await playwright.chromium.launchPersistentContext(this._userDataDir, this._launchOptions);
154
+ if (this._options.cdpEndpoint) {
155
+ const browser = await playwright.chromium.connectOverCDP(this._options.cdpEndpoint);
156
+ const browserContext = browser.contexts()[0];
157
+ let [page] = browserContext.pages();
158
+ if (!page)
159
+ page = await browserContext.newPage();
160
+ return { browser, page };
161
+ }
162
+ const context = await this._launchPersistentContext();
131
163
  const [page] = context.pages();
132
164
  return { page };
133
165
  }
166
+ async _launchPersistentContext() {
167
+ try {
168
+ const browserType = this._options.browserName ? playwright[this._options.browserName] : playwright.chromium;
169
+ return await browserType.launchPersistentContext(this._options.userDataDir, this._options.launchOptions);
170
+ }
171
+ catch (error) {
172
+ if (error.message.includes('Executable doesn\'t exist'))
173
+ throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
174
+ throw error;
175
+ }
176
+ }
134
177
  async allFramesSnapshot() {
135
- const page = this.existingPage();
136
- const visibleFrames = await page.locator('iframe').filter({ visible: true }).all();
137
- this._lastSnapshotFrames = visibleFrames.map(frame => frame.contentFrame());
138
- const snapshots = await Promise.all([
139
- page.locator('html').ariaSnapshot({ ref: true }),
140
- ...this._lastSnapshotFrames.map(async (frame, index) => {
141
- const snapshot = await frame.locator('html').ariaSnapshot({ ref: true });
142
- const args = [];
143
- const src = await frame.owner().getAttribute('src');
144
- if (src)
145
- args.push(`src=${src}`);
146
- const name = await frame.owner().getAttribute('name');
147
- if (name)
148
- args.push(`name=${name}`);
149
- return `\n# iframe ${args.join(' ')}\n` + snapshot.replaceAll('[ref=', `[ref=f${index}`);
150
- })
151
- ]);
152
- return snapshots.join('\n');
178
+ this._lastSnapshotFrames = [];
179
+ const yaml = await this._allFramesSnapshot(this.existingPage());
180
+ return yaml.toString().trim();
181
+ }
182
+ async _allFramesSnapshot(frame) {
183
+ const frameIndex = this._lastSnapshotFrames.push(frame) - 1;
184
+ const snapshotString = await frame.locator('body').ariaSnapshot({ ref: true });
185
+ const snapshot = yaml_1.default.parseDocument(snapshotString);
186
+ const visit = async (node) => {
187
+ if (yaml_1.default.isPair(node)) {
188
+ await Promise.all([
189
+ visit(node.key).then(k => node.key = k),
190
+ visit(node.value).then(v => node.value = v)
191
+ ]);
192
+ }
193
+ else if (yaml_1.default.isSeq(node) || yaml_1.default.isMap(node)) {
194
+ node.items = await Promise.all(node.items.map(visit));
195
+ }
196
+ else if (yaml_1.default.isScalar(node)) {
197
+ if (typeof node.value === 'string') {
198
+ const value = node.value;
199
+ if (frameIndex > 0)
200
+ node.value = value.replace('[ref=', `[ref=f${frameIndex}`);
201
+ if (value.startsWith('iframe ')) {
202
+ const ref = value.match(/\[ref=(.*)\]/)?.[1];
203
+ if (ref) {
204
+ try {
205
+ const childSnapshot = await this._allFramesSnapshot(frame.frameLocator(`aria-ref=${ref}`));
206
+ return snapshot.createPair(node.value, childSnapshot);
207
+ }
208
+ catch (error) {
209
+ return snapshot.createPair(node.value, '<could not take iframe snapshot>');
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+ return node;
216
+ };
217
+ await visit(snapshot.contents);
218
+ return snapshot;
153
219
  }
154
220
  refLocator(ref) {
155
- const page = this.existingPage();
156
- let frame = page.mainFrame();
221
+ let frame = this._lastSnapshotFrames[0];
157
222
  const match = ref.match(/^f(\d+)(.*)/);
158
223
  if (match) {
159
224
  const frameIndex = parseInt(match[1], 10);
160
- if (!this._lastSnapshotFrames[frameIndex])
161
- throw new Error(`Frame does not exist. Provide ref from the most current snapshot.`);
162
225
  frame = this._lastSnapshotFrames[frameIndex];
163
226
  ref = match[2];
164
227
  }
228
+ if (!frame)
229
+ throw new Error(`Frame does not exist. Provide ref from the most current snapshot.`);
165
230
  return frame.locator(`aria-ref=${ref}`);
166
231
  }
167
232
  }
package/lib/index.js CHANGED
@@ -59,6 +59,7 @@ const commonTools = [
59
59
  common.wait,
60
60
  common.pdf,
61
61
  common.close,
62
+ common.install,
62
63
  ];
63
64
  const snapshotTools = [
64
65
  common.navigate(true),
@@ -96,7 +97,9 @@ function createServer(options) {
96
97
  version: packageJSON.version,
97
98
  tools,
98
99
  resources,
100
+ browserName: options?.browserName,
99
101
  userDataDir: options?.userDataDir ?? '',
100
102
  launchOptions: options?.launchOptions,
103
+ cdpEndpoint: options?.cdpEndpoint,
101
104
  });
102
105
  }
package/lib/program.js CHANGED
@@ -26,99 +26,78 @@ const commander_1 = require("commander");
26
26
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
27
27
  const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
28
28
  const index_1 = require("./index");
29
+ const server_1 = require("./server");
29
30
  const assert_1 = __importDefault(require("assert"));
30
31
  const packageJSON = require('../package.json');
31
32
  commander_1.program
32
33
  .version('Version ' + packageJSON.version)
33
34
  .name(packageJSON.name)
35
+ .option('--browser <browser>', 'Browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
36
+ .option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
37
+ .option('--executable-path <path>', 'Path to the browser executable.')
34
38
  .option('--headless', 'Run browser in headless mode, headed by default')
39
+ .option('--port <port>', 'Port to listen on for SSE transport.')
35
40
  .option('--user-data-dir <path>', 'Path to the user data directory')
36
41
  .option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
37
- .option('--port <port>', 'Port to listen on for SSE transport.')
38
42
  .action(async (options) => {
43
+ let browserName;
44
+ let channel;
45
+ switch (options.browser) {
46
+ case 'chrome':
47
+ case 'chrome-beta':
48
+ case 'chrome-canary':
49
+ case 'chrome-dev':
50
+ case 'msedge':
51
+ case 'msedge-beta':
52
+ case 'msedge-canary':
53
+ case 'msedge-dev':
54
+ browserName = 'chromium';
55
+ channel = options.browser;
56
+ break;
57
+ case 'chromium':
58
+ browserName = 'chromium';
59
+ break;
60
+ case 'firefox':
61
+ browserName = 'firefox';
62
+ break;
63
+ case 'webkit':
64
+ browserName = 'webkit';
65
+ break;
66
+ default:
67
+ browserName = 'chromium';
68
+ channel = 'chrome';
69
+ }
39
70
  const launchOptions = {
40
71
  headless: !!options.headless,
41
- channel: 'chrome',
72
+ channel,
73
+ executablePath: options.executablePath,
42
74
  };
43
- const server = (0, index_1.createServer)({
44
- userDataDir: options.userDataDir ?? await userDataDir(),
75
+ const userDataDir = options.userDataDir ?? await createUserDataDir(browserName);
76
+ const serverList = new server_1.ServerList(() => (0, index_1.createServer)({
77
+ browserName,
78
+ userDataDir,
45
79
  launchOptions,
46
80
  vision: !!options.vision,
47
- });
48
- setupExitWatchdog(server);
81
+ cdpEndpoint: options.cdpEndpoint,
82
+ }));
83
+ setupExitWatchdog(serverList);
49
84
  if (options.port) {
50
- const sessions = new Map();
51
- const httpServer = http_1.default.createServer(async (req, res) => {
52
- if (req.method === 'POST') {
53
- const host = req.headers.host ?? 'http://unknown';
54
- const sessionId = new URL(host + req.url).searchParams.get('sessionId');
55
- if (!sessionId) {
56
- res.statusCode = 400;
57
- res.end('Missing sessionId');
58
- return;
59
- }
60
- const transport = sessions.get(sessionId);
61
- if (!transport) {
62
- res.statusCode = 404;
63
- res.end('Session not found');
64
- return;
65
- }
66
- await transport.handlePostMessage(req, res);
67
- return;
68
- }
69
- else if (req.method === 'GET') {
70
- const transport = new sse_js_1.SSEServerTransport('/sse', res);
71
- sessions.set(transport.sessionId, transport);
72
- res.on('close', () => {
73
- sessions.delete(transport.sessionId);
74
- });
75
- await server.connect(transport);
76
- return;
77
- }
78
- else {
79
- res.statusCode = 405;
80
- res.end('Method not allowed');
81
- }
82
- });
83
- httpServer.listen(+options.port, () => {
84
- const address = httpServer.address();
85
- (0, assert_1.default)(address, 'Could not bind server socket');
86
- let urlPrefixHumanReadable;
87
- if (typeof address === 'string') {
88
- urlPrefixHumanReadable = address;
89
- }
90
- else {
91
- const port = address.port;
92
- let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
93
- if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]')
94
- resolvedHost = 'localhost';
95
- urlPrefixHumanReadable = `http://${resolvedHost}:${port}`;
96
- }
97
- console.log(`Listening on ${urlPrefixHumanReadable}`);
98
- console.log('Put this in your client config:');
99
- console.log(JSON.stringify({
100
- 'mcpServers': {
101
- 'playwright': {
102
- 'url': `${urlPrefixHumanReadable}/sse`
103
- }
104
- }
105
- }, undefined, 2));
106
- });
85
+ startSSEServer(+options.port, serverList);
107
86
  }
108
87
  else {
109
- const transport = new stdio_js_1.StdioServerTransport();
110
- await server.connect(transport);
88
+ const server = await serverList.create();
89
+ await server.connect(new stdio_js_1.StdioServerTransport());
111
90
  }
112
91
  });
113
- function setupExitWatchdog(server) {
92
+ function setupExitWatchdog(serverList) {
114
93
  process.stdin.on('close', async () => {
115
94
  setTimeout(() => process.exit(0), 15000);
116
- await server.close();
95
+ await serverList.closeAll();
117
96
  process.exit(0);
118
97
  });
119
98
  }
120
99
  commander_1.program.parse(process.argv);
121
- async function userDataDir() {
100
+ async function createUserDataDir(browserName) {
122
101
  let cacheDirectory;
123
102
  if (process.platform === 'linux')
124
103
  cacheDirectory = process.env.XDG_CACHE_HOME || path_1.default.join(os_1.default.homedir(), '.cache');
@@ -128,7 +107,68 @@ async function userDataDir() {
128
107
  cacheDirectory = process.env.LOCALAPPDATA || path_1.default.join(os_1.default.homedir(), 'AppData', 'Local');
129
108
  else
130
109
  throw new Error('Unsupported platform: ' + process.platform);
131
- const result = path_1.default.join(cacheDirectory, 'ms-playwright', 'mcp-chrome-profile');
110
+ const result = path_1.default.join(cacheDirectory, 'ms-playwright', `mcp-${browserName}-profile`);
132
111
  await fs_1.default.promises.mkdir(result, { recursive: true });
133
112
  return result;
134
113
  }
114
+ async function startSSEServer(port, serverList) {
115
+ const sessions = new Map();
116
+ const httpServer = http_1.default.createServer(async (req, res) => {
117
+ if (req.method === 'POST') {
118
+ const searchParams = new URL(`http://localhost${req.url}`).searchParams;
119
+ const sessionId = searchParams.get('sessionId');
120
+ if (!sessionId) {
121
+ res.statusCode = 400;
122
+ res.end('Missing sessionId');
123
+ return;
124
+ }
125
+ const transport = sessions.get(sessionId);
126
+ if (!transport) {
127
+ res.statusCode = 404;
128
+ res.end('Session not found');
129
+ return;
130
+ }
131
+ await transport.handlePostMessage(req, res);
132
+ return;
133
+ }
134
+ else if (req.method === 'GET') {
135
+ const transport = new sse_js_1.SSEServerTransport('/sse', res);
136
+ sessions.set(transport.sessionId, transport);
137
+ const server = await serverList.create();
138
+ res.on('close', () => {
139
+ sessions.delete(transport.sessionId);
140
+ serverList.close(server).catch(e => console.error(e));
141
+ });
142
+ await server.connect(transport);
143
+ return;
144
+ }
145
+ else {
146
+ res.statusCode = 405;
147
+ res.end('Method not allowed');
148
+ }
149
+ });
150
+ httpServer.listen(port, () => {
151
+ const address = httpServer.address();
152
+ (0, assert_1.default)(address, 'Could not bind server socket');
153
+ let url;
154
+ if (typeof address === 'string') {
155
+ url = address;
156
+ }
157
+ else {
158
+ const resolvedPort = address.port;
159
+ let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
160
+ if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]')
161
+ resolvedHost = 'localhost';
162
+ url = `http://${resolvedHost}:${resolvedPort}`;
163
+ }
164
+ console.log(`Listening on ${url}`);
165
+ console.log('Put this in your client config:');
166
+ console.log(JSON.stringify({
167
+ 'mcpServers': {
168
+ 'playwright': {
169
+ 'url': `${url}/sse`
170
+ }
171
+ }
172
+ }, undefined, 2));
173
+ });
174
+ }
package/lib/server.js CHANGED
@@ -15,13 +15,14 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.ServerList = void 0;
18
19
  exports.createServerWithTools = createServerWithTools;
19
20
  const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
20
21
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
21
22
  const context_1 = require("./context");
22
23
  function createServerWithTools(options) {
23
- const { name, version, tools, resources, userDataDir, launchOptions } = options;
24
- const context = new context_1.Context(userDataDir, launchOptions);
24
+ const { name, version, tools, resources } = options;
25
+ const context = new context_1.Context(options);
25
26
  const server = new index_js_1.Server({ name, version }, {
26
27
  capabilities: {
27
28
  tools: {},
@@ -67,3 +68,25 @@ function createServerWithTools(options) {
67
68
  };
68
69
  return server;
69
70
  }
71
+ class ServerList {
72
+ _servers = [];
73
+ _serverFactory;
74
+ constructor(serverFactory) {
75
+ this._serverFactory = serverFactory;
76
+ }
77
+ async create() {
78
+ const server = this._serverFactory();
79
+ this._servers.push(server);
80
+ return server;
81
+ }
82
+ async close(server) {
83
+ const index = this._servers.indexOf(server);
84
+ if (index !== -1)
85
+ this._servers.splice(index, 1);
86
+ await server.close();
87
+ }
88
+ async closeAll() {
89
+ await Promise.all(this._servers.map(server => server.close()));
90
+ }
91
+ }
92
+ exports.ServerList = ServerList;
@@ -18,7 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
18
18
  return (mod && mod.__esModule) ? mod : { "default": mod };
19
19
  };
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.chooseFile = exports.close = exports.pdf = exports.pressKey = exports.wait = exports.goForward = exports.goBack = exports.navigate = void 0;
21
+ exports.install = exports.chooseFile = exports.close = exports.pdf = exports.pressKey = exports.wait = exports.goForward = exports.goBack = exports.navigate = void 0;
22
22
  const os_1 = __importDefault(require("os"));
23
23
  const path_1 = __importDefault(require("path"));
24
24
  const zod_1 = require("zod");
@@ -119,7 +119,7 @@ exports.pdf = {
119
119
  },
120
120
  handle: async (context) => {
121
121
  const page = context.existingPage();
122
- const fileName = path_1.default.join(os_1.default.tmpdir(), `/page-${new Date().toISOString()}.pdf`);
122
+ const fileName = path_1.default.join(os_1.default.tmpdir(), (0, utils_1.sanitizeForFilePath)(`page-${new Date().toISOString()}`)) + '.pdf';
123
123
  await page.pdf({ path: fileName });
124
124
  return {
125
125
  content: [{
@@ -163,3 +163,19 @@ const chooseFile = snapshot => ({
163
163
  },
164
164
  });
165
165
  exports.chooseFile = chooseFile;
166
+ exports.install = {
167
+ schema: {
168
+ name: 'browser_install',
169
+ description: 'Install the browser specified in the config. Call this if you get an error about the browser not being installed.',
170
+ inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({})),
171
+ },
172
+ handle: async (context) => {
173
+ const channel = await context.install();
174
+ return {
175
+ content: [{
176
+ type: 'text',
177
+ text: `Browser ${channel} installed`,
178
+ }],
179
+ };
180
+ },
181
+ };
@@ -17,6 +17,7 @@
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.runAndWait = runAndWait;
19
19
  exports.captureAriaSnapshot = captureAriaSnapshot;
20
+ exports.sanitizeForFilePath = sanitizeForFilePath;
20
21
  async function waitForCompletion(page, callback) {
21
22
  const requests = new Set();
22
23
  let frameNavigated = false;
@@ -88,3 +89,6 @@ async function captureAriaSnapshot(context, status = '') {
88
89
  content: [{ type: 'text', text: lines.join('\n') }],
89
90
  };
90
91
  }
92
+ function sanitizeForFilePath(s) {
93
+ return s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playwright/mcp",
3
- "version": "0.0.6",
3
+ "version": "0.0.9",
4
4
  "description": "Playwright Tools for MCP",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,7 +20,7 @@
20
20
  "watch": "tsc --watch",
21
21
  "test": "playwright test",
22
22
  "clean": "rm -rf lib",
23
- "publish": "npm run clean && npm run build && npm run test && npm publish"
23
+ "npm-publish": "npm run clean && npm run build && npm run test && npm publish"
24
24
  },
25
25
  "exports": {
26
26
  "./package.json": "./package.json",
@@ -32,18 +32,19 @@
32
32
  "dependencies": {
33
33
  "@modelcontextprotocol/sdk": "^1.6.1",
34
34
  "commander": "^13.1.0",
35
- "playwright": "1.52.0-alpha-1743011787000",
35
+ "playwright": "^1.52.0-alpha-1743163434000",
36
+ "yaml": "^2.7.1",
36
37
  "zod-to-json-schema": "^3.24.4"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@eslint/eslintrc": "^3.2.0",
40
41
  "@eslint/js": "^9.19.0",
41
- "@playwright/test": "1.52.0-alpha-1743011787000",
42
+ "@playwright/test": "^1.52.0-alpha-1743163434000",
42
43
  "@stylistic/eslint-plugin": "^3.0.1",
44
+ "@types/node": "^22.13.10",
43
45
  "@typescript-eslint/eslint-plugin": "^8.26.1",
44
46
  "@typescript-eslint/parser": "^8.26.1",
45
47
  "@typescript-eslint/utils": "^8.26.1",
46
- "@types/node": "^22.13.10",
47
48
  "eslint": "^9.19.0",
48
49
  "eslint-plugin-import": "^2.31.0",
49
50
  "eslint-plugin-notice": "^1.0.0",