mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2026-02-04 09:13:10 +00:00
Implement clipboard permissions support feature
Co-authored-by: pavelfeldman <883973+pavelfeldman@users.noreply.github.com>
This commit is contained in:
@@ -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
|
--no-sandbox disable the sandbox for all process types that
|
||||||
are normally sandboxed.
|
are normally sandboxed.
|
||||||
--output-dir <path> path to the directory for output files.
|
--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.
|
--port <port> port to listen on for SSE transport.
|
||||||
--proxy-bypass <bypass> comma-separated domains to bypass proxy, for
|
--proxy-bypass <bypass> comma-separated domains to bypass proxy, for
|
||||||
example ".com,chromium.org,.domain.com"
|
example ".com,chromium.org,.domain.com"
|
||||||
|
|||||||
5
config.d.ts
vendored
5
config.d.ts
vendored
@@ -54,6 +54,11 @@ export type Config = {
|
|||||||
*/
|
*/
|
||||||
contextOptions?: playwright.BrowserContextOptions;
|
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.
|
* Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers.
|
||||||
*/
|
*/
|
||||||
|
|||||||
95
examples/clipboard-permissions.md
Normal file
95
examples/clipboard-permissions.md
Normal 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
|
||||||
@@ -113,7 +113,10 @@ class IsolatedContextFactory extends BaseContextFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override async _doCreateContext(browser: playwright.Browser): Promise<playwright.BrowserContext> {
|
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> {
|
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> {
|
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];
|
const browserType = playwright[this.browserConfig.browserName];
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
try {
|
try {
|
||||||
const browserContext = await browserType.launchPersistentContext(userDataDir, {
|
const contextOptions = {
|
||||||
...this.browserConfig.launchOptions,
|
...this.browserConfig.launchOptions,
|
||||||
...this.browserConfig.contextOptions,
|
...this.browserConfig.contextOptions,
|
||||||
handleSIGINT: false,
|
handleSIGINT: false,
|
||||||
handleSIGTERM: 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);
|
const close = () => this._closeBrowserContext(browserContext, userDataDir);
|
||||||
return { browserContext, close };
|
return { browserContext, close };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export type CLIOptions = {
|
|||||||
imageResponses?: 'allow' | 'omit';
|
imageResponses?: 'allow' | 'omit';
|
||||||
sandbox?: boolean;
|
sandbox?: boolean;
|
||||||
outputDir?: string;
|
outputDir?: string;
|
||||||
|
permissions?: string[];
|
||||||
port?: number;
|
port?: number;
|
||||||
proxyBypass?: string;
|
proxyBypass?: string;
|
||||||
proxyServer?: string;
|
proxyServer?: string;
|
||||||
@@ -177,6 +178,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
|
|||||||
browserName,
|
browserName,
|
||||||
isolated: cliOptions.isolated,
|
isolated: cliOptions.isolated,
|
||||||
userDataDir: cliOptions.userDataDir,
|
userDataDir: cliOptions.userDataDir,
|
||||||
|
permissions: cliOptions.permissions,
|
||||||
launchOptions,
|
launchOptions,
|
||||||
contextOptions,
|
contextOptions,
|
||||||
cdpEndpoint: cliOptions.cdpEndpoint,
|
cdpEndpoint: cliOptions.cdpEndpoint,
|
||||||
@@ -218,6 +220,7 @@ function configFromEnv(): Config {
|
|||||||
options.imageResponses = 'omit';
|
options.imageResponses = 'omit';
|
||||||
options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX);
|
options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX);
|
||||||
options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR);
|
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.port = envToNumber(process.env.PLAYWRIGHT_MCP_PORT);
|
||||||
options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS);
|
options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS);
|
||||||
options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER);
|
options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER);
|
||||||
|
|||||||
@@ -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('--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('--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('--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('--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-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"')
|
.option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
|
||||||
|
|||||||
227
tests/permissions.spec.ts
Normal file
227
tests/permissions.spec.ts
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user