4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
41ba5ba0eb Merge remote-tracking branch 'origin/main' into copilot/fix-759
# Conflicts:
#	src/browserContextFactory.ts
2025-08-02 01:27:58 +00:00
copilot-swe-agent[bot]
d4667f865d Address review feedback: move permissions to contextOptions, remove redundant tests and files
Co-authored-by: pavelfeldman <883973+pavelfeldman@users.noreply.github.com>
2025-08-01 23:59:38 +00:00
copilot-swe-agent[bot]
ce81f556a5 Implement clipboard permissions support feature
Co-authored-by: pavelfeldman <883973+pavelfeldman@users.noreply.github.com>
2025-08-01 18:56:45 +00:00
copilot-swe-agent[bot]
fc127d5895 Initial plan 2025-08-01 18:32:15 +00:00
5 changed files with 180 additions and 2 deletions

View File

@@ -169,6 +169,8 @@ Playwright MCP server supports following arguments. They can be provided in the
--no-sandbox disable the sandbox for all process types that
are normally sandboxed.
--output-dir <path> path to the directory for output files.
--permissions <permissions> comma-separated list of permissions to grant, for
example "clipboard-read,clipboard-write"
--port <port> port to listen on for SSE transport.
--proxy-bypass <bypass> comma-separated domains to bypass proxy, for
example ".com,chromium.org,.domain.com"

View File

@@ -138,7 +138,7 @@ class CdpContextFactory extends BaseContextFactory {
}
protected override async _doCreateContext(browser: playwright.Browser): Promise<playwright.BrowserContext> {
return this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0];
return this.config.browser.isolated ? await browser.newContext(this.config.browser.contextOptions) : browser.contexts()[0];
}
}
@@ -156,7 +156,7 @@ class RemoteContextFactory extends BaseContextFactory {
}
protected override async _doCreateContext(browser: playwright.Browser): Promise<playwright.BrowserContext> {
return browser.newContext();
return browser.newContext(this.config.browser.contextOptions);
}
}

View File

@@ -40,6 +40,7 @@ export type CLIOptions = {
imageResponses?: 'allow' | 'omit';
sandbox?: boolean;
outputDir?: string;
permissions?: string[];
port?: number;
proxyBypass?: string;
proxyServer?: string;
@@ -170,6 +171,9 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
if (cliOptions.blockServiceWorkers)
contextOptions.serviceWorkers = 'block';
if (cliOptions.permissions)
contextOptions.permissions = cliOptions.permissions;
const result: Config = {
browser: {
browserName,
@@ -216,6 +220,7 @@ function configFromEnv(): Config {
options.imageResponses = 'omit';
options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX);
options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR);
options.permissions = commaSeparatedList(process.env.PLAYWRIGHT_MCP_PERMISSIONS);
options.port = envToNumber(process.env.PLAYWRIGHT_MCP_PORT);
options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS);
options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER);

View File

@@ -46,6 +46,7 @@ program
.option('--image-responses <mode>', 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".')
.option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.')
.option('--output-dir <path>', 'path to the directory for output files.')
.option('--permissions <permissions>', 'comma-separated list of permissions to grant, for example "clipboard-read,clipboard-write"', commaSeparatedList)
.option('--port <port>', 'port to listen on for SSE transport.')
.option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
.option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')

170
tests/permissions.spec.ts Normal file
View File

@@ -0,0 +1,170 @@
/**
* 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.
*/
import { test, expect } from './fixtures.js';
test('clipboard permissions support via CLI argument', async ({ startClient, server }) => {
server.setContent('/', `
<html>
<body>
<h1>Test Page</h1>
</body>
</html>
`, 'text/html');
const { client } = await startClient({ args: ['--permissions', 'clipboard-read,clipboard-write'] });
// Navigate to server page
const navigateResponse = await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(navigateResponse.isError).toBeFalsy();
// Verify permissions are granted
const permissionsResponse = await client.callTool({
name: 'browser_evaluate',
arguments: {
function: '() => navigator.permissions.query({ name: "clipboard-write" }).then(result => result.state)'
}
});
expect(permissionsResponse.isError).toBeFalsy();
expect(permissionsResponse).toHaveResponse({
result: '"granted"'
});
// Test clipboard write operation without user permission prompt
const writeResponse = await client.callTool({
name: 'browser_evaluate',
arguments: {
function: '() => navigator.clipboard.writeText("test clipboard content")'
}
});
expect(writeResponse.isError).toBeFalsy();
// Test clipboard read operation without user permission prompt
const readResponse = await client.callTool({
name: 'browser_evaluate',
arguments: {
function: '() => navigator.clipboard.readText()'
}
});
expect(readResponse.isError).toBeFalsy();
expect(readResponse).toHaveResponse({
result: '"test clipboard content"'
});
});
test('clipboard permissions support via config file contextOptions', async ({ startClient, server }) => {
server.setContent('/', `
<html>
<body>
<h1>Config Test Page</h1>
</body>
</html>
`, 'text/html');
const config = {
browser: {
contextOptions: {
permissions: ['clipboard-read', 'clipboard-write']
}
}
};
const { client } = await startClient({ config });
// Navigate to server page
const navigateResponse = await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(navigateResponse.isError).toBeFalsy();
// Verify permissions are granted via config
const permissionsResponse = await client.callTool({
name: 'browser_evaluate',
arguments: {
function: '() => navigator.permissions.query({ name: "clipboard-write" }).then(result => result.state)'
}
});
expect(permissionsResponse.isError).toBeFalsy();
expect(permissionsResponse).toHaveResponse({
result: '"granted"'
});
// Test clipboard operations work with config file
const writeResponse = await client.callTool({
name: 'browser_evaluate',
arguments: {
function: '() => navigator.clipboard.writeText("config test content")'
}
});
expect(writeResponse.isError).toBeFalsy();
const readResponse = await client.callTool({
name: 'browser_evaluate',
arguments: {
function: '() => navigator.clipboard.readText()'
}
});
expect(readResponse.isError).toBeFalsy();
expect(readResponse).toHaveResponse({
result: '"config test content"'
});
});
test('multiple permissions can be granted', async ({ startClient, server }) => {
server.setContent('/', `
<html>
<body>
<h1>Multiple Permissions Test</h1>
</body>
</html>
`, 'text/html');
const { client } = await startClient({ args: ['--permissions', 'clipboard-read,clipboard-write,geolocation'] });
// Navigate to server page
const navigateResponse = await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(navigateResponse.isError).toBeFalsy();
// Test that multiple permissions can be granted
const clipboardPermissionResponse = await client.callTool({
name: 'browser_evaluate',
arguments: {
function: '() => navigator.permissions.query({ name: "clipboard-write" }).then(result => result.state)'
}
});
expect(clipboardPermissionResponse.isError).toBeFalsy();
expect(clipboardPermissionResponse).toHaveResponse({
result: '"granted"'
});
const geolocationPermissionResponse = await client.callTool({
name: 'browser_evaluate',
arguments: {
function: '() => navigator.permissions.query({ name: "geolocation" }).then(result => result.state)'
}
});
expect(geolocationPermissionResponse.isError).toBeFalsy();
expect(geolocationPermissionResponse).toHaveResponse({
result: '"granted"'
});
});