chore: roll 1.59.0-alpha-1774903871000 (#1497)

Fixes https://github.com/microsoft/playwright-mcp/issues/1496
This commit is contained in:
Yury Semikhatsky
2026-03-30 15:56:05 -07:00
committed by GitHub
parent 9521308275
commit 3e8fc820a6
8 changed files with 117 additions and 79 deletions

View File

@@ -123,6 +123,14 @@ const test = base.extend<TestFixtures>({
},
});
function cliEnv() {
return {
PLAYWRIGHT_SERVER_REGISTRY: test.info().outputPath('registry'),
PLAYWRIGHT_DAEMON_SESSION_DIR: test.info().outputPath('daemon'),
PLAYWRIGHT_SOCKETS_DIR: path.join(test.info().project.outputDir, 'ds', String(test.info().parallelIndex)),
};
}
async function runCli(
args: string[],
options: { mcpBrowser?: string, testInfo: any },
@@ -133,7 +141,7 @@ async function runCli(
const testInfo = options.testInfo;
// Path to the terminal CLI
const cliPath = path.join(__dirname, '../../../node_modules/playwright/lib/cli/client/program.js');
const cliPath = path.join(__dirname, '../../../node_modules/playwright-core/lib/tools/cli-client/cli.js');
return new Promise<CliResult>((resolve, reject) => {
let stdout = '';
@@ -143,9 +151,7 @@ async function runCli(
cwd: testInfo.outputPath(),
env: {
...process.env,
PLAYWRIGHT_DAEMON_INSTALL_DIR: testInfo.outputPath(),
PLAYWRIGHT_DAEMON_SESSION_DIR: testInfo.outputPath('daemon'),
PLAYWRIGHT_DAEMON_SOCKETS_DIR: path.join(testInfo.project.outputDir, 'daemon-sockets'),
...cliEnv(),
PLAYWRIGHT_MCP_BROWSER: options.mcpBrowser,
PLAYWRIGHT_MCP_HEADLESS: 'false',
},
@@ -213,8 +219,7 @@ test(`navigate with extension`, async ({ browserWithExtension, startClient, serv
});
const selectorPage = await confirmationPagePromise;
// For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector
await selectorPage.getByRole('button', { name: 'Allow' }).click();
await selectorPage.locator('.tab-item', { hasText: 'Playwright MCP extension' }).getByRole('button', { name: 'Connect' }).click();
expect(await navigateResponse).toHaveResponse({
snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
@@ -295,8 +300,7 @@ testWithOldExtensionVersion(`works with old extension version`, async ({ browser
});
const selectorPage = await confirmationPagePromise;
// For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector
await selectorPage.getByRole('button', { name: 'Allow' }).click();
await selectorPage.locator('.tab-item', { hasText: 'Playwright MCP extension' }).getByRole('button', { name: 'Connect' }).click();
expect(await navigateResponse).toHaveResponse({
snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import type * as playwright from 'playwright';
import type * as playwright from '../../..';
export type ToolCapability =
'config' |
@@ -143,9 +143,9 @@ export type Config = {
sharedBrowserContext?: boolean;
/**
* Secrets are used to prevent LLM from getting sensitive data while
* automating scenarios such as authentication.
* Prefer the browser.contextOptions.storageState over secrets file as a more secure alternative.
* Secrets are used to replace matching plain text in the tool responses to prevent the LLM
* from accidentally getting sensitive data. It is a convenience and not a security feature,
* make sure to always examine information coming in and from the tool on the client.
*/
secrets?: Record<string, string>;
@@ -154,11 +154,6 @@ export type Config = {
*/
outputDir?: string;
/**
* Whether to save snapshots, console messages, network logs and other session logs to a file or to the standard output. Defaults to "stdout".
*/
outputMode?: 'file' | 'stdout';
console?: {
/**
* The level of console messages to return. Each level includes the messages of more severe levels. Defaults to "info".
@@ -217,12 +212,14 @@ export type Config = {
/**
* When taking snapshots for responses, specifies the mode to use.
*/
mode?: 'incremental' | 'full' | 'none';
mode?: 'full' | 'none';
};
/**
* Whether to allow file uploads from anywhere on the file system.
* By default (false), file uploads are restricted to paths within the MCP roots only.
* allowUnrestrictedFileAccess acts as a guardrail to prevent the LLM from accidentally
* wandering outside its intended workspace. It is a convenience defense to catch unintended
* file access, not a secure boundary; a deliberate attempt to reach other directories can be
* easily worked around, so always rely on client-level permissions for true security.
*/
allowUnrestrictedFileAccess?: boolean;

View File

@@ -33,8 +33,8 @@
}
},
"dependencies": {
"playwright": "1.59.0-alpha-1773608981000",
"playwright-core": "1.59.0-alpha-1773608981000"
"playwright": "1.59.0-alpha-1774903871000",
"playwright-core": "1.59.0-alpha-1774903871000"
},
"bin": {
"playwright-mcp": "cli.js"

View File

@@ -74,10 +74,10 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
},
startClient: async ({ mcpHeadless, mcpBrowser, mcpMode, mcpArgs }, use, testInfo) => {
const configDir = path.dirname(test.info().config.configFile!);
const clients: Client[] = [];
await use(async options => {
const cwd = testInfo.outputPath();
const args: string[] = mcpArgs ?? [];
if (process.env.CI && process.platform === 'linux')
args.push('--no-sandbox');
@@ -90,7 +90,7 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
if (options?.config) {
const configFile = testInfo.outputPath('config.json');
await fs.promises.writeFile(configFile, JSON.stringify(options.config, null, 2));
args.push(`--config=${path.relative(configDir, configFile)}`);
args.push(`--config=${path.relative(cwd, configFile)}`);
}
const client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' }, options?.roots ? { capabilities: { roots: {} } } : undefined);
@@ -103,7 +103,7 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
};
});
}
const { transport, stderr } = await createTransport(args, mcpMode, testInfo.outputPath('ms-playwright'), options?.extensionToken);
const { transport, stderr } = await createTransport(args, cwd, mcpMode, testInfo.outputPath('ms-playwright'), options?.extensionToken);
let stderrBuffer = '';
stderr?.on('data', data => {
if (process.env.PWMCP_DEBUG)
@@ -182,12 +182,14 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
},
});
async function createTransport(args: string[], mcpMode: TestOptions['mcpMode'], profilesDir: string, extensionToken?: string): Promise<{
async function createTransport(args: string[], cwd: string, mcpMode: TestOptions['mcpMode'], profilesDir: string, extensionToken?: string): Promise<{
transport: Transport,
stderr: Stream | null,
}> {
if (mcpMode === 'docker') {
const dockerArgs = ['run', '--rm', '-i', '--network=host', '-v', `${test.info().project.outputDir}:/app/test-results`];
const relCwd = path.relative(test.info().project.outputDir, cwd);
const dockerCwd = path.posix.join('/app/test-results', relCwd.split(path.sep).join('/'));
const dockerArgs = ['run', '--rm', '-i', '--network=host', '-v', `${test.info().project.outputDir}:/app/test-results`, '-w', dockerCwd];
const transport = new StdioClientTransport({
command: 'docker',
args: [...dockerArgs, 'playwright-mcp-dev:latest', ...args],
@@ -201,7 +203,7 @@ async function createTransport(args: string[], mcpMode: TestOptions['mcpMode'],
const transport = new StdioClientTransport({
command: 'node',
args: [path.join(__dirname, '../cli.js'), ...args],
cwd: path.dirname(test.info().config.configFile!),
cwd,
stderr: 'pipe',
env: {
...process.env,
@@ -222,7 +224,7 @@ type Response = Awaited<ReturnType<Client['callTool']>>;
export const expect = baseExpect.extend({
toHaveResponse(response: Response, object: any) {
const parsed = parseResponse(response);
const parsed = parseResponse(response, test.info().outputPath());
const isNot = this.isNot;
try {
if (isNot)
@@ -246,7 +248,7 @@ export function formatOutput(output: string): string[] {
return output.split('\n').map(line => line.replace(/^pw:mcp:test /, '').replace(/user data dir.*/, 'user data dir').trim()).filter(Boolean);
}
function parseResponse(response: any) {
function parseResponse(response: any, cwd: string) {
const text = response.content[0].text;
const sections = parseSections(text);
@@ -255,7 +257,7 @@ function parseResponse(response: any) {
const code = sections.get('Ran Playwright code');
const tabs = sections.get('Open tabs');
const pageState = sections.get('Page state');
const snapshot = sections.get('Snapshot');
const snapshotSection = sections.get('Snapshot');
const consoleMessages = sections.get('New console messages');
const modalState = sections.get('Modal state');
const downloads = sections.get('Downloads');
@@ -263,6 +265,19 @@ function parseResponse(response: any) {
const isError = response.isError;
const attachments = response.content.slice(1);
let snapshot: string | undefined;
if (snapshotSection) {
const match = snapshotSection.match(/\[Snapshot\]\(([^)]+)\)/);
if (match) {
try {
snapshot = fs.readFileSync(path.resolve(cwd, match[1]), 'utf-8');
} catch {
}
} else {
snapshot = snapshotSection.replace(/^```yaml\n?/, '').replace(/\n?```$/, '');
}
}
return {
error,
result,