From a21a17052b92b6a008d99fd7ba2114d157a55198 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 5 Mar 2026 19:52:14 +0000 Subject: [PATCH 1/3] chore(deps): bump hono to 4.12.5 and @hono/node-server to 1.19.11 (#39531) --- package-lock.json | 12 +- .../playwright-core/ThirdPartyNotices.txt | 382 ++---------------- .../bundles/mcp/package-lock.json | 12 +- 3 files changed, 35 insertions(+), 371 deletions(-) diff --git a/package-lock.json b/package-lock.json index 728681d41e995..bf0748c69484b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1000,9 +1000,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", "dev": true, "license": "MIT", "engines": { @@ -4883,9 +4883,9 @@ } }, "node_modules/hono": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.2.tgz", - "integrity": "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", + "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", "dev": true, "license": "MIT", "engines": { diff --git a/packages/playwright-core/ThirdPartyNotices.txt b/packages/playwright-core/ThirdPartyNotices.txt index cbe39c8e84405..339918d781ea2 100644 --- a/packages/playwright-core/ThirdPartyNotices.txt +++ b/packages/playwright-core/ThirdPartyNotices.txt @@ -4,7 +4,7 @@ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Microsoft received such components are set forth below. Microsoft reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. -- @hono/node-server@1.19.9 (https://github.com/honojs/node-server) +- @hono/node-server@1.19.11 (https://github.com/honojs/node-server) - @lowire/loop@0.0.25 (https://github.com/pavelfeldman/lowire) - @modelcontextprotocol/sdk@1.26.0 (https://github.com/modelcontextprotocol/typescript-sdk) - accepts@2.0.0 (https://github.com/jshttp/accepts) @@ -61,7 +61,7 @@ This project incorporates components from the projects listed below. The origina - graceful-fs@4.2.10 (https://github.com/isaacs/node-graceful-fs) - has-symbols@1.1.0 (https://github.com/inspect-js/has-symbols) - hasown@2.0.2 (https://github.com/inspect-js/hasOwn) -- hono@4.12.2 (https://github.com/honojs/hono) +- hono@4.12.5 (https://github.com/honojs/hono) - http-errors@2.0.1 (https://github.com/jshttp/http-errors) - https-proxy-agent@7.0.6 (https://github.com/TooTallNate/proxy-agents) - iconv-lite@0.7.2 (https://github.com/pillarjs/iconv-lite) @@ -139,367 +139,31 @@ This project incorporates components from the projects listed below. The origina - zod-to-json-schema@3.25.1 (https://github.com/StefanTerdell/zod-to-json-schema) - zod@4.3.5 (https://github.com/colinhacks/zod) -%% @hono/node-server@1.19.9 NOTICES AND INFORMATION BEGIN HERE +%% @hono/node-server@1.19.11 NOTICES AND INFORMATION BEGIN HERE ========================================= -# Node.js Adapter for Hono - -This adapter `@hono/node-server` allows you to run your Hono application on Node.js. -Initially, Hono wasn't designed for Node.js, but with this adapter, you can now use Hono on Node.js. -It utilizes web standard APIs implemented in Node.js version 18 or higher. - -## Benchmarks - -Hono is 3.5 times faster than Express. - -Express: - -```txt -$ bombardier -d 10s --fasthttp http://localhost:3000/ - -Statistics Avg Stdev Max - Reqs/sec 16438.94 1603.39 19155.47 - Latency 7.60ms 7.51ms 559.89ms - HTTP codes: - 1xx - 0, 2xx - 164494, 3xx - 0, 4xx - 0, 5xx - 0 - others - 0 - Throughput: 4.55MB/s -``` - -Hono + `@hono/node-server`: - -```txt -$ bombardier -d 10s --fasthttp http://localhost:3000/ - -Statistics Avg Stdev Max - Reqs/sec 58296.56 5512.74 74403.56 - Latency 2.14ms 1.46ms 190.92ms - HTTP codes: - 1xx - 0, 2xx - 583059, 3xx - 0, 4xx - 0, 5xx - 0 - others - 0 - Throughput: 12.56MB/s -``` - -## Requirements - -It works on Node.js versions greater than 18.x. The specific required Node.js versions are as follows: - -- 18.x => 18.14.1+ -- 19.x => 19.7.0+ -- 20.x => 20.0.0+ - -Essentially, you can simply use the latest version of each major release. - -## Installation - -You can install it from the npm registry with `npm` command: - -```sh -npm install @hono/node-server -``` - -Or use `yarn`: - -```sh -yarn add @hono/node-server -``` - -## Usage - -Just import `@hono/node-server` at the top and write the code as usual. -The same code that runs on Cloudflare Workers, Deno, and Bun will work. - -```ts -import { serve } from '@hono/node-server' -import { Hono } from 'hono' - -const app = new Hono() -app.get('/', (c) => c.text('Hono meets Node.js')) - -serve(app, (info) => { - console.log(`Listening on http://localhost:${info.port}`) // Listening on http://localhost:3000 -}) -``` - -For example, run it using `ts-node`. Then an HTTP server will be launched. The default port is `3000`. - -```sh -ts-node ./index.ts -``` - -Open `http://localhost:3000` with your browser. - -## Options - -### `port` - -```ts -serve({ - fetch: app.fetch, - port: 8787, // Port number, default is 3000 -}) -``` - -### `createServer` - -```ts -import { createServer } from 'node:https' -import fs from 'node:fs' - -//... - -serve({ - fetch: app.fetch, - createServer: createServer, - serverOptions: { - key: fs.readFileSync('test/fixtures/keys/agent1-key.pem'), - cert: fs.readFileSync('test/fixtures/keys/agent1-cert.pem'), - }, -}) -``` - -### `overrideGlobalObjects` - -The default value is `true`. The Node.js Adapter rewrites the global Request/Response and uses a lightweight Request/Response to improve performance. If you don't want to do that, set `false`. - -```ts -serve({ - fetch: app.fetch, - overrideGlobalObjects: false, -}) -``` - -### `autoCleanupIncoming` - -The default value is `true`. The Node.js Adapter automatically cleans up (explicitly call `destroy()` method) if application is not finished to consume the incoming request. If you don't want to do that, set `false`. - -If the application accepts connections from arbitrary clients, this cleanup must be done otherwise incomplete requests from clients may cause the application to stop responding. If your application only accepts connections from trusted clients, such as in a reverse proxy environment and there is no process that returns a response without reading the body of the POST request all the way through, you can improve performance by setting it to `false`. - -```ts -serve({ - fetch: app.fetch, - autoCleanupIncoming: false, -}) -``` - -## Middleware - -Most built-in middleware also works with Node.js. -Read [the documentation](https://hono.dev/middleware/builtin/basic-auth) and use the Middleware of your liking. - -```ts -import { serve } from '@hono/node-server' -import { Hono } from 'hono' -import { prettyJSON } from 'hono/pretty-json' - -const app = new Hono() - -app.get('*', prettyJSON()) -app.get('/', (c) => c.json({ 'Hono meets': 'Node.js' })) - -serve(app) -``` - -## Serve Static Middleware - -Use Serve Static Middleware that has been created for Node.js. - -```ts -import { serveStatic } from '@hono/node-server/serve-static' - -//... - -app.use('/static/*', serveStatic({ root: './' })) -``` - -If using a relative path, `root` will be relative to the current working directory from which the app was started. - -This can cause confusion when running your application locally. - -Imagine your project structure is: - -``` -my-hono-project/ - src/ - index.ts - static/ - index.html -``` - -Typically, you would run your app from the project's root directory (`my-hono-project`), -so you would need the following code to serve the `static` folder: - -```ts -app.use('/static/*', serveStatic({ root: './static' })) -``` - -Notice that `root` here is not relative to `src/index.ts`, rather to `my-hono-project`. - -### Options - -#### `rewriteRequestPath` - -If you want to serve files in `./.foojs` with the request path `/__foo/*`, you can write like the following. - -```ts -app.use( - '/__foo/*', - serveStatic({ - root: './.foojs/', - rewriteRequestPath: (path: string) => path.replace(/^\/__foo/, ''), - }) -) -``` - -#### `onFound` - -You can specify handling when the requested file is found with `onFound`. - -```ts -app.use( - '/static/*', - serveStatic({ - // ... - onFound: (_path, c) => { - c.header('Cache-Control', `public, immutable, max-age=31536000`) - }, - }) -) -``` - -#### `onNotFound` - -The `onNotFound` is useful for debugging. You can write a handle for when a file is not found. - -```ts -app.use( - '/static/*', - serveStatic({ - root: './non-existent-dir', - onNotFound: (path, c) => { - console.log(`${path} is not found, request to ${c.req.path}`) - }, - }) -) -``` - -#### `precompressed` - -The `precompressed` option checks if files with extensions like `.br` or `.gz` are available and serves them based on the `Accept-Encoding` header. It prioritizes Brotli, then Zstd, and Gzip. If none are available, it serves the original file. - -```ts -app.use( - '/static/*', - serveStatic({ - precompressed: true, - }) -) -``` - -## ConnInfo Helper - -You can use the [ConnInfo Helper](https://hono.dev/docs/helpers/conninfo) by importing `getConnInfo` from `@hono/node-server/conninfo`. - -```ts -import { getConnInfo } from '@hono/node-server/conninfo' - -app.get('/', (c) => { - const info = getConnInfo(c) // info is `ConnInfo` - return c.text(`Your remote address is ${info.remote.address}`) -}) -``` - -## Accessing Node.js API - -You can access the Node.js API from `c.env` in Node.js. For example, if you want to specify a type, you can write the following. - -```ts -import { serve } from '@hono/node-server' -import type { HttpBindings } from '@hono/node-server' -import { Hono } from 'hono' - -const app = new Hono<{ Bindings: HttpBindings }>() - -app.get('/', (c) => { - return c.json({ - remoteAddress: c.env.incoming.socket.remoteAddress, - }) -}) - -serve(app) -``` - -The APIs that you can get from `c.env` are as follows. - -```ts -type HttpBindings = { - incoming: IncomingMessage - outgoing: ServerResponse -} - -type Http2Bindings = { - incoming: Http2ServerRequest - outgoing: Http2ServerResponse -} -``` - -## Direct response from Node.js API - -You can directly respond to the client from the Node.js API. -In that case, the response from Hono should be ignored, so return `RESPONSE_ALREADY_SENT`. - -> [!NOTE] -> This feature can be used when migrating existing Node.js applications to Hono, but we recommend using Hono's API for new applications. - -```ts -import { serve } from '@hono/node-server' -import type { HttpBindings } from '@hono/node-server' -import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' -import { Hono } from 'hono' - -const app = new Hono<{ Bindings: HttpBindings }>() - -app.get('/', (c) => { - const { outgoing } = c.env - outgoing.writeHead(200, { 'Content-Type': 'text/plain' }) - outgoing.end('Hello World\n') - - return RESPONSE_ALREADY_SENT -}) - -serve(app) -``` - -## Listen to a UNIX domain socket - -You can configure the HTTP server to listen to a UNIX domain socket instead of a TCP port. - -```ts -import { createAdaptorServer } from '@hono/node-server' - -// ... - -const socketPath ='/tmp/example.sock' - -const server = createAdaptorServer(app) -server.listen(socketPath, () => { - console.log(`Listening on ${socketPath}`) -}) -``` - -## Related projects - -- Hono - -- Hono GitHub repository - +MIT License -## Author +Copyright (c) 2022 - present, Yusuke Wada and Hono contributors -Yusuke Wada +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -## License +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -MIT +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ========================================= -END OF @hono/node-server@1.19.9 AND INFORMATION +END OF @hono/node-server@1.19.11 AND INFORMATION %% @lowire/loop@0.0.25 NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -2146,7 +1810,7 @@ SOFTWARE. ========================================= END OF hasown@2.0.2 AND INFORMATION -%% hono@4.12.2 NOTICES AND INFORMATION BEGIN HERE +%% hono@4.12.5 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -2170,7 +1834,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF hono@4.12.2 AND INFORMATION +END OF hono@4.12.5 AND INFORMATION %% http-errors@2.0.1 NOTICES AND INFORMATION BEGIN HERE ========================================= diff --git a/packages/playwright-core/bundles/mcp/package-lock.json b/packages/playwright-core/bundles/mcp/package-lock.json index 97d02480d64dc..bf9708185256f 100644 --- a/packages/playwright-core/bundles/mcp/package-lock.json +++ b/packages/playwright-core/bundles/mcp/package-lock.json @@ -15,9 +15,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -576,9 +576,9 @@ } }, "node_modules/hono": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.2.tgz", - "integrity": "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", + "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", "license": "MIT", "engines": { "node": ">=16.9.0" From 6aaaa2c35d4201b01f0f665c097d23eeb0c1ec91 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 5 Mar 2026 13:23:39 -0800 Subject: [PATCH 2/3] chore: move session config to daemon (#39534) --- .../playwright-core/src/cli/client/program.ts | 93 ++++-------------- .../src/cli/client/registry.ts | 34 +++++-- .../playwright-core/src/cli/client/session.ts | 72 ++++---------- .../playwright-core/src/cli/daemon/DEPS.list | 1 + .../playwright-core/src/cli/daemon/daemon.ts | 89 +++++++++++------ .../playwright-core/src/cli/daemon/program.ts | 98 +++++++++++-------- .../playwright-core/src/mcp/browser/config.ts | 3 +- packages/playwright-core/src/mcp/exports.ts | 2 +- .../playwright-core/src/mcp/sdk/server.ts | 2 +- .../playwright/src/mcp/test/browserBackend.ts | 16 ++- tests/mcp/cli-isolated.spec.ts | 4 - tests/mcp/cli-session.spec.ts | 10 -- 12 files changed, 191 insertions(+), 233 deletions(-) diff --git a/packages/playwright-core/src/cli/client/program.ts b/packages/playwright-core/src/cli/client/program.ts index 5b10e82ae6a06..8ad2dc97b6d95 100644 --- a/packages/playwright-core/src/cli/client/program.ts +++ b/packages/playwright-core/src/cli/client/program.ts @@ -23,25 +23,17 @@ import crypto from 'crypto'; import fs from 'fs'; import os from 'os'; import path from 'path'; -import { createClientInfo, Registry } from './registry'; +import { createClientInfo, Registry, resolveSessionName } from './registry'; import { Session, renderResolvedConfig } from './session'; import type { Config } from '../../mcp/config'; -import type { SessionConfig, ClientInfo, SessionFile } from './registry'; +import type { ClientInfo, SessionFile } from './registry'; type MinimistArgs = { _: string[]; [key: string]: any; }; -function resolveSessionName(sessionName?: string): string { - if (sessionName) - return sessionName; - if (process.env.PLAYWRIGHT_CLI_SESSION) - return process.env.PLAYWRIGHT_CLI_SESSION; - return 'default'; -} - type GlobalOptions = { help?: boolean; session?: string; @@ -157,24 +149,12 @@ export async function program(options?: { embedderVersion?: string}) { const entry = registry.entry(clientInfo, sessionName); if (entry) await new Session(entry).stop(true); - const config = sessionConfigFromArgs(clientInfo, sessionName, args); - const sessionFile: SessionFile = { - daemonDir: clientInfo.daemonProfilesDir, - file: path.join(clientInfo.daemonProfilesDir, `${sessionName}.session`), - config, - }; - const session = new Session(sessionFile); - // Stale session. - if (await session.canConnect()) - await session.stop(true); - - for (const globalOption of globalOptions) - delete args[globalOption]; - const result = await session.open(args); - console.log(result.text); + + await Session.startDaemon(clientInfo, args); + const newEntry = await registry.loadEntry(clientInfo, sessionName); + await runInSession(newEntry, clientInfo, args); return; } - case 'close': const closeEntry = registry.entry(clientInfo, sessionName); const session = closeEntry ? new Session(closeEntry) : undefined; @@ -200,23 +180,26 @@ export async function program(options?: { embedderVersion?: string}) { return; } default: { - const defaultEntry = registry.entry(clientInfo, sessionName); - if (!defaultEntry) { + const entry = registry.entry(clientInfo, sessionName); + if (!entry) { console.log(`The browser '${sessionName}' is not open, please run open first`); console.log(''); console.log(` playwright-cli${sessionName !== 'default' ? ` -s=${sessionName}` : ''} open [params]`); process.exit(1); } - - for (const globalOption of globalOptions) - delete args[globalOption]; - const session = new Session(defaultEntry); - const result = await session.run(clientInfo, args); - console.log(result.text); + await runInSession(entry, clientInfo, args); } } } +async function runInSession(entry: SessionFile, clientInfo: ClientInfo, args: MinimistArgs) { + for (const globalOption of globalOptions) + delete args[globalOption]; + const session = new Session(entry); + const result = await session.run(clientInfo, args); + console.log(result.text); +} + async function install(args: MinimistArgs) { const cwd = process.cwd(); @@ -298,48 +281,10 @@ async function findOrInstallDefaultBrowser() { return 'chromium'; } -function daemonSocketPath(clientInfo: ClientInfo, sessionName: string): string { - const userNameHash = calculateSha1(process.env.USERNAME || 'default').slice(0, 8); - const socketName = `${sessionName}-${userNameHash}.sock`; - if (os.platform() === 'win32') - return `\\\\.\\pipe\\${clientInfo.workspaceDirHash}-${socketName}`; - const socketsDir = process.env.PLAYWRIGHT_DAEMON_SOCKETS_DIR || path.join(os.tmpdir(), 'playwright-cli'); - return path.join(socketsDir, clientInfo.workspaceDirHash, socketName); -} - function defaultConfigFile(): string { return path.resolve('.playwright', 'cli.config.json'); } -export function sessionConfigFromArgs(clientInfo: ClientInfo, sessionName: string, args: MinimistArgs): SessionConfig { - let config = args.config ? path.resolve(args.config) : undefined; - try { - if (!config && fs.existsSync(defaultConfigFile())) - config = defaultConfigFile(); - } catch { - } - - if (!args.persistent && args.profile) - args.persistent = true; - - return { - name: sessionName, - version: clientInfo.version, - timestamp: Date.now(), - socketPath: daemonSocketPath(clientInfo, sessionName), - cli: { - headed: args.headed, - extension: args.extension, - browser: args.browser, - persistent: args.persistent, - profile: args.profile, - config, - }, - userDataDirPrefix: path.resolve(clientInfo.daemonProfilesDir, `ud-${sessionName}`), - workspaceDir: clientInfo.workspaceDir, - }; -} - async function killAllDaemons(): Promise { const platform = os.platform(); let killed = 0; @@ -349,7 +294,7 @@ async function killAllDaemons(): Promise { const result = execSync( `powershell -NoProfile -NonInteractive -Command ` + `"Get-CimInstance Win32_Process ` - + `| Where-Object { $_.CommandLine -like '*-server*' -and $_.CommandLine -like '*--daemon-session*' } ` + + `| Where-Object { $_.CommandLine -like '*-server*' -and $_.CommandLine -like '*--daemon-dir*' } ` + `| ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue; $_.ProcessId }"`, { encoding: 'utf-8' } ); @@ -363,7 +308,7 @@ async function killAllDaemons(): Promise { const result = execSync('ps aux', { encoding: 'utf-8' }); const lines = result.split('\n'); for (const line of lines) { - if ((line.includes('-server')) && line.includes('--daemon-session')) { + if ((line.includes('-server')) && line.includes('--daemon-dir')) { const parts = line.trim().split(/\s+/); const pid = parts[1]; if (pid && /^\d+$/.test(pid)) { diff --git a/packages/playwright-core/src/cli/client/registry.ts b/packages/playwright-core/src/cli/client/registry.ts index 48a769fee22ac..8a9199ccda15e 100644 --- a/packages/playwright-core/src/cli/client/registry.ts +++ b/packages/playwright-core/src/cli/client/registry.ts @@ -34,16 +34,10 @@ export type SessionConfig = { timestamp: number; socketPath: string; cli: { - headed?: boolean; - extension?: boolean; - browser?: string; persistent?: boolean; - profile?: string; - config?: string; }; - userDataDirPrefix?: string; workspaceDir?: string; - resolvedConfig?: FullConfig + resolvedConfig?: FullConfig; }; export type SessionFile = { @@ -74,6 +68,24 @@ export class Registry { return this._files; } + async loadEntry(clientInfo: ClientInfo, sessionName: string): Promise { + const entry = await Registry._loadSessionEntry(clientInfo.daemonProfilesDir, sessionName + '.session'); + if (!entry) + throw new Error(`Could not start the session "${sessionName}"`); + + const key = clientInfo.workspaceDir || clientInfo.workspaceDirHash; + let list = this._files.get(key); + if (!list) { + list = []; + this._files.set(key, list); + } + const oldIndex = list.findIndex(e => e.config.name === sessionName); + if (oldIndex !== -1) + list.splice(oldIndex, 1); + list.push(entry); + return entry; + } + private static async _loadSessionEntry(daemonDir: string, file: string): Promise { try { const fileName = path.join(daemonDir, file); @@ -169,3 +181,11 @@ function findWorkspaceDir(startDir: string): string | undefined { const daemonProfilesDir = (workspaceDirHash: string) => { return path.join(baseDaemonDir, workspaceDirHash); }; + +export function resolveSessionName(sessionName?: string): string { + if (sessionName) + return sessionName; + if (process.env.PLAYWRIGHT_CLI_SESSION) + return process.env.PLAYWRIGHT_CLI_SESSION; + return 'default'; +} diff --git a/packages/playwright-core/src/cli/client/session.ts b/packages/playwright-core/src/cli/client/session.ts index 44ca7f42c9805..5ca1bb081f0f1 100644 --- a/packages/playwright-core/src/cli/client/session.ts +++ b/packages/playwright-core/src/cli/client/session.ts @@ -15,7 +15,6 @@ */ /* eslint-disable no-console */ -/* eslint-disable no-restricted-properties */ import { spawn } from 'child_process'; @@ -24,6 +23,7 @@ import net from 'net'; import os from 'os'; import path from 'path'; import { compareSemver, SocketConnection } from './socketConnection'; +import { resolveSessionName } from './registry'; import type { FullConfig } from '../../mcp/browser/config'; import type { SessionConfig, ClientInfo, SessionFile } from './registry'; @@ -48,11 +48,6 @@ export class Session { return compareSemver(clientInfo.version, this.config.version) >= 0; } - async open(args: MinimistArgs, cwd?: string): Promise<{ text: string }> { - const socket = await this._startDaemon(); - return await SocketConnectionClient.sendAndClose(socket, 'run', { args, cwd: cwd || process.cwd() }); - } - async run(clientInfo: ClientInfo, args: MinimistArgs, cwd?: string): Promise<{ text: string }> { if (!this.isCompatible(clientInfo)) throw new Error(`Client is v${clientInfo.version}, session '${this.name}' is v${this.config.version}. Run\n\n playwright-cli${this.name !== 'default' ? ` -s=${this.name}` : ''} open\n\nto restart the browser session.`); @@ -60,7 +55,7 @@ export class Session { const { socket } = await this._connect(); if (!socket) throw new Error(`Browser '${this.name}' is not open. Run\n\n playwright-cli${this.name !== 'default' ? ` -s=${this.name}` : ''} open\n\nto start the browser session.`); - return await SocketConnectionClient.sendAndClose(socket, 'run', { args, cwd: cwd || process.cwd() }); + return await SocketConnectionClient.sendAndClose(socket, 'run', { args, cwd: process.cwd() }); } async stop(quiet: boolean = false): Promise { @@ -129,19 +124,32 @@ export class Session { return false; } - private async _startDaemon(): Promise { - await fs.promises.mkdir(this._sessionFile.daemonDir, { recursive: true }); + static async startDaemon(clientInfo: ClientInfo, cliArgs: MinimistArgs) { + await fs.promises.mkdir(clientInfo.daemonProfilesDir, { recursive: true }); + const cliPath = path.join(__dirname, '../../../cli.js'); - await fs.promises.writeFile(this._sessionFile.file, JSON.stringify(this.config, null, 2)); - const errLog = this._sessionFile.file.replace(/\.session$/, '.err'); + const sessionName = resolveSessionName(cliArgs.session); + const errLog = path.join(clientInfo.daemonProfilesDir, sessionName + '.err'); const err = fs.openSync(errLog, 'w'); const args = [ cliPath, 'run-cli-server', - `--daemon-session=${this._sessionFile.file}`, + sessionName, ]; + if (cliArgs.headed) + args.push('--headed'); + if (cliArgs.extension) + args.push('--extension'); + if (cliArgs.browser) + args.push(`--browser=${cliArgs.browser}`); + if (cliArgs.persistent) + args.push('--persistent'); + if (cliArgs.profile) + args.push(`--profile=${cliArgs.profile}`); + if (cliArgs.config) + args.push(`--config=${cliArgs.config}`); const child = spawn(process.execPath, args, { detached: true, @@ -190,24 +198,7 @@ export class Session { child.stdout!.destroy(); child.unref(); - const { socket } = await this._connect(); - if (socket) { - console.log(`### Browser \`${this.name}\` opened with pid ${child.pid}.`); - const resolvedConfig = await parseResolvedConfig(outLog); - if (resolvedConfig) { - this.config.resolvedConfig = resolvedConfig; - console.log(`- ${this.name}:`); - console.log(renderResolvedConfig(resolvedConfig).join('\n')); - } - console.log(`---`); - - this.config.timestamp = Date.now(); - await fs.promises.writeFile(this._sessionFile.file, JSON.stringify(this.config, null, 2)); - return socket; - } - - console.error(`Failed to connect to daemon at ${this.config.socketPath}`); - process.exit(1); + console.log(`### Browser \`${sessionName}\` opened with pid ${child.pid}.`); } private async _stopDaemon(): Promise { @@ -219,10 +210,6 @@ export class Session { let error: Error | undefined; await SocketConnectionClient.sendAndClose(socket, 'stop', {}).catch(e => error = e); - if (os.platform() !== 'win32') - await fs.promises.unlink(this.config.socketPath).catch(() => {}); - if (!this.config.cli.persistent) - await this.deleteSessionConfig(); if (error && !error?.message?.includes('Session closed')) throw error; } @@ -245,23 +232,6 @@ export function renderResolvedConfig(resolvedConfig: FullConfig) { return lines; } -async function parseResolvedConfig(errLog: string): Promise { - const marker = '### Config\n```json\n'; - const markerIndex = errLog.indexOf(marker); - if (markerIndex === -1) - return null; - const jsonStart = markerIndex + marker.length; - const jsonEnd = errLog.indexOf('\n```', jsonStart); - if (jsonEnd === -1) - throw null; - const jsonString = errLog.substring(jsonStart, jsonEnd).trim(); - try { - return JSON.parse(jsonString) as FullConfig; - } catch { - return null; - } -} - class SocketConnectionClient { private _connection: SocketConnection; private _nextMessageId = 1; diff --git a/packages/playwright-core/src/cli/daemon/DEPS.list b/packages/playwright-core/src/cli/daemon/DEPS.list index bde3c84988373..781cf49382f58 100644 --- a/packages/playwright-core/src/cli/daemon/DEPS.list +++ b/packages/playwright-core/src/cli/daemon/DEPS.list @@ -2,6 +2,7 @@ ../../mcp/browser/** ../../mcp/extension/** ../client/socketConnection.ts +../client/registry.ts ../../utilsBundle.ts ../../utils/ ../../mcpBundle.ts diff --git a/packages/playwright-core/src/cli/daemon/daemon.ts b/packages/playwright-core/src/cli/daemon/daemon.ts index d77a1d5940966..51c3b8419eb9b 100644 --- a/packages/playwright-core/src/cli/daemon/daemon.ts +++ b/packages/playwright-core/src/cli/daemon/daemon.ts @@ -14,12 +14,13 @@ * limitations under the License. */ -import fs from 'fs/promises'; +import fs from 'fs'; import net from 'net'; import os from 'os'; import path from 'path'; import url from 'url'; +import { calculateSha1 } from '../../utils'; import { debug } from '../../utilsBundle'; import { decorateServer } from '../../server/utils/network'; import { gracefullyProcessExitDoNotHang } from '../../server/utils/processLauncher'; @@ -29,16 +30,17 @@ import { browserTools } from '../../mcp/browser/tools'; import { SocketConnection } from '../client/socketConnection'; import { commands } from './commands'; import { parseCommand } from './command'; +import { createClientInfo } from '../client/registry'; import type * as playwright from '../../..'; import type * as mcp from '../../mcp/exports'; -import type { SessionConfig } from '../client/registry'; +import type { SessionConfig, ClientInfo } from '../client/registry'; const daemonDebug = debug('pw:daemon'); async function socketExists(socketPath: string): Promise { try { - const stat = await fs.stat(socketPath); + const stat = await fs.promises.stat(socketPath); if (stat?.isSocket()) return true; } catch (e) { @@ -47,31 +49,27 @@ async function socketExists(socketPath: string): Promise { } export async function startMcpDaemonServer( - config: mcp.ContextConfig, - sessionConfig: SessionConfig, + sessionName: string, browserContext: playwright.BrowserContext, - noShutdown?: boolean, -): Promise<() => Promise> { + mcpConfig: mcp.FullConfig, + clientInfo = createClientInfo(), + persistent?: boolean, +): Promise { + const sessionConfig = createSessionConfig(clientInfo, sessionName, persistent); const { socketPath } = sessionConfig; + // Clean up existing socket file on Unix if (os.platform() !== 'win32' && await socketExists(socketPath)) { daemonDebug(`Socket already exists, removing: ${socketPath}`); try { - await fs.unlink(socketPath); + await fs.promises.unlink(socketPath); } catch (error) { daemonDebug(`Failed to remove existing socket: ${error}`); throw error; } } - if (!noShutdown) { - browserContext.on('close', () => { - daemonDebug('browser closed, shutting down daemon'); - shutdown(0); - }); - } - - const backend = new BrowserServerBackend(config, browserContext, browserTools); + const backend = new BrowserServerBackend(mcpConfig, browserContext, browserTools); await backend.initialize({ name: 'playwright-cli', version: sessionConfig.version, @@ -82,13 +80,7 @@ export async function startMcpDaemonServer( timestamp: Date.now(), }); - await fs.mkdir(path.dirname(socketPath), { recursive: true }); - - const shutdown = (exitCode: number) => { - daemonDebug(`shutting down daemon with exit code ${exitCode}`); - server.close(); - gracefullyProcessExitDoNotHang(exitCode); - }; + await fs.promises.mkdir(path.dirname(socketPath), { recursive: true }); const server = net.createServer(socket => { daemonDebug('new client connection'); @@ -102,6 +94,11 @@ export async function startMcpDaemonServer( daemonDebug('received command', method); if (method === 'stop') { daemonDebug('stop command received, shutting down'); + if (process.platform !== 'win32') + await fs.promises.unlink(sessionConfig.socketPath).catch(() => {}); + if (!sessionConfig.cli.persistent) + await deleteSessionFile(clientInfo, sessionConfig); + gracefullyProcessExitDoNotHang(0, async () => { await connection.send({ id, result: 'ok' }).catch(() => {}); server.close(); @@ -124,20 +121,28 @@ export async function startMcpDaemonServer( }); decorateServer(server); - return new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { server.on('error', (error: NodeJS.ErrnoException) => { daemonDebug(`server error: ${error.message}`); reject(error); }); - - server.listen(socketPath, () => { - daemonDebug(`daemon server listening on ${socketPath}`); - resolve(async () => { - await backend.dispose(); - await new Promise(cb => server.close(cb)); - }); - }); + server.listen(socketPath, () => resolve()); }); + + sessionConfig.resolvedConfig = mcpConfig; + await saveSessionFile(clientInfo, sessionConfig); + return socketPath; +} + +async function saveSessionFile(clientInfo: ClientInfo, sessionConfig: SessionConfig) { + await fs.promises.mkdir(clientInfo.daemonProfilesDir, { recursive: true }); + const sessionFile = path.join(clientInfo.daemonProfilesDir, `${sessionConfig.name}.session`); + await fs.promises.writeFile(sessionFile, JSON.stringify(sessionConfig, null, 2)); +} + +async function deleteSessionFile(clientInfo: ClientInfo, sessionConfig: SessionConfig) { + const sessionFile = path.join(clientInfo.daemonProfilesDir, `${sessionConfig.name}.session`); + await fs.promises.rm(sessionFile).catch(() => {}); } function formatResult(result: mcp.CallToolResult) { @@ -152,3 +157,23 @@ function parseCliCommand(args: Record & { _: string[] }): { tool throw new Error('Command is required'); return parseCommand(command, args); } + +function daemonSocketPath(clientInfo: ClientInfo, sessionName: string): string { + const userNameHash = calculateSha1(process.env.USERNAME || 'default').slice(0, 8); + const socketName = `${sessionName}-${userNameHash}.sock`; + if (process.platform === 'win32') + return `\\\\.\\pipe\\${clientInfo.workspaceDirHash}-${socketName}`; + const socketsDir = process.env.PLAYWRIGHT_DAEMON_SOCKETS_DIR || path.join(os.tmpdir(), 'playwright-cli'); + return path.join(socketsDir, clientInfo.workspaceDirHash, socketName); +} + +function createSessionConfig(clientInfo: ClientInfo, sessionName: string, persistent?: boolean): SessionConfig { + return { + name: sessionName, + version: clientInfo.version, + timestamp: Date.now(), + socketPath: daemonSocketPath(clientInfo, sessionName), + workspaceDir: clientInfo.workspaceDir, + cli: { persistent }, + }; +} diff --git a/packages/playwright-core/src/cli/daemon/program.ts b/packages/playwright-core/src/cli/daemon/program.ts index e79269b1d9de0..27e35aed0956c 100644 --- a/packages/playwright-core/src/cli/daemon/program.ts +++ b/packages/playwright-core/src/cli/daemon/program.ts @@ -18,52 +18,52 @@ import fs from 'fs'; import url from 'url'; +import path from 'path'; import { startMcpDaemonServer } from './daemon'; import { setupExitWatchdog } from '../../mcp/browser/watchdog'; import { contextFactory } from '../../mcp/browser/browserContextFactory'; import { ExtensionContextFactory } from '../../mcp/extension/extensionContextFactory'; -import { configFromCLIOptions, configFromEnv, defaultConfig, loadConfig, mergeConfig, validateConfig } from '../../mcp/browser/config'; +import * as configUtils from '../../mcp/browser/config'; +import { gracefullyProcessExitDoNotHang } from '../../utils'; +import { ClientInfo, createClientInfo } from '../client/registry'; import type { Command } from '../../utilsBundle'; -import type { SessionConfig } from '../client/registry'; import type { FullConfig } from '../../mcp/browser/config'; export function decorateCLICommand(command: Command, version: string) { command .version(version) - .option('--daemon-session ', 'path to the daemon config.') - .action(async options => { - // normalize the --no-chromium-sandbox option: chromiumSandbox = true => nothing was passed, chromiumSandbox = false => --no-chromium-sandbox was passed. - options.chromiumSandbox = options.chromiumSandbox === true ? undefined : false; + .argument('[session-name]', 'name of the session to create or connect to', 'default') + .option('--headed', 'run in headed mode (non-headless)') + .option('--extension', 'run with the extension') + .option('--browser ', 'browser to use (chromium, chrome, firefox, webkit)') + .option('--persistent', 'use a persistent browser context') + .option('--profile ', 'path to the user data dir') + .option('--config ', 'path to the config file') + + .action(async (sessionName: string, options: any) => { setupExitWatchdog(); - - const sessionConfig = await fs.promises.readFile(options.daemonSession, 'utf-8').then(data => JSON.parse(data) as SessionConfig); - - const cwd = url.pathToFileURL(process.cwd()).href; - const clientInfo = { + const clientInfo = createClientInfo(); + const mcpConfig = await resolveCLIConfig(clientInfo, sessionName, options); + const mcpClientInfo = { name: 'playwright-cli', - version: sessionConfig.version, + version: require('../../../package.json').version, roots: [{ - uri: cwd, + uri: url.pathToFileURL(process.cwd()).href, name: 'cwd' }], timestamp: Date.now(), }; - const mcpConfig = await resolveCLIConfig(sessionConfig); - const extensionContextFactory = new ExtensionContextFactory(mcpConfig.browser.launchOptions.channel || 'chrome', mcpConfig.browser.userDataDir, mcpConfig.browser.launchOptions.executablePath); - const browserContextFactory = contextFactory(mcpConfig); - const cf = mcpConfig.extension ? extensionContextFactory : browserContextFactory; - try { - const browserContext = mcpConfig.browser.isolated ? await cf.createContext(clientInfo) : (await cf.contexts(clientInfo))[0]; - await startMcpDaemonServer(mcpConfig, sessionConfig, browserContext); - console.log(`### Config`); - console.log('```json'); - console.log(JSON.stringify(mcpConfig, null, 2)); - console.log('```'); - console.log(`### Success\nDaemon listening on ${sessionConfig.socketPath}`); + const extensionContextFactory = new ExtensionContextFactory(mcpConfig.browser.launchOptions.channel || 'chrome', mcpConfig.browser.userDataDir, mcpConfig.browser.launchOptions.executablePath); + const browserContextFactory = contextFactory(mcpConfig); + const cf = mcpConfig.extension ? extensionContextFactory : browserContextFactory; + const browserContext = mcpConfig.browser.isolated ? await cf.createContext(mcpClientInfo) : (await cf.contexts(mcpClientInfo))[0]; + browserContext.on('close', () => gracefullyProcessExitDoNotHang(0)); + const socketPath = await startMcpDaemonServer(sessionName, browserContext, mcpConfig, clientInfo, options.persistent); + console.log(`### Success\nDaemon listening on ${socketPath}`); console.log(''); } catch (error) { const message = process.env.PWDEBUGIMPL ? (error as Error).stack || (error as Error).message : (error as Error).message; @@ -73,23 +73,37 @@ export function decorateCLICommand(command: Command, version: string) { }); } -export async function resolveCLIConfig(sessionConfig: SessionConfig): Promise { - const daemonOverrides = configFromCLIOptions({ - config: sessionConfig.cli.config, - browser: sessionConfig.cli.browser, - isolated: sessionConfig.cli.persistent === true ? false : undefined, - headless: sessionConfig.cli.headed ? false : undefined, - extension: sessionConfig.cli.extension, - userDataDir: sessionConfig.cli.profile, +function defaultConfigFile(): string { + return path.resolve('.playwright', 'cli.config.json'); +} + +export async function resolveCLIConfig(clientInfo: ClientInfo, sessionName: string, options: any): Promise { + const config = options.config ? path.resolve(options.config) : undefined; + try { + if (!config && fs.existsSync(defaultConfigFile())) + options.config = defaultConfigFile(); + } catch { + } + + if (!options.persistent && options.profile) + options.persistent = true; + + const daemonOverrides = configUtils.configFromCLIOptions({ + config: options.config, + browser: options.browser, + isolated: options.persistent === true ? false : undefined, + headless: options.headed ? false : undefined, + extension: options.extension, + userDataDir: options.profile, outputMode: 'file', snapshotMode: 'full', }); - const envOverrides = configFromEnv(); + const envOverrides = configUtils.configFromEnv(); const configFile = envOverrides.configFile ?? daemonOverrides.configFile; - const configInFile = await loadConfig(configFile); + const configInFile = await configUtils.loadConfig(configFile); - let result = mergeConfig(defaultConfig, { + let result = configUtils.mergeConfig(configUtils.defaultConfig, { browser: { launchOptions: { headless: true, @@ -98,14 +112,14 @@ export async function resolveCLIConfig(sessionConfig: SessionConfig): Promise; contextOptions: NonNullable; - isolated: boolean; }, - server: NonNullable, + server?: Config['server'], skillMode?: boolean; configFile?: string; }; diff --git a/packages/playwright-core/src/mcp/exports.ts b/packages/playwright-core/src/mcp/exports.ts index 16b84f6c6e273..1d3ae9d1288bc 100644 --- a/packages/playwright-core/src/mcp/exports.ts +++ b/packages/playwright-core/src/mcp/exports.ts @@ -25,7 +25,7 @@ export { setupExitWatchdog } from './browser/watchdog'; export type { Tool as BrowserTool } from './browser/tools/tool'; export { logUnhandledError } from './log'; export type { ContextConfig } from './browser/context'; +export type { FullConfig } from './browser/config'; export { startMcpDaemonServer } from '../cli/daemon/daemon'; -export { sessionConfigFromArgs } from '../cli/client/program'; export { createClientInfo } from '../cli/client/registry'; export { filteredTools } from './browser/tools'; diff --git a/packages/playwright-core/src/mcp/sdk/server.ts b/packages/playwright-core/src/mcp/sdk/server.ts index b15b66c8a7f5c..dee2f9bb7249c 100644 --- a/packages/playwright-core/src/mcp/sdk/server.ts +++ b/packages/playwright-core/src/mcp/sdk/server.ts @@ -195,7 +195,7 @@ function addServerListener(server: Server, event: 'close' | 'initialized', liste }; } -export async function start(serverBackendFactory: ServerBackendFactory, options: { host?: string; port?: number, allowedHosts?: string[], socketPath?: string }) { +export async function start(serverBackendFactory: ServerBackendFactory, options: { host?: string; port?: number, allowedHosts?: string[], socketPath?: string } = {}) { if (options.port === undefined) { await connect(serverBackendFactory, new mcpBundle.StdioServerTransport(), false); return; diff --git a/packages/playwright/src/mcp/test/browserBackend.ts b/packages/playwright/src/mcp/test/browserBackend.ts index 12b698f274cd7..fc36578fe8716 100644 --- a/packages/playwright/src/mcp/test/browserBackend.ts +++ b/packages/playwright/src/mcp/test/browserBackend.ts @@ -15,7 +15,6 @@ */ import path from 'path'; -import fs from 'fs'; import { createGuid } from 'playwright-core/lib/utils'; import * as mcp from 'playwright-core/lib/mcp/exports'; import { BrowserServerBackend } from 'playwright-core/lib/mcp/exports'; @@ -125,17 +124,16 @@ export async function runDaemonForContext(testInfo: TestInfoImpl, context: playw const outputDir = path.join(testInfo.artifactsDir(), '.playwright-mcp'); const sessionName = `test-worker-${createGuid().slice(0, 6)}`; - const clientInfo = mcp.createClientInfo(); - const sessionConfig = mcp.sessionConfigFromArgs(clientInfo, sessionName, { _: [] }); - const sessionConfigFile = path.resolve(clientInfo.daemonProfilesDir, `${sessionName}.session`); - await fs.promises.mkdir(path.dirname(sessionConfigFile), { recursive: true }); - await fs.promises.writeFile(sessionConfigFile, JSON.stringify(sessionConfig, null, 2)); - - await mcp.startMcpDaemonServer({ + await mcp.startMcpDaemonServer(sessionName, context, { + browser: { + browserName: 'chromium', + launchOptions: {}, + contextOptions: {}, + }, outputMode: 'file', snapshot: { mode: 'full' }, outputDir, - }, sessionConfig, context, true /* noShutdown */); + }); const lines = ['']; if (testInfo.errors.length) { diff --git a/tests/mcp/cli-isolated.spec.ts b/tests/mcp/cli-isolated.spec.ts index aedc266e28e83..7d7b072841761 100644 --- a/tests/mcp/cli-isolated.spec.ts +++ b/tests/mcp/cli-isolated.spec.ts @@ -28,7 +28,6 @@ test('should not save user data by default (in-memory mode)', async ({ cli, serv cli: {}, socketPath: expect.any(String), timestamp: expect.any(Number), - userDataDirPrefix: expect.any(String), version: expect.any(String), workspaceDir: testInfo.outputPath(), resolvedConfig: expect.any(Object), @@ -55,7 +54,6 @@ test('should save user data with --persistent flag', async ({ cli, server, mcpBr }, socketPath: expect.any(String), timestamp: expect.any(Number), - userDataDirPrefix: expect.any(String), version: expect.any(String), workspaceDir: testInfo.outputPath(), resolvedConfig: expect.any(Object), @@ -71,11 +69,9 @@ test('should use custom user data dir with --profile=', async ({ cli, serve name: 'default', cli: { persistent: true, - profile: customDir, }, socketPath: expect.any(String), timestamp: expect.any(Number), - userDataDirPrefix: expect.any(String), version: expect.any(String), workspaceDir: testInfo.outputPath(), resolvedConfig: expect.any(Object), diff --git a/tests/mcp/cli-session.spec.ts b/tests/mcp/cli-session.spec.ts index b1adb837ce349..746e885ef7a18 100644 --- a/tests/mcp/cli-session.spec.ts +++ b/tests/mcp/cli-session.spec.ts @@ -140,16 +140,6 @@ test('session reopen with different config', async ({ cli, server }, testInfo) = } }); -test('session start should print browser config', async ({ cli, server }, testInfo) => { - const configPath = testInfo.outputPath('my-config.json'); - await fs.promises.writeFile(configPath, JSON.stringify({}, null, 2)); - - const { output } = await cli('open', '--headed', '--config=' + configPath, server.HELLO_WORLD); - expect(output).toContain('### Browser `default` opened'); - expect(output).toContain('- default:'); - expect(output).toContain('- headed:'); -}); - test('workspace isolation - sessions in different workspaces are isolated', async ({ cli, server }, testInfo) => { // Create two separate workspaces with their own daemon dirs const workspace1 = testInfo.outputPath('workspace1'); From 7aac9b4fb8f7d2d3f04f28cf446f8328673b551f Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 5 Mar 2026 15:16:11 -0800 Subject: [PATCH 3/3] chore: organize client folders (#39535) --- .claude/skills/playwright-dev/mcp-dev.md | 6 +- packages/playwright-core/package.json | 1 + .../playwright-core/src/cli/client/DEPS.list | 6 -- .../playwright-core/src/cli/client/program.ts | 12 +-- .../src/cli/client/registry.ts | 8 +- .../playwright-core/src/cli/client/session.ts | 11 ++- .../playwright-core/src/cli/daemon/DEPS.list | 4 +- .../src/cli/daemon/commands.ts | 10 --- .../playwright-core/src/cli/daemon/daemon.ts | 32 ++++---- .../playwright-core/src/cli/daemon/program.ts | 21 ++---- .../playwright-core/src/client/browser.ts | 4 +- .../playwright-core/src/client/browserType.ts | 2 +- .../playwright-core/src/devtools/DEPS.list | 7 ++ .../src/{cli/client => devtools}/appIcon.png | Bin .../{cli/client => devtools}/devtoolsApp.ts | 18 ++--- packages/playwright-core/src/mcp/DEPS.list | 13 +++- .../playwright-core/src/mcp/browser/DEPS.list | 13 ---- .../src/mcp/browser/tools/DEPS.list | 11 --- .../{browser => }/browserContextFactory.ts | 23 +++--- .../src/mcp/{extension => }/cdpRelay.ts | 18 ++--- .../src/mcp/{browser => }/config.ts | 10 +-- .../src/mcp/{browser => }/configIni.ts | 4 +- packages/playwright-core/src/mcp/exports.ts | 19 ++--- .../src/mcp/extension/DEPS.list | 10 --- .../extensionContextFactory.ts | 10 +-- packages/playwright-core/src/mcp/index.ts | 12 +-- packages/playwright-core/src/mcp/program.ts | 12 +-- .../src/mcp/{extension => }/protocol.ts | 0 .../playwright-core/src/mcp/sdk/server.ts | 18 ++--- .../src/mcp/{browser => }/watchdog.ts | 4 +- packages/playwright-core/src/tools/DEPS.list | 7 ++ .../browser => tools}/browserServerBackend.ts | 23 ++---- .../src/{mcp/browser => }/tools/common.ts | 4 +- .../src/{mcp/browser => }/tools/config.ts | 2 +- .../src/{mcp/browser => }/tools/console.ts | 2 +- .../src/{mcp/browser => tools}/context.ts | 69 ++++++++++-------- .../src/{mcp/browser => }/tools/cookies.ts | 2 +- .../src/{mcp/browser => }/tools/devtools.ts | 2 +- .../src/{mcp/browser => }/tools/dialogs.ts | 2 +- .../src/{mcp/browser => }/tools/evaluate.ts | 6 +- packages/playwright-core/src/tools/exports.ts | 24 ++++++ .../src/{mcp/browser => }/tools/files.ts | 2 +- .../src/{mcp/browser => }/tools/form.ts | 4 +- .../src/{mcp/browser => }/tools/keyboard.ts | 2 +- .../src/{mcp/browser => tools}/logFile.ts | 4 +- .../src/{mcp/browser => }/tools/mouse.ts | 4 +- .../src/{mcp/browser => }/tools/navigate.ts | 2 +- .../src/{mcp/browser => }/tools/network.ts | 4 +- .../src/{mcp/browser => }/tools/pdf.ts | 4 +- .../src/{mcp/browser => tools}/response.ts | 4 +- .../src/{mcp/browser => }/tools/route.ts | 6 +- .../src/{mcp/browser => }/tools/runCode.ts | 4 +- .../src/{mcp/browser => }/tools/screenshot.ts | 10 +-- .../src/{mcp/browser => tools}/sessionLog.ts | 0 .../src/{mcp/browser => }/tools/snapshot.ts | 4 +- .../src/{mcp/browser => }/tools/storage.ts | 2 +- .../src/{mcp/browser => tools}/tab.ts | 30 ++++---- .../src/{mcp/browser => }/tools/tabs.ts | 4 +- .../src/{mcp/browser => }/tools/tool.ts | 20 +++-- .../src/{mcp/browser => tools}/tools.ts | 52 ++++++------- .../src/{mcp/browser => }/tools/tracing.ts | 30 +------- .../src/{mcp/browser => }/tools/utils.ts | 4 +- .../src/{mcp/browser => }/tools/verify.ts | 4 +- .../src/{mcp/browser => }/tools/video.ts | 2 +- .../src/{mcp/browser => }/tools/wait.ts | 2 +- .../src/{mcp/browser => }/tools/webstorage.ts | 2 +- packages/playwright/src/DEPS.list | 1 - packages/playwright/src/agents/DEPS.list | 1 - .../playwright/src/mcp/test/browserBackend.ts | 20 ++--- .../playwright/src/mcp/test/testBackend.ts | 4 +- .../playwright/src/mcp/test/testContext.ts | 10 +-- tests/mcp/cli-devtools.spec.ts | 10 --- tests/mcp/cli-isolated.spec.ts | 6 +- tests/mcp/config.spec.ts | 4 +- tests/mcp/fixtures.ts | 4 +- tests/mcp/http.spec.ts | 2 +- tests/mcp/profile-lock.spec.ts | 2 +- tests/mcp/sse.spec.ts | 2 +- tests/mcp/tracing.spec.ts | 34 --------- utils/build/build.js | 6 +- 80 files changed, 340 insertions(+), 434 deletions(-) create mode 100644 packages/playwright-core/src/devtools/DEPS.list rename packages/playwright-core/src/{cli/client => devtools}/appIcon.png (100%) rename packages/playwright-core/src/{cli/client => devtools}/devtoolsApp.ts (94%) delete mode 100644 packages/playwright-core/src/mcp/browser/DEPS.list delete mode 100644 packages/playwright-core/src/mcp/browser/tools/DEPS.list rename packages/playwright-core/src/mcp/{browser => }/browserContextFactory.ts (93%) rename packages/playwright-core/src/mcp/{extension => }/cdpRelay.ts (96%) rename packages/playwright-core/src/mcp/{browser => }/config.ts (98%) rename packages/playwright-core/src/mcp/{browser => }/configIni.ts (98%) delete mode 100644 packages/playwright-core/src/mcp/extension/DEPS.list rename packages/playwright-core/src/mcp/{extension => }/extensionContextFactory.ts (88%) rename packages/playwright-core/src/mcp/{extension => }/protocol.ts (100%) rename packages/playwright-core/src/mcp/{browser => }/watchdog.ts (91%) create mode 100644 packages/playwright-core/src/tools/DEPS.list rename packages/playwright-core/src/{mcp/browser => tools}/browserServerBackend.ts (81%) rename packages/playwright-core/src/{mcp/browser => }/tools/common.ts (95%) rename packages/playwright-core/src/{mcp/browser => }/tools/config.ts (96%) rename packages/playwright-core/src/{mcp/browser => }/tools/console.ts (98%) rename packages/playwright-core/src/{mcp/browser => tools}/context.ts (90%) rename packages/playwright-core/src/{mcp/browser => }/tools/cookies.ts (99%) rename packages/playwright-core/src/{mcp/browser => }/tools/devtools.ts (96%) rename packages/playwright-core/src/{mcp/browser => }/tools/dialogs.ts (97%) rename packages/playwright-core/src/{mcp/browser => }/tools/evaluate.ts (93%) create mode 100644 packages/playwright-core/src/tools/exports.ts rename packages/playwright-core/src/{mcp/browser => }/tools/files.ts (97%) rename packages/playwright-core/src/{mcp/browser => }/tools/form.ts (95%) rename packages/playwright-core/src/{mcp/browser => }/tools/keyboard.ts (99%) rename packages/playwright-core/src/{mcp/browser => tools}/logFile.ts (96%) rename packages/playwright-core/src/{mcp/browser => }/tools/mouse.ts (98%) rename packages/playwright-core/src/{mcp/browser => }/tools/navigate.ts (98%) rename packages/playwright-core/src/{mcp/browser => }/tools/network.ts (97%) rename packages/playwright-core/src/{mcp/browser => }/tools/pdf.ts (93%) rename packages/playwright-core/src/{mcp/browser => tools}/response.ts (99%) rename packages/playwright-core/src/{mcp/browser => }/tools/route.ts (97%) rename packages/playwright-core/src/{mcp/browser => }/tools/runCode.ts (94%) rename packages/playwright-core/src/{mcp/browser => }/tools/screenshot.ts (93%) rename packages/playwright-core/src/{mcp/browser => tools}/sessionLog.ts (100%) rename packages/playwright-core/src/{mcp/browser => }/tools/snapshot.ts (98%) rename packages/playwright-core/src/{mcp/browser => }/tools/storage.ts (98%) rename packages/playwright-core/src/{mcp/browser => tools}/tab.ts (96%) rename packages/playwright-core/src/{mcp/browser => }/tools/tabs.ts (95%) rename packages/playwright-core/src/{mcp/browser => }/tools/tool.ts (81%) rename packages/playwright-core/src/{mcp/browser => tools}/tools.ts (56%) rename packages/playwright-core/src/{mcp/browser => }/tools/tracing.ts (71%) rename packages/playwright-core/src/{mcp/browser => }/tools/utils.ts (97%) rename packages/playwright-core/src/{mcp/browser => }/tools/verify.ts (98%) rename packages/playwright-core/src/{mcp/browser => }/tools/video.ts (98%) rename packages/playwright-core/src/{mcp/browser => }/tools/wait.ts (98%) rename packages/playwright-core/src/{mcp/browser => }/tools/webstorage.ts (99%) diff --git a/.claude/skills/playwright-dev/mcp-dev.md b/.claude/skills/playwright-dev/mcp-dev.md index e022738a5b376..dffe0ba4e9614 100644 --- a/.claude/skills/playwright-dev/mcp-dev.md +++ b/.claude/skills/playwright-dev/mcp-dev.md @@ -4,7 +4,7 @@ ### Step 1: Create the Tool File -Create `packages/playwright/src/mcp/browser/tools/.ts`. +Create `packages/playwright/src/tools/.ts`. Import zod from the MCP bundle and use `defineTool` or `defineTabTool`: @@ -115,7 +115,7 @@ export type ToolCapability = ### Step 3: Register the Tool -In `packages/playwright/src/mcp/browser/tools.ts`: +In `packages/playwright/src/tools/tools.ts`: ```typescript import myTool from './tools/myTool'; @@ -331,7 +331,7 @@ export type Config = { }; ``` -### 2. CLI options type: `packages/playwright/src/mcp/browser/config.ts` +### 2. CLI options type: `packages/playwright/src/mcp/config.ts` Add to `CLIOptions` type: diff --git a/packages/playwright-core/package.json b/packages/playwright-core/package.json index 2789a85e68779..f8134d33d0b47 100644 --- a/packages/playwright-core/package.json +++ b/packages/playwright-core/package.json @@ -27,6 +27,7 @@ "./lib/cli/client/program": "./lib/cli/client/program.js", "./lib/mcpBundle": "./lib/mcpBundle.js", "./lib/mcp/exports": "./lib/mcp/exports.js", + "./lib/tools/exports": "./lib/tools/exports.js", "./lib/mcp/index": "./lib/mcp/index.js", "./lib/remote/playwrightServer": "./lib/remote/playwrightServer.js", "./lib/server": "./lib/server/index.js", diff --git a/packages/playwright-core/src/cli/client/DEPS.list b/packages/playwright-core/src/cli/client/DEPS.list index 4b7838b817aa2..0f79a6e8a2f2d 100644 --- a/packages/playwright-core/src/cli/client/DEPS.list +++ b/packages/playwright-core/src/cli/client/DEPS.list @@ -13,9 +13,3 @@ [registry.ts] "strict" - -[devtoolsApp.ts] -../../../ -../../server/registry/index.ts -../../server/utils/ -../../utils/ diff --git a/packages/playwright-core/src/cli/client/program.ts b/packages/playwright-core/src/cli/client/program.ts index 8ad2dc97b6d95..08ce1f0546032 100644 --- a/packages/playwright-core/src/cli/client/program.ts +++ b/packages/playwright-core/src/cli/client/program.ts @@ -26,7 +26,7 @@ import path from 'path'; import { createClientInfo, Registry, resolveSessionName } from './registry'; import { Session, renderResolvedConfig } from './session'; -import type { Config } from '../../mcp/config'; +import type { Config } from '../../mcp/config.d'; import type { ClientInfo, SessionFile } from './registry'; type MinimistArgs = { @@ -171,7 +171,7 @@ export async function program(options?: { embedderVersion?: string}) { await installBrowser(); return; case 'show': { - const daemonScript = path.join(__dirname, 'devtoolsApp.js'); + const daemonScript = require.resolve('../../devtools/devtoolsApp.js'); const child = spawn(process.execPath, [daemonScript], { detached: true, stdio: 'ignore', @@ -294,7 +294,7 @@ async function killAllDaemons(): Promise { const result = execSync( `powershell -NoProfile -NonInteractive -Command ` + `"Get-CimInstance Win32_Process ` - + `| Where-Object { $_.CommandLine -like '*-server*' -and $_.CommandLine -like '*--daemon-dir*' } ` + + `| Where-Object { $_.CommandLine -like '*-server*' -and $_.CommandLine -like '*--daemon-*' } ` + `| ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue; $_.ProcessId }"`, { encoding: 'utf-8' } ); @@ -308,7 +308,7 @@ async function killAllDaemons(): Promise { const result = execSync('ps aux', { encoding: 'utf-8' }); const lines = result.split('\n'); for (const line of lines) { - if ((line.includes('-server')) && line.includes('--daemon-dir')) { + if ((line.includes('-server')) && line.includes('--daemon-')) { const parts = line.trim().split(/\s+/); const pid = parts[1]; if (pid && /^\d+$/.test(pid)) { @@ -385,8 +385,8 @@ async function renderSessionStatus(clientInfo: ClientInfo, session: Session) { text.push(` - status: ${canConnect ? 'open' : 'closed'}`); if (canConnect && !session.isCompatible(clientInfo)) text.push(` - version: v${config.version} [incompatible please re-open]`); - if (config.resolvedConfig) - text.push(...renderResolvedConfig(config.resolvedConfig)); + if (config.browser) + text.push(...renderResolvedConfig(config)); return text.join('\n'); } diff --git a/packages/playwright-core/src/cli/client/registry.ts b/packages/playwright-core/src/cli/client/registry.ts index 8a9199ccda15e..0a9f8d359070d 100644 --- a/packages/playwright-core/src/cli/client/registry.ts +++ b/packages/playwright-core/src/cli/client/registry.ts @@ -19,7 +19,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import type { FullConfig } from '../../mcp/browser/config'; +import type * as playwright from '../../..'; export type ClientInfo = { version: string; @@ -37,7 +37,11 @@ export type SessionConfig = { persistent?: boolean; }; workspaceDir?: string; - resolvedConfig?: FullConfig; + browser: { + browserName: string; + launchOptions: playwright.LaunchOptions; + userDataDir?: string; + }; }; export type SessionFile = { diff --git a/packages/playwright-core/src/cli/client/session.ts b/packages/playwright-core/src/cli/client/session.ts index 5ca1bb081f0f1..fc874c5aa2607 100644 --- a/packages/playwright-core/src/cli/client/session.ts +++ b/packages/playwright-core/src/cli/client/session.ts @@ -25,7 +25,6 @@ import path from 'path'; import { compareSemver, SocketConnection } from './socketConnection'; import { resolveSessionName } from './registry'; -import type { FullConfig } from '../../mcp/browser/config'; import type { SessionConfig, ClientInfo, SessionFile } from './registry'; type MinimistArgs = { @@ -219,16 +218,16 @@ export class Session { } } -export function renderResolvedConfig(resolvedConfig: FullConfig) { - const channel = resolvedConfig.browser.launchOptions.channel ?? resolvedConfig.browser.browserName; +export function renderResolvedConfig(config: SessionConfig) { + const channel = config.browser.launchOptions.channel ?? config.browser.browserName; const lines = []; if (channel) lines.push(` - browser-type: ${channel}`); - if (resolvedConfig.browser.isolated) + if (!config.cli.persistent) lines.push(` - user-data-dir: `); else - lines.push(` - user-data-dir: ${resolvedConfig.browser.userDataDir}`); - lines.push(` - headed: ${!resolvedConfig.browser.launchOptions.headless}`); + lines.push(` - user-data-dir: ${config.browser.userDataDir}`); + lines.push(` - headed: ${!config.browser.launchOptions.headless}`); return lines; } diff --git a/packages/playwright-core/src/cli/daemon/DEPS.list b/packages/playwright-core/src/cli/daemon/DEPS.list index 781cf49382f58..7a50ccf92bf40 100644 --- a/packages/playwright-core/src/cli/daemon/DEPS.list +++ b/packages/playwright-core/src/cli/daemon/DEPS.list @@ -1,9 +1,9 @@ [*] -../../mcp/browser/** -../../mcp/extension/** ../client/socketConnection.ts ../client/registry.ts +../../tools/ ../../utilsBundle.ts ../../utils/ ../../mcpBundle.ts +../../mcp/ ../../server/utils/ diff --git a/packages/playwright-core/src/cli/daemon/commands.ts b/packages/playwright-core/src/cli/daemon/commands.ts index 2065cc1d9366c..1987ef8486a97 100644 --- a/packages/playwright-core/src/cli/daemon/commands.ts +++ b/packages/playwright-core/src/cli/daemon/commands.ts @@ -754,15 +754,6 @@ const tracingStop = declareCommand({ toolParams: () => ({}), }); -const tracingShow = declareCommand({ - name: 'tracing-show', - description: 'Open trace viewer for the recorded trace', - category: 'devtools', - args: z.object({}), - toolName: 'browser_show_tracing', - toolParams: () => ({}), -}); - const videoStart = declareCommand({ name: 'video-start', description: 'Start video recording', @@ -974,7 +965,6 @@ const commandsArray: AnyCommandSchema[] = [ networkRequests, tracingStart, tracingStop, - tracingShow, videoStart, videoStop, devtoolsShow, diff --git a/packages/playwright-core/src/cli/daemon/daemon.ts b/packages/playwright-core/src/cli/daemon/daemon.ts index 51c3b8419eb9b..4b4b273fe8f8e 100644 --- a/packages/playwright-core/src/cli/daemon/daemon.ts +++ b/packages/playwright-core/src/cli/daemon/daemon.ts @@ -18,23 +18,24 @@ import fs from 'fs'; import net from 'net'; import os from 'os'; import path from 'path'; -import url from 'url'; import { calculateSha1 } from '../../utils'; import { debug } from '../../utilsBundle'; import { decorateServer } from '../../server/utils/network'; import { gracefullyProcessExitDoNotHang } from '../../server/utils/processLauncher'; -import { BrowserServerBackend } from '../../mcp/browser/browserServerBackend'; -import { browserTools } from '../../mcp/browser/tools'; +import { BrowserServerBackend } from '../../tools/browserServerBackend'; +import { browserTools } from '../../tools/tools'; import { SocketConnection } from '../client/socketConnection'; import { commands } from './commands'; import { parseCommand } from './command'; import { createClientInfo } from '../client/registry'; import type * as playwright from '../../..'; +import type * as tools from '../../tools/exports'; import type * as mcp from '../../mcp/exports'; import type { SessionConfig, ClientInfo } from '../client/registry'; +import type { BrowserContext } from '../../client/browserContext'; const daemonDebug = debug('pw:daemon'); @@ -51,15 +52,15 @@ async function socketExists(socketPath: string): Promise { export async function startMcpDaemonServer( sessionName: string, browserContext: playwright.BrowserContext, - mcpConfig: mcp.FullConfig, + mcpConfig: tools.ContextConfig, clientInfo = createClientInfo(), persistent?: boolean, ): Promise { - const sessionConfig = createSessionConfig(clientInfo, sessionName, persistent); + const sessionConfig = createSessionConfig(clientInfo, sessionName, browserContext, persistent); const { socketPath } = sessionConfig; // Clean up existing socket file on Unix - if (os.platform() !== 'win32' && await socketExists(socketPath)) { + if (process.platform !== 'win32' && await socketExists(socketPath)) { daemonDebug(`Socket already exists, removing: ${socketPath}`); try { await fs.promises.unlink(socketPath); @@ -70,15 +71,7 @@ export async function startMcpDaemonServer( } const backend = new BrowserServerBackend(mcpConfig, browserContext, browserTools); - await backend.initialize({ - name: 'playwright-cli', - version: sessionConfig.version, - roots: [{ - uri: url.pathToFileURL(process.cwd()).href, - name: 'cwd', - }], - timestamp: Date.now(), - }); + await backend.initialize({ cwd: process.cwd() }); await fs.promises.mkdir(path.dirname(socketPath), { recursive: true }); @@ -129,7 +122,6 @@ export async function startMcpDaemonServer( server.listen(socketPath, () => resolve()); }); - sessionConfig.resolvedConfig = mcpConfig; await saveSessionFile(clientInfo, sessionConfig); return socketPath; } @@ -167,7 +159,8 @@ function daemonSocketPath(clientInfo: ClientInfo, sessionName: string): string { return path.join(socketsDir, clientInfo.workspaceDirHash, socketName); } -function createSessionConfig(clientInfo: ClientInfo, sessionName: string, persistent?: boolean): SessionConfig { +function createSessionConfig(clientInfo: ClientInfo, sessionName: string, browserContext: playwright.BrowserContext, persistent?: boolean): SessionConfig { + const bc = browserContext as BrowserContext; return { name: sessionName, version: clientInfo.version, @@ -175,5 +168,10 @@ function createSessionConfig(clientInfo: ClientInfo, sessionName: string, persis socketPath: daemonSocketPath(clientInfo, sessionName), workspaceDir: clientInfo.workspaceDir, cli: { persistent }, + browser: { + browserName: bc.browser()!.browserType().name(), + launchOptions: bc.browser()!._options, + userDataDir: bc.browser()?._userDataDir, + }, }; } diff --git a/packages/playwright-core/src/cli/daemon/program.ts b/packages/playwright-core/src/cli/daemon/program.ts index 27e35aed0956c..af23cffcb9d29 100644 --- a/packages/playwright-core/src/cli/daemon/program.ts +++ b/packages/playwright-core/src/cli/daemon/program.ts @@ -17,19 +17,18 @@ /* eslint-disable no-console */ import fs from 'fs'; -import url from 'url'; import path from 'path'; import { startMcpDaemonServer } from './daemon'; -import { setupExitWatchdog } from '../../mcp/browser/watchdog'; -import { contextFactory } from '../../mcp/browser/browserContextFactory'; -import { ExtensionContextFactory } from '../../mcp/extension/extensionContextFactory'; -import * as configUtils from '../../mcp/browser/config'; +import { setupExitWatchdog } from '../../mcp/watchdog'; +import { contextFactory } from '../../mcp/browserContextFactory'; +import { ExtensionContextFactory } from '../../mcp/extensionContextFactory'; +import * as configUtils from '../../mcp/config'; import { gracefullyProcessExitDoNotHang } from '../../utils'; import { ClientInfo, createClientInfo } from '../client/registry'; import type { Command } from '../../utilsBundle'; -import type { FullConfig } from '../../mcp/browser/config'; +import type { FullConfig } from '../../mcp/config'; export function decorateCLICommand(command: Command, version: string) { command @@ -46,15 +45,7 @@ export function decorateCLICommand(command: Command, version: string) { setupExitWatchdog(); const clientInfo = createClientInfo(); const mcpConfig = await resolveCLIConfig(clientInfo, sessionName, options); - const mcpClientInfo = { - name: 'playwright-cli', - version: require('../../../package.json').version, - roots: [{ - uri: url.pathToFileURL(process.cwd()).href, - name: 'cwd' - }], - timestamp: Date.now(), - }; + const mcpClientInfo = { cwd: process.cwd() }; try { const extensionContextFactory = new ExtensionContextFactory(mcpConfig.browser.launchOptions.channel || 'chrome', mcpConfig.browser.userDataDir, mcpConfig.browser.launchOptions.executablePath); diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index d21c88924e782..2cf3b3cac171a 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -35,6 +35,7 @@ export class Browser extends ChannelOwner implements ap _shouldCloseConnectionOnClose = false; _browserType!: BrowserType; _options: LaunchOptions = {}; + _userDataDir: string | undefined; readonly _name: string; private _path: string | undefined; _closeReason: string | undefined; @@ -89,12 +90,13 @@ export class Browser extends ChannelOwner implements ap return context; } - _connectToBrowserType(browserType: BrowserType, browserOptions: LaunchOptions, logger: Logger | undefined) { + _connectToBrowserType(browserType: BrowserType, browserOptions: LaunchOptions, logger: Logger | undefined, userDataDir?: string) { // Note: when using connect(), `browserType` is different from `this._parent`. // This is why browser type is not wired up in the constructor, // and instead this separate method is called later on. this._browserType = browserType; this._options = browserOptions; + this._userDataDir = userDataDir; this._logger = logger; for (const context of this._contexts) this._setupBrowserContext(context); diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index ac4a19cd63f24..e6ae29ca8c0a2 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -112,7 +112,7 @@ export class BrowserType extends ChannelOwner imple const context = await this._wrapApiCall(async () => { const result = await this._channel.launchPersistentContext(persistentParams); const browser = Browser.from(result.browser); - browser._connectToBrowserType(this, options, logger); + browser._connectToBrowserType(this, options, logger, userDataDir); const context = BrowserContext.from(result.context); await context._initializeHarFromOptions(options.recordHar); return context; diff --git a/packages/playwright-core/src/devtools/DEPS.list b/packages/playwright-core/src/devtools/DEPS.list new file mode 100644 index 0000000000000..b422e7cab99f2 --- /dev/null +++ b/packages/playwright-core/src/devtools/DEPS.list @@ -0,0 +1,7 @@ +[*] +../../ +../server/registry/index.ts +../server/utils/ +../utils/ +../cli/client/registry.ts +../cli/client/session.ts diff --git a/packages/playwright-core/src/cli/client/appIcon.png b/packages/playwright-core/src/devtools/appIcon.png similarity index 100% rename from packages/playwright-core/src/cli/client/appIcon.png rename to packages/playwright-core/src/devtools/appIcon.png diff --git a/packages/playwright-core/src/cli/client/devtoolsApp.ts b/packages/playwright-core/src/devtools/devtoolsApp.ts similarity index 94% rename from packages/playwright-core/src/cli/client/devtoolsApp.ts rename to packages/playwright-core/src/devtools/devtoolsApp.ts index b479cd9d85037..948d9cf63ae88 100644 --- a/packages/playwright-core/src/cli/client/devtoolsApp.ts +++ b/packages/playwright-core/src/devtools/devtoolsApp.ts @@ -20,17 +20,17 @@ import os from 'os'; import net from 'net'; -import { chromium } from '../../..'; -import { HttpServer } from '../../server/utils/httpServer'; -import { gracefullyProcessExitDoNotHang } from '../../server/utils/processLauncher'; -import { findChromiumChannelBestEffort, registryDirectory } from '../../server/registry/index'; -import { calculateSha1 } from '../../utils'; -import { createClientInfo, Registry } from './registry'; -import { Session } from './session'; +import { chromium } from '../..'; +import { HttpServer } from '../server/utils/httpServer'; +import { gracefullyProcessExitDoNotHang } from '../server/utils/processLauncher'; +import { findChromiumChannelBestEffort, registryDirectory } from '../server/registry/index'; +import { calculateSha1 } from '../utils'; +import { createClientInfo, Registry } from '../cli/client/registry'; +import { Session } from '../cli/client/session'; import type http from 'http'; -import type { Page } from '../../..'; -import type { ClientInfo, SessionFile } from './registry'; +import type { Page } from '../../types/types'; +import type { ClientInfo, SessionFile } from '../cli/client/registry'; import type { SessionStatus } from '@devtools/sessionModel'; function readBody(request: http.IncomingMessage): Promise { diff --git a/packages/playwright-core/src/mcp/DEPS.list b/packages/playwright-core/src/mcp/DEPS.list index 0f8caf69d6f0d..31ee6692f7815 100644 --- a/packages/playwright-core/src/mcp/DEPS.list +++ b/packages/playwright-core/src/mcp/DEPS.list @@ -1,8 +1,13 @@ [*] ../../ ./sdk/ -./browser/ -./browser/tools/ -./extension/ -../cli/ +../tools/ +../utils/ +../utils/isomorphic/ ../utilsBundle.ts +../mcpBundle.ts +../server/ +../server/registry/ +../server/utils/ +../client/ +../cli/ diff --git a/packages/playwright-core/src/mcp/browser/DEPS.list b/packages/playwright-core/src/mcp/browser/DEPS.list deleted file mode 100644 index a944a2b211da6..0000000000000 --- a/packages/playwright-core/src/mcp/browser/DEPS.list +++ /dev/null @@ -1,13 +0,0 @@ -[*] -../../../ -./tools/ -../sdk/ -../log.ts -../../utils/ -../../utils/isomorphic/ -../../utilsBundle.ts -../../mcpBundle.ts -../../server/ -../../server/registry/ -../../server/utils/ -../../client/ diff --git a/packages/playwright-core/src/mcp/browser/tools/DEPS.list b/packages/playwright-core/src/mcp/browser/tools/DEPS.list deleted file mode 100644 index 0a00935d43195..0000000000000 --- a/packages/playwright-core/src/mcp/browser/tools/DEPS.list +++ /dev/null @@ -1,11 +0,0 @@ -[*] -../../../../ -../ -../../sdk/ -../../../utils/isomorphic/ -../../../utilsBundle.ts -../../../mcpBundle.ts -../../../server/ -../../../server/registry/ -../../../server/utils/ -../../../client/ diff --git a/packages/playwright-core/src/mcp/browser/browserContextFactory.ts b/packages/playwright-core/src/mcp/browserContextFactory.ts similarity index 93% rename from packages/playwright-core/src/mcp/browser/browserContextFactory.ts rename to packages/playwright-core/src/mcp/browserContextFactory.ts index 4f4c620562c54..3a7d98faa9093 100644 --- a/packages/playwright-core/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright-core/src/mcp/browserContextFactory.ts @@ -19,16 +19,15 @@ import fs from 'fs'; import net from 'net'; import path from 'path'; -import * as playwright from '../../..'; -import { registryDirectory } from '../../server/registry/index'; -import { startTraceViewerServer } from '../../server'; -import { testDebug } from '../log'; -import { outputDir, outputFile } from './context'; -import { firstRootPath } from '../sdk/server'; +import * as playwright from '../..'; +import { registryDirectory } from '../server/registry/index'; +import { startTraceViewerServer } from '../server'; +import { testDebug } from './log'; +import { outputDir, outputFile } from '../tools/context'; import type { FullConfig } from './config'; -import type { LaunchOptions, BrowserContextOptions } from '../../client/types'; -import type { ClientInfo } from '../sdk/server'; +import type { LaunchOptions, BrowserContextOptions } from '../client/types'; +import type { ClientInfo } from './sdk/server'; export function contextFactory(config: FullConfig): BrowserContextFactory { if (config.browser.remoteEndpoint) @@ -216,7 +215,7 @@ class PersistentContextFactory extends BaseContextFactory { const dir = process.env.PWMCP_PROFILES_DIR_FOR_TEST ?? registryDirectory; const browserToken = this.config.browser.launchOptions?.channel ?? this.config.browser?.browserName; // Hesitant putting hundreds of files into the user's workspace, so using it for hashing instead. - const rootPathToken = createHash(firstRootPath(clientInfo)); + const rootPathToken = createHash(clientInfo.cwd); const result = path.join(dir, `mcp-${browserToken}-${rootPathToken}`); await fs.promises.mkdir(result, { recursive: true }); return result; @@ -255,15 +254,13 @@ function createHash(data: string): string { } async function computeTracesDir(config: FullConfig, clientInfo: ClientInfo): Promise { - const cwd = firstRootPath(clientInfo); - return path.resolve(outputDir({ config, cwd }), 'traces'); + return path.resolve(outputDir({ config, cwd: clientInfo.cwd }), 'traces'); } async function browserContextOptionsFromConfig(config: FullConfig, clientInfo: ClientInfo): Promise { const result = { ...config.browser.contextOptions }; if (config.saveVideo) { - const cwd = firstRootPath(clientInfo); - const dir = await outputFile({ config, cwd }, `videos`, { origin: 'code' }); + const dir = await outputFile({ config, cwd: clientInfo.cwd }, `videos`, { origin: 'code' }); result.recordVideo = { dir, size: config.saveVideo, diff --git a/packages/playwright-core/src/mcp/extension/cdpRelay.ts b/packages/playwright-core/src/mcp/cdpRelay.ts similarity index 96% rename from packages/playwright-core/src/mcp/extension/cdpRelay.ts rename to packages/playwright-core/src/mcp/cdpRelay.ts index 2dd5a064f0f8c..1e78ed6d2b2ee 100644 --- a/packages/playwright-core/src/mcp/extension/cdpRelay.ts +++ b/packages/playwright-core/src/mcp/cdpRelay.ts @@ -26,18 +26,18 @@ import { spawn } from 'child_process'; import http from 'http'; import os from 'os'; -import { debug, ws, wsServer } from '../../utilsBundle'; -import { registry } from '../../server/registry/index'; -import { ManualPromise } from '../../utils/isomorphic/manualPromise'; +import { debug, ws, wsServer } from '../utilsBundle'; +import { registry } from '../server/registry/index'; +import { ManualPromise } from '../utils/isomorphic/manualPromise'; -import { addressToString } from '../sdk/http'; -import { logUnhandledError } from '../log'; +import { addressToString } from './sdk/http'; +import { logUnhandledError } from './log'; import * as protocol from './protocol'; import type websocket from 'ws'; -import type { ClientInfo } from '../sdk/server'; +import type { ClientInfo } from './sdk/server'; import type { ExtensionCommand, ExtensionEvents } from './protocol'; -import type { WebSocket, WebSocketServer } from '../../utilsBundle'; +import type { WebSocket, WebSocketServer } from '../utilsBundle'; const debugLogger = debug('pw:mcp:relay'); @@ -120,8 +120,8 @@ export class CDPRelayServer { const url = new URL('chrome-extension://mmlmfjhmonkocbjadbfplnigmagldckm/connect.html'); url.searchParams.set('mcpRelayUrl', mcpRelayEndpoint); const client = { - name: clientInfo.name, - version: clientInfo.version, + name: 'Playwright Agent', + version: require('../../../package.json').version, }; url.searchParams.set('client', JSON.stringify(client)); url.searchParams.set('protocolVersion', process.env.PWMCP_TEST_PROTOCOL_VERSION ?? protocol.VERSION.toString()); diff --git a/packages/playwright-core/src/mcp/browser/config.ts b/packages/playwright-core/src/mcp/config.ts similarity index 98% rename from packages/playwright-core/src/mcp/browser/config.ts rename to packages/playwright-core/src/mcp/config.ts index 304b8b632c095..f0d96d693bb28 100644 --- a/packages/playwright-core/src/mcp/browser/config.ts +++ b/packages/playwright-core/src/mcp/config.ts @@ -17,14 +17,14 @@ import fs from 'fs'; import os from 'os'; -import { registry } from '../../server'; -import { devices } from '../../..'; -import { dotenv } from '../../utilsBundle'; +import { registry } from '../server'; +import { devices } from '../..'; +import { dotenv } from '../utilsBundle'; import { configFromIniFile } from './configIni'; -import type * as playwright from '../../..'; -import type { Config, ToolCapability } from '../config'; +import type * as playwright from '../..'; +import type { Config, ToolCapability } from './config.d'; async function fileExistsAsync(resolved: string) { try { return (await fs.promises.stat(resolved)).isFile(); } catch { return false; } diff --git a/packages/playwright-core/src/mcp/browser/configIni.ts b/packages/playwright-core/src/mcp/configIni.ts similarity index 98% rename from packages/playwright-core/src/mcp/browser/configIni.ts rename to packages/playwright-core/src/mcp/configIni.ts index 6a90f9902ead9..966e4ab617c9b 100644 --- a/packages/playwright-core/src/mcp/browser/configIni.ts +++ b/packages/playwright-core/src/mcp/configIni.ts @@ -16,9 +16,9 @@ import fs from 'fs'; -import { ini } from '../../utilsBundle'; +import { ini } from '../utilsBundle'; -import type { Config } from '../config'; +import type { Config } from './config.d'; export function configFromIniFile(filePath: string): Config { const content = fs.readFileSync(filePath, 'utf8'); diff --git a/packages/playwright-core/src/mcp/exports.ts b/packages/playwright-core/src/mcp/exports.ts index 1d3ae9d1288bc..a4352586315b3 100644 --- a/packages/playwright-core/src/mcp/exports.ts +++ b/packages/playwright-core/src/mcp/exports.ts @@ -14,18 +14,11 @@ * limitations under the License. */ -// SDK -export * from './sdk/server'; -export * from './sdk/tool'; -export { browserTools } from './browser/tools'; -export { BrowserServerBackend } from './browser/browserServerBackend'; -export { parseResponse } from './browser/response'; -export { Tab } from './browser/tab'; -export { setupExitWatchdog } from './browser/watchdog'; -export type { Tool as BrowserTool } from './browser/tools/tool'; +export { createClientInfo } from '../cli/client/registry'; export { logUnhandledError } from './log'; -export type { ContextConfig } from './browser/context'; -export type { FullConfig } from './browser/config'; +export { setupExitWatchdog } from './watchdog'; export { startMcpDaemonServer } from '../cli/daemon/daemon'; -export { createClientInfo } from '../cli/client/registry'; -export { filteredTools } from './browser/tools'; +export * from './sdk/server'; +export * from './sdk/tool'; + +export type { FullConfig } from './config'; diff --git a/packages/playwright-core/src/mcp/extension/DEPS.list b/packages/playwright-core/src/mcp/extension/DEPS.list deleted file mode 100644 index d40e3620f39a7..0000000000000 --- a/packages/playwright-core/src/mcp/extension/DEPS.list +++ /dev/null @@ -1,10 +0,0 @@ -[*] -../../../ -../sdk/ -../browser/ -../log.ts -../../utilsBundle.ts -../../utils/isomorphic/ -../../server/ -../../server/registry/ -../../server/utils/ diff --git a/packages/playwright-core/src/mcp/extension/extensionContextFactory.ts b/packages/playwright-core/src/mcp/extensionContextFactory.ts similarity index 88% rename from packages/playwright-core/src/mcp/extension/extensionContextFactory.ts rename to packages/playwright-core/src/mcp/extensionContextFactory.ts index 741c6b051132a..b4b52ebba447d 100644 --- a/packages/playwright-core/src/mcp/extension/extensionContextFactory.ts +++ b/packages/playwright-core/src/mcp/extensionContextFactory.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import * as playwright from '../../..'; -import { debug } from '../../utilsBundle'; -import { createHttpServer, startHttpServer } from '../../server/utils/network'; +import * as playwright from '../..'; +import { debug } from '../utilsBundle'; +import { createHttpServer, startHttpServer } from '../server/utils/network'; import { CDPRelayServer } from './cdpRelay'; -import type { BrowserContextFactory } from '../browser/browserContextFactory'; -import type { ClientInfo } from '../sdk/server'; +import type { BrowserContextFactory } from './browserContextFactory'; +import type { ClientInfo } from './sdk/server'; const debugLogger = debug('pw:mcp:relay'); diff --git a/packages/playwright-core/src/mcp/index.ts b/packages/playwright-core/src/mcp/index.ts index 228432f0f45d7..e0179e986084a 100644 --- a/packages/playwright-core/src/mcp/index.ts +++ b/packages/playwright-core/src/mcp/index.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import { resolveConfig } from './browser/config'; -import { filteredTools } from './browser/tools'; -import { contextFactory } from './browser/browserContextFactory'; -import { BrowserServerBackend } from './browser/browserServerBackend'; +import { resolveConfig } from './config'; +import { filteredTools } from '../tools/tools'; +import { contextFactory } from './browserContextFactory'; +import { BrowserServerBackend } from '../tools/browserServerBackend'; import { createServer } from './sdk/server'; -import type { BrowserContextFactory } from './browser/browserContextFactory'; +import type { BrowserContextFactory } from './browserContextFactory'; import type { BrowserContext } from 'playwright'; import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; import type { ClientInfo, ServerBackendFactory } from './sdk/server'; -import type { Config } from './config'; +import type { Config } from './config.d'; const packageJSON = require('../../package.json'); diff --git a/packages/playwright-core/src/mcp/program.ts b/packages/playwright-core/src/mcp/program.ts index 1ec90c6653412..b165d3db38f74 100644 --- a/packages/playwright-core/src/mcp/program.ts +++ b/packages/playwright-core/src/mcp/program.ts @@ -17,12 +17,12 @@ import { ProgramOption } from '../utilsBundle'; import * as mcpServer from './sdk/server'; -import { commaSeparatedList, dotenvFileLoader, enumParser, headerParser, numberParser, resolutionParser, resolveCLIConfig, semicolonSeparatedList } from './browser/config'; -import { setupExitWatchdog } from './browser/watchdog'; -import { contextFactory } from './browser/browserContextFactory'; -import { BrowserServerBackend } from './browser/browserServerBackend'; -import { ExtensionContextFactory } from './extension/extensionContextFactory'; -import { filteredTools } from './browser/tools'; +import { commaSeparatedList, dotenvFileLoader, enumParser, headerParser, numberParser, resolutionParser, resolveCLIConfig, semicolonSeparatedList } from './config'; +import { setupExitWatchdog } from './watchdog'; +import { contextFactory } from './browserContextFactory'; +import { BrowserServerBackend } from '../tools/browserServerBackend'; +import { ExtensionContextFactory } from './extensionContextFactory'; +import { filteredTools } from '../tools/tools'; import { testDebug } from './log'; import type { Command } from '../utilsBundle'; diff --git a/packages/playwright-core/src/mcp/extension/protocol.ts b/packages/playwright-core/src/mcp/protocol.ts similarity index 100% rename from packages/playwright-core/src/mcp/extension/protocol.ts rename to packages/playwright-core/src/mcp/protocol.ts diff --git a/packages/playwright-core/src/mcp/sdk/server.ts b/packages/playwright-core/src/mcp/sdk/server.ts index dee2f9bb7249c..97f34cbf04869 100644 --- a/packages/playwright-core/src/mcp/sdk/server.ts +++ b/packages/playwright-core/src/mcp/sdk/server.ts @@ -33,10 +33,7 @@ const serverDebug = debug('pw:mcp:server'); const serverDebugResponse = debug('pw:mcp:server:response'); export type ClientInfo = { - name: string; - version: string; - roots: Root[]; - timestamp: number; + cwd: string; }; export type ProgressParams = { message?: string, progress?: number, total?: number }; @@ -160,10 +157,7 @@ const initializeServer = async (server: Server, factory: ServerBackendFactory, r } const clientInfo: ClientInfo = { - name: server.getClientVersion()?.name ?? 'unknown', - version: server.getClientVersion()?.version ?? 'unknown', - roots: clientRoots, - timestamp: Date.now(), + cwd: firstRootPath(clientRoots), }; const backend = await backendManager.createBackend(factory, clientInfo); @@ -217,13 +211,13 @@ export async function start(serverBackendFactory: ServerBackendFactory, options: console.error(message); } -export function firstRootPath(clientInfo: ClientInfo): string { - return allRootPaths(clientInfo)[0]; +export function firstRootPath(roots: Root[]): string { + return allRootPaths(roots)[0]; } -export function allRootPaths(clientInfo: ClientInfo): string[] { +export function allRootPaths(roots: Root[]): string[] { const paths: string[] = []; - for (const root of clientInfo.roots) { + for (const root of roots) { const url = new URL(root.uri); let rootPath; try { diff --git a/packages/playwright-core/src/mcp/browser/watchdog.ts b/packages/playwright-core/src/mcp/watchdog.ts similarity index 91% rename from packages/playwright-core/src/mcp/browser/watchdog.ts rename to packages/playwright-core/src/mcp/watchdog.ts index f4d28b4caa24f..dc5860f8ae887 100644 --- a/packages/playwright-core/src/mcp/browser/watchdog.ts +++ b/packages/playwright-core/src/mcp/watchdog.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { gracefullyCloseAll, gracefullyCloseSet } from '../../utils'; -import { testDebug } from '../log'; +import { gracefullyCloseAll, gracefullyCloseSet } from '../utils'; +import { testDebug } from './log'; export function setupExitWatchdog() { let isExiting = false; diff --git a/packages/playwright-core/src/tools/DEPS.list b/packages/playwright-core/src/tools/DEPS.list new file mode 100644 index 0000000000000..300b3fcbcd84e --- /dev/null +++ b/packages/playwright-core/src/tools/DEPS.list @@ -0,0 +1,7 @@ +[*] +../.. +../mcpBundle.ts +../utils/ +../utils/isomorphic/ +../utilsBundle.ts +../client/ diff --git a/packages/playwright-core/src/mcp/browser/browserServerBackend.ts b/packages/playwright-core/src/tools/browserServerBackend.ts similarity index 81% rename from packages/playwright-core/src/mcp/browser/browserServerBackend.ts rename to packages/playwright-core/src/tools/browserServerBackend.ts index 5b005d5a0c21e..529493674e777 100644 --- a/packages/playwright-core/src/mcp/browser/browserServerBackend.ts +++ b/packages/playwright-core/src/tools/browserServerBackend.ts @@ -17,15 +17,13 @@ import { Context } from './context'; import { Response } from './response'; import { SessionLog } from './sessionLog'; -import { toMcpTool } from '../sdk/tool'; -import { logUnhandledError } from '../log'; -import { firstRootPath } from '../sdk/server'; +import { debug } from '../utilsBundle'; import type { ContextConfig } from './context'; -import type * as playwright from '../../..'; -import type { Tool } from './tools/tool'; -import type * as mcpServer from '../sdk/server'; -import type { ClientInfo, ServerBackend } from '../sdk/server'; +import type * as playwright from '../../types/types'; +import type { Tool } from './tool'; +import type * as mcpServer from '../mcp/sdk/server'; +import type { ClientInfo, ServerBackend } from '../mcp/sdk/server'; export class BrowserServerBackend implements ServerBackend { private _tools: Tool[]; @@ -41,21 +39,16 @@ export class BrowserServerBackend implements ServerBackend { } async initialize(clientInfo: ClientInfo): Promise { - const cwd = firstRootPath(clientInfo); - this._sessionLog = this._config.saveSession ? await SessionLog.create(this._config, cwd) : undefined; + this._sessionLog = this._config.saveSession ? await SessionLog.create(this._config, clientInfo.cwd) : undefined; this._context = new Context(this.browserContext, { config: this._config, sessionLog: this._sessionLog, - cwd, + cwd: clientInfo.cwd, }); } async dispose() { - await this._context?.dispose().catch(logUnhandledError); - } - - async listTools(): Promise { - return this._tools.map(tool => toMcpTool(tool.schema)); + await this._context?.dispose().catch(e => debug('pw:tools:error')(e)); } async callTool(name: string, rawArguments: mcpServer.CallToolRequest['params']['arguments']) { diff --git a/packages/playwright-core/src/mcp/browser/tools/common.ts b/packages/playwright-core/src/tools/common.ts similarity index 95% rename from packages/playwright-core/src/mcp/browser/tools/common.ts rename to packages/playwright-core/src/tools/common.ts index f1ae396a5ce78..58cb58e03f68a 100644 --- a/packages/playwright-core/src/mcp/browser/tools/common.ts +++ b/packages/playwright-core/src/tools/common.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTabTool, defineTool } from './tool'; -import { renderTabsMarkdown } from '../response'; +import { renderTabsMarkdown } from './response'; const close = defineTool({ capability: 'core', diff --git a/packages/playwright-core/src/mcp/browser/tools/config.ts b/packages/playwright-core/src/tools/config.ts similarity index 96% rename from packages/playwright-core/src/mcp/browser/tools/config.ts rename to packages/playwright-core/src/tools/config.ts index 2a3e979143328..78ddea14a1fbf 100644 --- a/packages/playwright-core/src/mcp/browser/tools/config.ts +++ b/packages/playwright-core/src/tools/config.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTool } from './tool'; const configShow = defineTool({ diff --git a/packages/playwright-core/src/mcp/browser/tools/console.ts b/packages/playwright-core/src/tools/console.ts similarity index 98% rename from packages/playwright-core/src/mcp/browser/tools/console.ts rename to packages/playwright-core/src/tools/console.ts index bc7013fe74b3d..be6ed7cd6d488 100644 --- a/packages/playwright-core/src/mcp/browser/tools/console.ts +++ b/packages/playwright-core/src/tools/console.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTabTool } from './tool'; const console = defineTabTool({ diff --git a/packages/playwright-core/src/mcp/browser/context.ts b/packages/playwright-core/src/tools/context.ts similarity index 90% rename from packages/playwright-core/src/mcp/browser/context.ts rename to packages/playwright-core/src/tools/context.ts index 8f0a40755cdca..992d7f7ecef53 100644 --- a/packages/playwright-core/src/mcp/browser/context.ts +++ b/packages/playwright-core/src/tools/context.ts @@ -17,44 +17,53 @@ import fs from 'fs'; import path from 'path'; -import { disposeAll } from '../../client/disposable'; -import { eventsHelper } from '../../client/eventEmitter'; -import { debug } from '../../utilsBundle'; -import { escapeWithQuotes } from '../../utils/isomorphic/stringUtils'; -import { selectors } from '../../..'; +import { disposeAll } from '../client/disposable'; +import { eventsHelper } from '../client/eventEmitter'; +import { debug } from '../utilsBundle'; +import { escapeWithQuotes } from '../utils/isomorphic/stringUtils'; +import { selectors } from '../..'; import { Tab } from './tab'; -import type * as playwright from '../../..'; +import type * as playwright from '../..'; import type { SessionLog } from './sessionLog'; -import type { Tracing } from '../../client/tracing'; -import type { Disposable } from '../../client/disposable'; -import type { BrowserContext } from '../../client/browserContext'; -import type { Config } from '../config.d.ts'; +import type { Tracing } from '../client/tracing'; +import type { Disposable } from '../client/disposable'; +import type { BrowserContext } from '../client/browserContext'; +import type { ToolCapability } from './tool'; const testDebug = debug('pw:mcp:test'); -export type ContextConfig = Pick & { - browser?: { - initScript?: string[]; - initPage?: string[]; - }; - skillMode?: boolean; +export type ContextConfig = { + allowUnrestrictedFileAccess?: boolean; + capabilities?: ToolCapability[]; + codegen?: 'typescript' | 'none'; + console?: { level?: 'error' | 'warning' | 'info' | 'debug' }; + imageResponses?: 'allow' | 'omit'; + network?: { + allowedOrigins?: string[]; + blockedOrigins?: string[]; + }; + outputDir?: string; + outputMode?: 'file' | 'stdout'; + saveSession?: boolean; + saveTrace?: boolean; + secrets?: Record; + snapshot?: { + mode?: 'incremental' | 'full' | 'none'; + }; + testIdAttribute?: string; + timeouts?: { + action?: number; + navigation?: number; + expect?: number; }; + browser?: { + initScript?: string[]; + initPage?: string[]; + }; + skillMode?: boolean; +}; type ContextOptions = { config: ContextConfig; diff --git a/packages/playwright-core/src/mcp/browser/tools/cookies.ts b/packages/playwright-core/src/tools/cookies.ts similarity index 99% rename from packages/playwright-core/src/mcp/browser/tools/cookies.ts rename to packages/playwright-core/src/tools/cookies.ts index 74b49da851594..2a123fb94a7bb 100644 --- a/packages/playwright-core/src/mcp/browser/tools/cookies.ts +++ b/packages/playwright-core/src/tools/cookies.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTool } from './tool'; const cookieList = defineTool({ diff --git a/packages/playwright-core/src/mcp/browser/tools/devtools.ts b/packages/playwright-core/src/tools/devtools.ts similarity index 96% rename from packages/playwright-core/src/mcp/browser/tools/devtools.ts rename to packages/playwright-core/src/tools/devtools.ts index b5bea924424cd..7aabe63d19df5 100644 --- a/packages/playwright-core/src/mcp/browser/tools/devtools.ts +++ b/packages/playwright-core/src/tools/devtools.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTool } from './tool'; const devtoolsConnect = defineTool({ diff --git a/packages/playwright-core/src/mcp/browser/tools/dialogs.ts b/packages/playwright-core/src/tools/dialogs.ts similarity index 97% rename from packages/playwright-core/src/mcp/browser/tools/dialogs.ts rename to packages/playwright-core/src/tools/dialogs.ts index 8f741a921078a..cdfde818ecac1 100644 --- a/packages/playwright-core/src/mcp/browser/tools/dialogs.ts +++ b/packages/playwright-core/src/tools/dialogs.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTabTool } from './tool'; export const handleDialog = defineTabTool({ diff --git a/packages/playwright-core/src/mcp/browser/tools/evaluate.ts b/packages/playwright-core/src/tools/evaluate.ts similarity index 93% rename from packages/playwright-core/src/mcp/browser/tools/evaluate.ts rename to packages/playwright-core/src/tools/evaluate.ts index 47c61b2e73786..e71817a986638 100644 --- a/packages/playwright-core/src/mcp/browser/tools/evaluate.ts +++ b/packages/playwright-core/src/tools/evaluate.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; -import { escapeWithQuotes } from '../../../utils/isomorphic/stringUtils'; +import { z } from '../mcpBundle'; +import { escapeWithQuotes } from '../utils/isomorphic/stringUtils'; import { defineTabTool } from './tool'; -import type { Tab } from '../tab'; +import type { Tab } from './tab'; const evaluateSchema = z.object({ function: z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'), diff --git a/packages/playwright-core/src/tools/exports.ts b/packages/playwright-core/src/tools/exports.ts new file mode 100644 index 0000000000000..f6926f010e3bd --- /dev/null +++ b/packages/playwright-core/src/tools/exports.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { BrowserServerBackend } from './browserServerBackend'; +export { browserTools } from './tools'; +export { filteredTools } from './tools'; +export { parseResponse } from './response'; +export { Tab } from './tab'; + +export type { ContextConfig } from './context'; +export type { Tool as BrowserTool } from './tool'; diff --git a/packages/playwright-core/src/mcp/browser/tools/files.ts b/packages/playwright-core/src/tools/files.ts similarity index 97% rename from packages/playwright-core/src/mcp/browser/tools/files.ts rename to packages/playwright-core/src/tools/files.ts index e85d79ed93b9b..e9265fb5ced0b 100644 --- a/packages/playwright-core/src/mcp/browser/tools/files.ts +++ b/packages/playwright-core/src/tools/files.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTabTool } from './tool'; export const uploadFile = defineTabTool({ diff --git a/packages/playwright-core/src/mcp/browser/tools/form.ts b/packages/playwright-core/src/tools/form.ts similarity index 95% rename from packages/playwright-core/src/mcp/browser/tools/form.ts rename to packages/playwright-core/src/tools/form.ts index 17b7ec136e8c5..6d2f536660b2c 100644 --- a/packages/playwright-core/src/mcp/browser/tools/form.ts +++ b/packages/playwright-core/src/tools/form.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; -import { escapeWithQuotes } from '../../../utils/isomorphic/stringUtils'; +import { z } from '../mcpBundle'; +import { escapeWithQuotes } from '../utils/isomorphic/stringUtils'; import { defineTabTool } from './tool'; diff --git a/packages/playwright-core/src/mcp/browser/tools/keyboard.ts b/packages/playwright-core/src/tools/keyboard.ts similarity index 99% rename from packages/playwright-core/src/mcp/browser/tools/keyboard.ts rename to packages/playwright-core/src/tools/keyboard.ts index 8b688e7d8ee6a..4c1cbf44e583a 100644 --- a/packages/playwright-core/src/mcp/browser/tools/keyboard.ts +++ b/packages/playwright-core/src/tools/keyboard.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTabTool } from './tool'; import { elementSchema } from './snapshot'; diff --git a/packages/playwright-core/src/mcp/browser/logFile.ts b/packages/playwright-core/src/tools/logFile.ts similarity index 96% rename from packages/playwright-core/src/mcp/browser/logFile.ts rename to packages/playwright-core/src/tools/logFile.ts index cc90262dd847d..89e8d4670ac8a 100644 --- a/packages/playwright-core/src/mcp/browser/logFile.ts +++ b/packages/playwright-core/src/tools/logFile.ts @@ -17,7 +17,7 @@ import fs from 'fs'; import path from 'path'; -import { logUnhandledError } from '../log'; +import { debug } from '../utilsBundle'; import type { Context } from './context'; @@ -53,7 +53,7 @@ export class LogFile { } appendLine(wallTime: number, text: () => string) { - this._writeChain = this._writeChain.then(() => this._write(wallTime, text)).catch(logUnhandledError); + this._writeChain = this._writeChain.then(() => this._write(wallTime, text)).catch(e => debug('pw:tools:error')(e)); } stop() { diff --git a/packages/playwright-core/src/mcp/browser/tools/mouse.ts b/packages/playwright-core/src/tools/mouse.ts similarity index 98% rename from packages/playwright-core/src/mcp/browser/tools/mouse.ts rename to packages/playwright-core/src/tools/mouse.ts index b3a9565085c17..eb597edde76d6 100644 --- a/packages/playwright-core/src/mcp/browser/tools/mouse.ts +++ b/packages/playwright-core/src/tools/mouse.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; -import { formatObjectOrVoid } from '../../../utils/isomorphic/stringUtils'; +import { z } from '../mcpBundle'; +import { formatObjectOrVoid } from '../utils/isomorphic/stringUtils'; import { defineTabTool } from './tool'; const mouseMove = defineTabTool({ diff --git a/packages/playwright-core/src/mcp/browser/tools/navigate.ts b/packages/playwright-core/src/tools/navigate.ts similarity index 98% rename from packages/playwright-core/src/mcp/browser/tools/navigate.ts rename to packages/playwright-core/src/tools/navigate.ts index 8d947db504757..773b1b0467760 100644 --- a/packages/playwright-core/src/mcp/browser/tools/navigate.ts +++ b/packages/playwright-core/src/tools/navigate.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTool, defineTabTool } from './tool'; const navigate = defineTool({ diff --git a/packages/playwright-core/src/mcp/browser/tools/network.ts b/packages/playwright-core/src/tools/network.ts similarity index 97% rename from packages/playwright-core/src/mcp/browser/tools/network.ts rename to packages/playwright-core/src/tools/network.ts index 361d22fb0a849..785cc5847b587 100644 --- a/packages/playwright-core/src/mcp/browser/tools/network.ts +++ b/packages/playwright-core/src/tools/network.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTool, defineTabTool } from './tool'; -import type * as playwright from '../../../..'; +import type * as playwright from '../../types/types'; const requests = defineTabTool({ capability: 'core', diff --git a/packages/playwright-core/src/mcp/browser/tools/pdf.ts b/packages/playwright-core/src/tools/pdf.ts similarity index 93% rename from packages/playwright-core/src/mcp/browser/tools/pdf.ts rename to packages/playwright-core/src/tools/pdf.ts index 779e9060399f8..182e66d829d0b 100644 --- a/packages/playwright-core/src/mcp/browser/tools/pdf.ts +++ b/packages/playwright-core/src/tools/pdf.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; -import { formatObject } from '../../../utils/isomorphic/stringUtils'; +import { z } from '../mcpBundle'; +import { formatObject } from '../utils/isomorphic/stringUtils'; import { defineTabTool } from './tool'; diff --git a/packages/playwright-core/src/mcp/browser/response.ts b/packages/playwright-core/src/tools/response.ts similarity index 99% rename from packages/playwright-core/src/mcp/browser/response.ts rename to packages/playwright-core/src/tools/response.ts index e15f6ba8f2661..2bf41e8d453c0 100644 --- a/packages/playwright-core/src/mcp/browser/response.ts +++ b/packages/playwright-core/src/tools/response.ts @@ -17,9 +17,9 @@ import fs from 'fs'; import path from 'path'; -import { debug } from '../../utilsBundle'; +import { debug } from '../utilsBundle'; import { renderModalStates, shouldIncludeMessage } from './tab'; -import { scaleImageToFitMessage } from './tools/screenshot'; +import { scaleImageToFitMessage } from './screenshot'; import type { TabHeader } from './tab'; import type { CallToolResult, ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js'; diff --git a/packages/playwright-core/src/mcp/browser/tools/route.ts b/packages/playwright-core/src/tools/route.ts similarity index 97% rename from packages/playwright-core/src/mcp/browser/tools/route.ts rename to packages/playwright-core/src/tools/route.ts index 57df2fa3e59f8..2e722e39b4906 100644 --- a/packages/playwright-core/src/mcp/browser/tools/route.ts +++ b/packages/playwright-core/src/tools/route.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTool } from './tool'; -import type * as playwright from '../../../..'; -import type { RouteEntry } from '../context'; +import type * as playwright from '../../types/types'; +import type { RouteEntry } from './context'; const route = defineTool({ capability: 'network', diff --git a/packages/playwright-core/src/mcp/browser/tools/runCode.ts b/packages/playwright-core/src/tools/runCode.ts similarity index 94% rename from packages/playwright-core/src/mcp/browser/tools/runCode.ts rename to packages/playwright-core/src/tools/runCode.ts index 5b7d5eabeb130..82fc6d810c9c0 100644 --- a/packages/playwright-core/src/mcp/browser/tools/runCode.ts +++ b/packages/playwright-core/src/tools/runCode.ts @@ -16,9 +16,9 @@ import vm from 'vm'; -import { ManualPromise } from '../../../utils/isomorphic/manualPromise'; +import { ManualPromise } from '../utils/isomorphic/manualPromise'; -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTabTool } from './tool'; const codeSchema = z.object({ diff --git a/packages/playwright-core/src/mcp/browser/tools/screenshot.ts b/packages/playwright-core/src/tools/screenshot.ts similarity index 93% rename from packages/playwright-core/src/mcp/browser/tools/screenshot.ts rename to packages/playwright-core/src/tools/screenshot.ts index f66130c5128e0..a940cb62f1689 100644 --- a/packages/playwright-core/src/mcp/browser/tools/screenshot.ts +++ b/packages/playwright-core/src/tools/screenshot.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { scaleImageToSize } from '../../../utils/isomorphic/imageUtils'; -import { jpegjs, PNG } from '../../../utilsBundle'; -import { formatObject } from '../../../utils/isomorphic/stringUtils'; +import { scaleImageToSize } from '../utils/isomorphic/imageUtils'; +import { jpegjs, PNG } from '../utilsBundle'; +import { formatObject } from '../utils/isomorphic/stringUtils'; -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTabTool } from './tool'; -import type * as playwright from '../../../..'; +import type * as playwright from '../../types/types'; const screenshotSchema = z.object({ type: z.enum(['png', 'jpeg']).default('png').describe('Image format for the screenshot. Default is png.'), diff --git a/packages/playwright-core/src/mcp/browser/sessionLog.ts b/packages/playwright-core/src/tools/sessionLog.ts similarity index 100% rename from packages/playwright-core/src/mcp/browser/sessionLog.ts rename to packages/playwright-core/src/tools/sessionLog.ts diff --git a/packages/playwright-core/src/mcp/browser/tools/snapshot.ts b/packages/playwright-core/src/tools/snapshot.ts similarity index 98% rename from packages/playwright-core/src/mcp/browser/tools/snapshot.ts rename to packages/playwright-core/src/tools/snapshot.ts index 53ad0cf8ce872..6053505c23597 100644 --- a/packages/playwright-core/src/mcp/browser/tools/snapshot.ts +++ b/packages/playwright-core/src/tools/snapshot.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; -import { formatObject, formatObjectOrVoid } from '../../../utils/isomorphic/stringUtils'; +import { z } from '../mcpBundle'; +import { formatObject, formatObjectOrVoid } from '../utils/isomorphic/stringUtils'; import { defineTabTool, defineTool } from './tool'; diff --git a/packages/playwright-core/src/mcp/browser/tools/storage.ts b/packages/playwright-core/src/tools/storage.ts similarity index 98% rename from packages/playwright-core/src/mcp/browser/tools/storage.ts rename to packages/playwright-core/src/tools/storage.ts index 97a4eb876037d..d7668daebefc1 100644 --- a/packages/playwright-core/src/mcp/browser/tools/storage.ts +++ b/packages/playwright-core/src/tools/storage.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTool } from './tool'; const storageState = defineTool({ diff --git a/packages/playwright-core/src/mcp/browser/tab.ts b/packages/playwright-core/src/tools/tab.ts similarity index 96% rename from packages/playwright-core/src/mcp/browser/tab.ts rename to packages/playwright-core/src/tools/tab.ts index 4a8cc2b72d83d..7ecb8981b17a1 100644 --- a/packages/playwright-core/src/mcp/browser/tab.ts +++ b/packages/playwright-core/src/tools/tab.ts @@ -17,23 +17,23 @@ import url from 'url'; import { EventEmitter } from 'events'; -import * as playwright from '../../..'; -import { asLocator } from '../../utils/isomorphic/locatorGenerators'; -import { ManualPromise } from '../../utils/isomorphic/manualPromise'; +import { asLocator } from '../utils/isomorphic/locatorGenerators'; +import { ManualPromise } from '../utils/isomorphic/manualPromise'; +import { debug } from '../utilsBundle'; -import { eventsHelper } from '../../client/eventEmitter'; -import { callOnPageNoTrace, waitForCompletion, eventWaiter } from './tools/utils'; -import { logUnhandledError } from '../log'; +import { eventsHelper } from '../client/eventEmitter'; +import { callOnPageNoTrace, waitForCompletion, eventWaiter } from './utils'; import { LogFile } from './logFile'; -import { ModalState } from './tools/tool'; -import { handleDialog } from './tools/dialogs'; -import { uploadFile } from './tools/files'; -import { disposeAll } from '../../client/disposable'; +import { ModalState } from './tool'; +import { handleDialog } from './dialogs'; +import { uploadFile } from './files'; +import { disposeAll } from '../client/disposable'; -import type { Disposable } from '../../client/disposable'; +import type { Disposable } from '../client/disposable'; import type { Context, ContextConfig } from './context'; -import type { Page } from '../../client/page'; -import type { Locator } from '../../client/locator'; +import type { Page } from '../client/page'; +import type { Locator } from '../client/locator'; +import type * as playwright from '../../types/types'; const TabEvents = { modalState: 'modalState' @@ -174,7 +174,7 @@ export class Tab extends EventEmitter { const { default: func } = await import(url.pathToFileURL(initPage).href); await func({ page: this.page }); } catch (e) { - logUnhandledError(e); + debug('pw:tools:error')(e); } } } @@ -292,7 +292,7 @@ export class Tab extends EventEmitter { async waitForLoadState(state: 'load', options?: { timeout?: number }): Promise { await this._initializedPromise; - await callOnPageNoTrace(this.page, page => page.waitForLoadState(state, options).catch(logUnhandledError)); + await callOnPageNoTrace(this.page, page => page.waitForLoadState(state, options).catch(e => debug('pw:tools:error')(e))); } async navigate(url: string) { diff --git a/packages/playwright-core/src/mcp/browser/tools/tabs.ts b/packages/playwright-core/src/tools/tabs.ts similarity index 95% rename from packages/playwright-core/src/mcp/browser/tools/tabs.ts rename to packages/playwright-core/src/tools/tabs.ts index 85ff3aa8e8062..b308ae119a4e6 100644 --- a/packages/playwright-core/src/mcp/browser/tools/tabs.ts +++ b/packages/playwright-core/src/tools/tabs.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTool } from './tool'; -import { renderTabsMarkdown } from '../response'; +import { renderTabsMarkdown } from './response'; const browserTabs = defineTool({ capability: 'core-tabs', diff --git a/packages/playwright-core/src/mcp/browser/tools/tool.ts b/packages/playwright-core/src/tools/tool.ts similarity index 81% rename from packages/playwright-core/src/mcp/browser/tools/tool.ts rename to packages/playwright-core/src/tools/tool.ts index fa5b0e73c1d06..9c88db38e22ac 100644 --- a/packages/playwright-core/src/mcp/browser/tools/tool.ts +++ b/packages/playwright-core/src/tools/tool.ts @@ -15,12 +15,20 @@ */ import type { z } from 'zod'; -import type { Context } from '../context'; -import type * as playwright from '../../../..'; -import type { ToolCapability } from '../../config'; -import type { Tab } from '../tab'; -import type { Response } from '../response'; -import type { ToolSchema } from '../../sdk/tool'; +import type { Context } from './context'; +import type * as playwright from '../../types/types'; +import type { Tab } from './tab'; +import type { Response } from './response'; + +type ToolSchema = { + name: string; + title: string; + description: string; + inputSchema: Input; + type: 'input' | 'assertion' | 'action' | 'readOnly'; +}; + +export type ToolCapability = 'config' | 'core' | 'core-navigation' | 'core-tabs' | 'core-input' | 'core-install' | 'network' | 'pdf' | 'storage' | 'testing' | 'vision' | 'devtools'; export type FileUploadModalState = { type: 'fileChooser'; diff --git a/packages/playwright-core/src/mcp/browser/tools.ts b/packages/playwright-core/src/tools/tools.ts similarity index 56% rename from packages/playwright-core/src/mcp/browser/tools.ts rename to packages/playwright-core/src/tools/tools.ts index eea07dc6b5d72..ea7c4e5bd10f1 100644 --- a/packages/playwright-core/src/mcp/browser/tools.ts +++ b/packages/playwright-core/src/tools/tools.ts @@ -14,33 +14,33 @@ * limitations under the License. */ -import common from './tools/common'; -import config from './tools/config'; -import console from './tools/console'; -import cookies from './tools/cookies'; -import devtools from './tools/devtools'; -import dialogs from './tools/dialogs'; -import evaluate from './tools/evaluate'; -import files from './tools/files'; -import form from './tools/form'; -import keyboard from './tools/keyboard'; -import mouse from './tools/mouse'; -import navigate from './tools/navigate'; -import network from './tools/network'; -import pdf from './tools/pdf'; -import route from './tools/route'; -import runCode from './tools/runCode'; -import snapshot from './tools/snapshot'; -import screenshot from './tools/screenshot'; -import storage from './tools/storage'; -import tabs from './tools/tabs'; -import tracing from './tools/tracing'; -import verify from './tools/verify'; -import video from './tools/video'; -import wait from './tools/wait'; -import webstorage from './tools/webstorage'; +import common from './common'; +import config from './config'; +import console from './console'; +import cookies from './cookies'; +import devtools from './devtools'; +import dialogs from './dialogs'; +import evaluate from './evaluate'; +import files from './files'; +import form from './form'; +import keyboard from './keyboard'; +import mouse from './mouse'; +import navigate from './navigate'; +import network from './network'; +import pdf from './pdf'; +import route from './route'; +import runCode from './runCode'; +import snapshot from './snapshot'; +import screenshot from './screenshot'; +import storage from './storage'; +import tabs from './tabs'; +import tracing from './tracing'; +import verify from './verify'; +import video from './video'; +import wait from './wait'; +import webstorage from './webstorage'; -import type { Tool } from './tools/tool'; +import type { Tool } from './tool'; import type { ContextConfig } from './context'; export const browserTools: Tool[] = [ diff --git a/packages/playwright-core/src/mcp/browser/tools/tracing.ts b/packages/playwright-core/src/tools/tracing.ts similarity index 71% rename from packages/playwright-core/src/mcp/browser/tools/tracing.ts rename to packages/playwright-core/src/tools/tracing.ts index 61f89802c087c..606ab0ee33773 100644 --- a/packages/playwright-core/src/mcp/browser/tools/tracing.ts +++ b/packages/playwright-core/src/tools/tracing.ts @@ -14,12 +14,10 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; -import { runTraceViewerApp } from '../../../server'; -import { isUnderTest } from '../../../server/utils/debug'; +import { z } from '../mcpBundle'; import { defineTool } from './tool'; -import type { Tracing } from '../../../client/tracing'; +import type { Tracing } from '../client/tracing'; const tracingStart = defineTool({ capability: 'devtools', @@ -72,33 +70,9 @@ const tracingStop = defineTool({ }, }); -const tracingShow = defineTool({ - capability: 'devtools', - - schema: { - name: 'browser_show_tracing', - title: 'Show tracing', - description: 'Open trace viewer for the recorded trace', - inputSchema: z.object({}), - type: 'readOnly', - }, - - handle: async (context, params, response) => { - const browserContext = await context.ensureBrowserContext(); - const traceLegend = (browserContext.tracing as any)[traceLegendSymbol]; - if (!traceLegend) { - response.addError('No trace recording found. Start tracing first with browser_start_tracing.'); - return; - } - await runTraceViewerApp(`${traceLegend.tracesDir}/${traceLegend.name}.json`, 'chromium', { headless: isUnderTest() ? true : undefined }); - response.addTextResult('Trace viewer opened.'); - }, -}); - export default [ tracingStart, tracingStop, - tracingShow, ]; const traceLegendSymbol = Symbol('tracesDir'); diff --git a/packages/playwright-core/src/mcp/browser/tools/utils.ts b/packages/playwright-core/src/tools/utils.ts similarity index 97% rename from packages/playwright-core/src/mcp/browser/tools/utils.ts rename to packages/playwright-core/src/tools/utils.ts index d5d566e4a80d5..cd25bd1649b12 100644 --- a/packages/playwright-core/src/mcp/browser/tools/utils.ts +++ b/packages/playwright-core/src/tools/utils.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import type * as playwright from '../../../..'; -import type { Tab } from '../tab'; +import type * as playwright from '../../types/types'; +import type { Tab } from './tab'; export async function waitForCompletion(tab: Tab, callback: () => Promise): Promise { const requests: playwright.Request[] = []; diff --git a/packages/playwright-core/src/mcp/browser/tools/verify.ts b/packages/playwright-core/src/tools/verify.ts similarity index 98% rename from packages/playwright-core/src/mcp/browser/tools/verify.ts rename to packages/playwright-core/src/tools/verify.ts index f28fd0c3aa507..495d3ed9858ae 100644 --- a/packages/playwright-core/src/mcp/browser/tools/verify.ts +++ b/packages/playwright-core/src/tools/verify.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; -import { escapeWithQuotes } from '../../../utils/isomorphic/stringUtils'; +import { z } from '../mcpBundle'; +import { escapeWithQuotes } from '../utils/isomorphic/stringUtils'; import { defineTabTool } from './tool'; diff --git a/packages/playwright-core/src/mcp/browser/tools/video.ts b/packages/playwright-core/src/tools/video.ts similarity index 98% rename from packages/playwright-core/src/mcp/browser/tools/video.ts rename to packages/playwright-core/src/tools/video.ts index 02b19fd6f334f..698ccc9a83993 100644 --- a/packages/playwright-core/src/mcp/browser/tools/video.ts +++ b/packages/playwright-core/src/tools/video.ts @@ -15,7 +15,7 @@ */ import path from 'path'; -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTool } from './tool'; const startVideo = defineTool({ diff --git a/packages/playwright-core/src/mcp/browser/tools/wait.ts b/packages/playwright-core/src/tools/wait.ts similarity index 98% rename from packages/playwright-core/src/mcp/browser/tools/wait.ts rename to packages/playwright-core/src/tools/wait.ts index d0c913c768391..97bbe9876838b 100644 --- a/packages/playwright-core/src/mcp/browser/tools/wait.ts +++ b/packages/playwright-core/src/tools/wait.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTool } from './tool'; const wait = defineTool({ diff --git a/packages/playwright-core/src/mcp/browser/tools/webstorage.ts b/packages/playwright-core/src/tools/webstorage.ts similarity index 99% rename from packages/playwright-core/src/mcp/browser/tools/webstorage.ts rename to packages/playwright-core/src/tools/webstorage.ts index 9c41f11cfe850..5beacb1d62a93 100644 --- a/packages/playwright-core/src/mcp/browser/tools/webstorage.ts +++ b/packages/playwright-core/src/tools/webstorage.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../../../mcpBundle'; +import { z } from '../mcpBundle'; import { defineTabTool } from './tool'; const localStorageList = defineTabTool({ diff --git a/packages/playwright/src/DEPS.list b/packages/playwright/src/DEPS.list index 680cf5e8870e8..39705a210a7db 100644 --- a/packages/playwright/src/DEPS.list +++ b/packages/playwright/src/DEPS.list @@ -10,7 +10,6 @@ common/ @testIsomorphic/** ./prompt.ts ./worker/testTracing.ts -./mcp/browser/ ./mcp/sdk/ ./mcp/test/ ./transform/babelBundle.ts diff --git a/packages/playwright/src/agents/DEPS.list b/packages/playwright/src/agents/DEPS.list index 49f26daf1a5d6..c7b2b6f80192e 100644 --- a/packages/playwright/src/agents/DEPS.list +++ b/packages/playwright/src/agents/DEPS.list @@ -1,5 +1,4 @@ [*] -../mcp/browser/ ../mcp/sdk/ ../mcp/test/ ../common/ diff --git a/packages/playwright/src/mcp/test/browserBackend.ts b/packages/playwright/src/mcp/test/browserBackend.ts index fc36578fe8716..2f24b78060e6e 100644 --- a/packages/playwright/src/mcp/test/browserBackend.ts +++ b/packages/playwright/src/mcp/test/browserBackend.ts @@ -17,7 +17,7 @@ import path from 'path'; import { createGuid } from 'playwright-core/lib/utils'; import * as mcp from 'playwright-core/lib/mcp/exports'; -import { BrowserServerBackend } from 'playwright-core/lib/mcp/exports'; +import * as tools from 'playwright-core/lib/tools/exports'; import { stripAnsiEscapes } from '../../util'; @@ -40,14 +40,15 @@ export type BrowserMCPResponse = { }; export function createCustomMessageHandler(testInfo: TestInfoImpl, context: playwright.BrowserContext) { - let backend: BrowserServerBackend | undefined; + let backend: tools.BrowserServerBackend | undefined; + const config: tools.ContextConfig = { capabilities: ['testing'] }; + const toolList = tools.filteredTools(config); + return async (data: BrowserMCPRequest): Promise => { if (data.initialize) { if (backend) throw new Error('MCP backend is already initialized'); - const config: mcp.ContextConfig = { capabilities: ['testing'] }; - const tools = mcp.filteredTools(config); - backend = new BrowserServerBackend(config, context, tools); + backend = new tools.BrowserServerBackend(config, context, toolList); await backend.initialize(data.initialize.clientInfo); const pausedMessage = await generatePausedMessage(testInfo, context); return { initialize: { pausedMessage } }; @@ -56,7 +57,7 @@ export function createCustomMessageHandler(testInfo: TestInfoImpl, context: play if (data.listTools) { if (!backend) throw new Error('MCP backend is not initialized'); - return { listTools: await backend.listTools() }; + return { listTools: toolList.map(t => mcp.toMcpTool(t.schema)) }; } if (data.callTool) { @@ -96,7 +97,7 @@ async function generatePausedMessage(testInfo: TestInfoImpl, context: playwright `- Page Title: ${await page.title()}`.trim() ); // Only print console errors when pausing on error, not when everything works as expected. - let console = testInfo.errors.length ? await mcp.Tab.collectConsoleMessages(page) : []; + let console = testInfo.errors.length ? await tools.Tab.collectConsoleMessages(page) : []; console = console.filter(msg => msg.type === 'error'); if (console.length) { lines.push('- Console Messages:'); @@ -125,11 +126,6 @@ export async function runDaemonForContext(testInfo: TestInfoImpl, context: playw const outputDir = path.join(testInfo.artifactsDir(), '.playwright-mcp'); const sessionName = `test-worker-${createGuid().slice(0, 6)}`; await mcp.startMcpDaemonServer(sessionName, context, { - browser: { - browserName: 'chromium', - launchOptions: {}, - contextOptions: {}, - }, outputMode: 'file', snapshot: { mode: 'full' }, outputDir, diff --git a/packages/playwright/src/mcp/test/testBackend.ts b/packages/playwright/src/mcp/test/testBackend.ts index 40be367af5e3d..4ee3fd0800d9b 100644 --- a/packages/playwright/src/mcp/test/testBackend.ts +++ b/packages/playwright/src/mcp/test/testBackend.ts @@ -18,7 +18,7 @@ import EventEmitter from 'events'; import { z as zod } from 'playwright-core/lib/mcpBundle'; import * as mcp from 'playwright-core/lib/mcp/exports'; -import { browserTools } from 'playwright-core/lib/mcp/exports'; +import { browserTools } from 'playwright-core/lib/tools/exports'; import { TestContext } from './testContext'; import * as testTools from './testTools.js'; @@ -26,7 +26,7 @@ import * as generatorTools from './generatorTools.js'; import * as plannerTools from './plannerTools.js'; import type { TestTool } from './testTool'; -import type { BrowserTool } from 'playwright-core/lib/mcp/exports'; +import type { BrowserTool } from 'playwright-core/lib/tools/exports'; const typesWithIntent = ['action', 'assertion', 'input']; diff --git a/packages/playwright/src/mcp/test/testContext.ts b/packages/playwright/src/mcp/test/testContext.ts index 654ab0524a6e9..99f3423c558d4 100644 --- a/packages/playwright/src/mcp/test/testContext.ts +++ b/packages/playwright/src/mcp/test/testContext.ts @@ -19,7 +19,8 @@ import os from 'os'; import path from 'path'; import { noColors, escapeRegExp, ManualPromise, toPosixPath } from 'playwright-core/lib/utils'; -import { firstRootPath, parseResponse, logUnhandledError } from 'playwright-core/lib/mcp/exports'; +import { parseResponse } from 'playwright-core/lib/tools/exports'; +import { debug } from 'playwright-core/lib/utilsBundle'; import { terminalScreen } from '../../reporters/base'; import ListReporter from '../../reporters/list'; @@ -97,9 +98,8 @@ export class TestContext { constructor(clientInfo: ClientInfo, configPath: string | undefined, options?: { muteConsole?: boolean, headless?: boolean }) { this._clientInfo = clientInfo; - const rootPath = firstRootPath(clientInfo); - this._configLocation = resolveConfigLocation(configPath || rootPath); - this.rootPath = rootPath || this._configLocation.configDir; + this._configLocation = resolveConfigLocation(configPath || clientInfo.cwd); + this.rootPath = clientInfo.cwd || this._configLocation.configDir; if (options?.headless !== undefined) this.computedHeaded = !options.headless; @@ -250,7 +250,7 @@ export class TestContext { } async close() { - await this._cleanupTestRunner().catch(logUnhandledError); + await this._cleanupTestRunner().catch(e => debug('pw:mcp:error')(e)); } async sendMessageToPausedTest(request: BrowserMCPRequest): Promise { diff --git a/tests/mcp/cli-devtools.spec.ts b/tests/mcp/cli-devtools.spec.ts index dd546c378d212..8bdd8e3397004 100644 --- a/tests/mcp/cli-devtools.spec.ts +++ b/tests/mcp/cli-devtools.spec.ts @@ -91,16 +91,6 @@ test('tracing-start-stop', async ({ cli, server }, testInfo) => { expect(fs.existsSync(testInfo.outputPath('.playwright-cli', 'traces', `trace-${timestamp}.network`))).toBeTruthy(); }); -test('tracing-show', async ({ cli, server }) => { - await cli('open', server.HELLO_WORLD, { env: { PWTEST_UNDER_TEST: '1' } }); - const { output } = await cli('tracing-start'); - expect(output).toContain('Trace recording started'); - await cli('eval', '() => fetch("/hello-world")'); - - const { output: tracingShowOutput } = await cli('tracing-show'); - expect(tracingShowOutput).toContain('Trace viewer opened.'); -}); - test('video-start-stop', async ({ cli, server }) => { await cli('open', server.HELLO_WORLD); const { output: videoStartOutput } = await cli('video-start'); diff --git a/tests/mcp/cli-isolated.spec.ts b/tests/mcp/cli-isolated.spec.ts index 7d7b072841761..407d2f282aac0 100644 --- a/tests/mcp/cli-isolated.spec.ts +++ b/tests/mcp/cli-isolated.spec.ts @@ -30,7 +30,7 @@ test('should not save user data by default (in-memory mode)', async ({ cli, serv timestamp: expect.any(Number), version: expect.any(String), workspaceDir: testInfo.outputPath(), - resolvedConfig: expect.any(Object), + browser: expect.any(Object), }); const { output: listOutput } = await cli('list'); @@ -56,7 +56,7 @@ test('should save user data with --persistent flag', async ({ cli, server, mcpBr timestamp: expect.any(Number), version: expect.any(String), workspaceDir: testInfo.outputPath(), - resolvedConfig: expect.any(Object), + browser: expect.any(Object), }); }); @@ -74,6 +74,6 @@ test('should use custom user data dir with --profile=', async ({ cli, serve timestamp: expect.any(Number), version: expect.any(String), workspaceDir: testInfo.outputPath(), - resolvedConfig: expect.any(Object), + browser: expect.any(Object), }); }); diff --git a/tests/mcp/config.spec.ts b/tests/mcp/config.spec.ts index 28da967e72636..f0b6b4388cf82 100644 --- a/tests/mcp/config.spec.ts +++ b/tests/mcp/config.spec.ts @@ -17,8 +17,8 @@ import fs from 'node:fs'; import { test, expect, parseResponse } from './fixtures'; -import { resolveCLIConfig } from '../../packages/playwright-core/lib/mcp/browser/config'; -import type { Config } from '../../packages/playwright-core/src/mcp/config'; +import { resolveCLIConfig } from '../../packages/playwright-core/lib/mcp/config'; +import type { Config } from '../../packages/playwright-core/src/mcp/config.d'; test('config user data dir', async ({ startClient, server }, testInfo) => { server.setContent('/', ` diff --git a/tests/mcp/fixtures.ts b/tests/mcp/fixtures.ts index 7429ada6c0989..ddd1daeae2bf0 100644 --- a/tests/mcp/fixtures.ts +++ b/tests/mcp/fixtures.ts @@ -25,11 +25,11 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { ListRootsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { TestServer } from '../config/testserver'; import { serverFixtures } from '../config/serverFixtures'; -import { parseResponse } from '../../packages/playwright-core/lib/mcp/browser/response'; +import { parseResponse } from '../../packages/playwright-core/lib/tools/response'; import { commonFixtures } from '../config/commonFixtures'; import type { CommonFixtures, CommonWorkerFixtures } from '../config/commonFixtures'; -import type { Config } from '../../packages/playwright-core/src/mcp/config'; +import type { Config } from '../../packages/playwright-core/src/mcp/config.d'; import type { BrowserContext } from 'playwright'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import type { Stream } from 'stream'; diff --git a/tests/mcp/http.spec.ts b/tests/mcp/http.spec.ts index 3ab95d70fdabd..495d68198e6fa 100644 --- a/tests/mcp/http.spec.ts +++ b/tests/mcp/http.spec.ts @@ -23,7 +23,7 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { test as baseTest, expect, mcpServerPath, formatLog } from './fixtures'; -import type { Config } from '../../packages/playwright-core/src/mcp/config'; +import type { Config } from '../../packages/playwright-core/src/mcp/config.d'; import { ListRootsRequestSchema } from 'playwright-core/lib/mcpBundle'; const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({ diff --git a/tests/mcp/profile-lock.spec.ts b/tests/mcp/profile-lock.spec.ts index 69b780dbb4ebc..c5e0e173834f1 100644 --- a/tests/mcp/profile-lock.spec.ts +++ b/tests/mcp/profile-lock.spec.ts @@ -17,7 +17,7 @@ import { chromium } from 'playwright'; import { test, expect } from './fixtures'; -import { isProfileLocked } from '../../packages/playwright-core/lib/mcp/browser/browserContextFactory'; +import { isProfileLocked } from '../../packages/playwright-core/lib/mcp/browserContextFactory'; test('isProfileLocked returns false for empty directory', async ({ mcpBrowser }, testInfo) => { test.skip(!['chromium', 'chrome', 'msedge'].includes(mcpBrowser!), 'Chromium-only'); diff --git a/tests/mcp/sse.spec.ts b/tests/mcp/sse.spec.ts index ad0e390a95fe2..13c0ce63c67d5 100644 --- a/tests/mcp/sse.spec.ts +++ b/tests/mcp/sse.spec.ts @@ -21,7 +21,7 @@ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { test as baseTest, expect, mcpServerPath, formatLog } from './fixtures'; -import type { Config } from '../../packages/playwright-core/src/mcp/config'; +import type { Config } from '../../packages/playwright-core/src/mcp/config.d'; const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({ serverEndpoint: async ({ mcpHeadless }, use, testInfo) => { diff --git a/tests/mcp/tracing.spec.ts b/tests/mcp/tracing.spec.ts index c693fd59ea904..34aa9cb097064 100644 --- a/tests/mcp/tracing.spec.ts +++ b/tests/mcp/tracing.spec.ts @@ -69,40 +69,6 @@ test('check that trace is saved with browser_start_tracing', async ({ startClien ]); }); -test('check that browser_show_tracing returns error without tracing', async ({ startClient }) => { - const { client } = await startClient({ args: ['--caps=tracing'] }); - - expect(await client.callTool({ - name: 'browser_show_tracing', - })).toHaveResponse({ - error: expect.stringContaining('No trace recording found'), - isError: true, - }); -}); - -test('check that browser_show_tracing opens trace viewer', async ({ startClient, server }) => { - const { client } = await startClient({ args: ['--caps=tracing'], env: { PWTEST_UNDER_TEST: '1' } }); - - expect(await client.callTool({ - name: 'browser_start_tracing', - })).toHaveResponse({ - result: expect.stringContaining('Trace recording started'), - }); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - code: expect.stringContaining(`page.goto('http://localhost`), - }); - - expect(await client.callTool({ - name: 'browser_show_tracing', - })).toHaveResponse({ - result: 'Trace viewer opened.', - }); -}); - test('check that trace is saved with browser_start_tracing (no output dir)', async ({ startClient, server }, testInfo) => { const { client } = await startClient({ args: ['--caps=tracing'], diff --git a/utils/build/build.js b/utils/build/build.js index a5136484fc655..7d4ff162e5930 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -554,8 +554,8 @@ for (const webPackage of ['html-reporter', 'recorder', 'trace-viewer', 'devtools // Generate CLI help. onChanges.push({ inputs: [ - 'packages/playwright/src/mcp/terminal/commands.ts', - 'packages/playwright/src/mcp/terminal/helpGenerator.ts', + 'packages/playwright-core/src/cli/daemon/commands.ts', + 'packages/playwright-core/src/cli/daemon/helpGenerator.ts', 'utils/generate_cli_help.js', ], script: 'utils/generate_cli_help.js', @@ -654,7 +654,7 @@ copyFiles.push({ }); copyFiles.push({ - files: 'packages/playwright-core/src/cli/client/*.{png,ico}', + files: 'packages/playwright-core/src/devtools/*.{png,ico}', from: 'packages/playwright-core/src', to: 'packages/playwright-core/lib', });