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', });