Implement clipboard permissions support feature

Co-authored-by: pavelfeldman <883973+pavelfeldman@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-08-01 18:56:45 +00:00
parent fc127d5895
commit ce81f556a5
7 changed files with 353 additions and 5 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"

5
config.d.ts vendored
View File

@@ -54,6 +54,11 @@ export type Config = {
*/
contextOptions?: playwright.BrowserContextOptions;
/**
* Permissions to grant to the browser context, for example ["clipboard-read", "clipboard-write"].
*/
permissions?: string[];
/**
* Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers.
*/

View File

@@ -0,0 +1,95 @@
# Clipboard Permissions Example
This example demonstrates how to use the new `--permissions` feature to enable clipboard operations without user permission prompts.
## Usage
### Via Command Line Arguments
```json
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest",
"--permissions", "clipboard-read,clipboard-write"
]
}
}
}
```
### Via Configuration File
Create a config file `playwright-mcp-config.json`:
```json
{
"browser": {
"permissions": ["clipboard-read", "clipboard-write"]
}
}
```
Then use it:
```json
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest",
"--config", "playwright-mcp-config.json"
]
}
}
}
```
### Via Environment Variable
```bash
export PLAYWRIGHT_MCP_PERMISSIONS="clipboard-read,clipboard-write"
```
## Clipboard Operations
Once permissions are granted, you can use clipboard APIs via `browser_evaluate`:
```javascript
// Write to clipboard (no permission prompt)
await browser_evaluate({
function: "() => navigator.clipboard.writeText('Copy this!')"
})
// Read from clipboard (no permission prompt)
await browser_evaluate({
function: "() => navigator.clipboard.readText()"
})
```
## Supported Permissions
You can grant multiple permissions as a comma-separated list:
- `clipboard-read`
- `clipboard-write`
- `geolocation`
- `camera`
- `microphone`
- `notifications`
- And any other [Web API permissions](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API)
Example with multiple permissions:
```bash
--permissions "clipboard-read,clipboard-write,geolocation,notifications"
```
## Notes
- Permissions are applied when the browser context is created
- The clipboard API requires secure contexts (HTTPS) in production environments
- Some permissions may not be supported in all browsers or may require additional user activation

View File

@@ -113,7 +113,10 @@ class IsolatedContextFactory extends BaseContextFactory {
}
protected override async _doCreateContext(browser: playwright.Browser): Promise<playwright.BrowserContext> {
return browser.newContext(this.browserConfig.contextOptions);
const contextOptions = { ...this.browserConfig.contextOptions };
if (this.browserConfig.permissions)
contextOptions.permissions = this.browserConfig.permissions;
return browser.newContext(contextOptions);
}
}
@@ -127,7 +130,13 @@ class CdpContextFactory extends BaseContextFactory {
}
protected override async _doCreateContext(browser: playwright.Browser): Promise<playwright.BrowserContext> {
return this.browserConfig.isolated ? await browser.newContext() : browser.contexts()[0];
if (this.browserConfig.isolated) {
const contextOptions: playwright.BrowserContextOptions = {};
if (this.browserConfig.permissions)
contextOptions.permissions = this.browserConfig.permissions;
return browser.newContext(contextOptions);
}
return browser.contexts()[0];
}
}
@@ -145,7 +154,10 @@ class RemoteContextFactory extends BaseContextFactory {
}
protected override async _doCreateContext(browser: playwright.Browser): Promise<playwright.BrowserContext> {
return browser.newContext();
const contextOptions: playwright.BrowserContextOptions = {};
if (this.browserConfig.permissions)
contextOptions.permissions = this.browserConfig.permissions;
return browser.newContext(contextOptions);
}
}
@@ -168,12 +180,15 @@ class PersistentContextFactory implements BrowserContextFactory {
const browserType = playwright[this.browserConfig.browserName];
for (let i = 0; i < 5; i++) {
try {
const browserContext = await browserType.launchPersistentContext(userDataDir, {
const contextOptions = {
...this.browserConfig.launchOptions,
...this.browserConfig.contextOptions,
handleSIGINT: false,
handleSIGTERM: false,
});
};
if (this.browserConfig.permissions)
contextOptions.permissions = this.browserConfig.permissions;
const browserContext = await browserType.launchPersistentContext(userDataDir, contextOptions);
const close = () => this._closeBrowserContext(browserContext, userDataDir);
return { browserContext, close };
} catch (error: any) {

View File

@@ -39,6 +39,7 @@ export type CLIOptions = {
imageResponses?: 'allow' | 'omit';
sandbox?: boolean;
outputDir?: string;
permissions?: string[];
port?: number;
proxyBypass?: string;
proxyServer?: string;
@@ -177,6 +178,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
browserName,
isolated: cliOptions.isolated,
userDataDir: cliOptions.userDataDir,
permissions: cliOptions.permissions,
launchOptions,
contextOptions,
cdpEndpoint: cliOptions.cdpEndpoint,
@@ -218,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"')

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

@@ -0,0 +1,227 @@
/**
* 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', async ({ startClient, server }) => {
server.setContent('/', `
<html>
<body>
<h1>Config Test Page</h1>
</body>
</html>
`, 'text/html');
const config = {
browser: {
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"'
});
});
test('clipboard permissions via environment variable', async ({ startClient, server }) => {
server.setContent('/', `
<html>
<body>
<h1>Environment Variable Test</h1>
</body>
</html>
`, 'text/html');
// Set environment variable
process.env.PLAYWRIGHT_MCP_PERMISSIONS = 'clipboard-read,clipboard-write';
try {
const { client } = await startClient({ args: [] });
// 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 environment variable
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
const writeResponse = await client.callTool({
name: 'browser_evaluate',
arguments: {
function: '() => navigator.clipboard.writeText("env 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: '"env test content"'
});
} finally {
// Clean up environment variable
delete process.env.PLAYWRIGHT_MCP_PERMISSIONS;
}
});