mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-26 08:32:26 +08:00
feat(storage): allow passing storage state for isolated contexts (#409)
Fixes https://github.com/microsoft/playwright-mcp/issues/403 Ref https://github.com/microsoft/playwright-mcp/issues/367
This commit is contained in:
parent
949f956378
commit
ce72367208
45
README.md
45
README.md
@ -117,6 +117,7 @@ Playwright MCP server supports following arguments. They can be provided in the
|
||||
- Default: `chrome`
|
||||
- `--caps <caps>`: Comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.
|
||||
- `--cdp-endpoint <endpoint>`: CDP endpoint to connect to
|
||||
- `--isolated`: Keep the browser profile in memory, do not save it to disk
|
||||
- `--executable-path <path>`: Path to the browser executable
|
||||
- `--headless`: Run browser in headless mode (headed by default)
|
||||
- `--device`: Emulate mobile device
|
||||
@ -131,15 +132,45 @@ Playwright MCP server supports following arguments. They can be provided in the
|
||||
|
||||
### User profile
|
||||
|
||||
Playwright MCP will launch the browser with the new profile, located at
|
||||
You can run Playwright MCP with persistent profile like a regular browser (default), or in the isolated contexts for the testing sessions.
|
||||
|
||||
```
|
||||
- `%USERPROFILE%\AppData\Local\ms-playwright\mcp-{channel}-profile` on Windows
|
||||
- `~/Library/Caches/ms-playwright/mcp-{channel}-profile` on macOS
|
||||
- `~/.cache/ms-playwright/mcp-{channel}-profile` on Linux
|
||||
**Persistent profile**
|
||||
|
||||
All the logged in information will be stored in the persistent profile, you can delete it between sessions if you'd like to clear the offline state.
|
||||
Persistent profile is located at the following locations and you can override it with the `--user-data-dir` argument.
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
%USERPROFILE%\AppData\Local\ms-playwright\mcp-{channel}-profile
|
||||
|
||||
# macOS
|
||||
- ~/Library/Caches/ms-playwright/mcp-{channel}-profile
|
||||
|
||||
# Linux
|
||||
- ~/.cache/ms-playwright/mcp-{channel}-profile
|
||||
```
|
||||
|
||||
All the logged in information will be stored in that profile, you can delete it between sessions if you'd like to clear the offline state.
|
||||
**Isolated**
|
||||
|
||||
In the isolated mode, each session is started in the isolated profile. Every time you ask MCP to close the browser,
|
||||
the session is closed and all the storage state for this session is lost. You can provide initial storage state
|
||||
to the browser via the config's `contextOptions` or via the `--storage-state` argument. Learn more about the storage
|
||||
state [here](https://playwright.dev/docs/auth).
|
||||
|
||||
```js
|
||||
{
|
||||
"mcpServers": {
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"@playwright/mcp@latest",
|
||||
"--isolated",
|
||||
"--storage-state={path/to/storage.json}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration file
|
||||
|
||||
@ -161,7 +192,7 @@ npx @playwright/mcp@latest --config path/to/config.json
|
||||
browserName?: 'chromium' | 'firefox' | 'webkit';
|
||||
|
||||
// Keep the browser profile in memory, do not save it to disk.
|
||||
ephemeral?: boolean;
|
||||
isolated?: boolean;
|
||||
|
||||
// Path to user data directory for browser profile persistence
|
||||
userDataDir?: string;
|
||||
|
2
config.d.ts
vendored
2
config.d.ts
vendored
@ -31,7 +31,7 @@ export type Config = {
|
||||
/**
|
||||
* Keep the browser profile in memory, do not save it to disk.
|
||||
*/
|
||||
ephemeral?: boolean;
|
||||
isolated?: boolean;
|
||||
|
||||
/**
|
||||
* Path to a user data directory for browser profile persistence.
|
||||
|
@ -28,11 +28,12 @@ export type CLIOptions = {
|
||||
browser?: string;
|
||||
caps?: string;
|
||||
cdpEndpoint?: string;
|
||||
ephemeral?: boolean;
|
||||
isolated?: boolean;
|
||||
executablePath?: string;
|
||||
headless?: boolean;
|
||||
device?: string;
|
||||
userDataDir?: string;
|
||||
storageState?: string;
|
||||
port?: number;
|
||||
host?: string;
|
||||
vision?: boolean;
|
||||
@ -102,12 +103,14 @@ export async function configFromCLIOptions(cliOptions: CLIOptions): Promise<Conf
|
||||
if (browserName === 'chromium')
|
||||
(launchOptions as any).cdpPort = await findFreePort();
|
||||
|
||||
const contextOptions: BrowserContextOptions | undefined = cliOptions.device ? devices[cliOptions.device] : undefined;
|
||||
const contextOptions: BrowserContextOptions = cliOptions.device ? devices[cliOptions.device] : {};
|
||||
if (cliOptions.storageState)
|
||||
contextOptions.storageState = cliOptions.storageState;
|
||||
|
||||
return {
|
||||
browser: {
|
||||
browserName,
|
||||
ephemeral: cliOptions.ephemeral,
|
||||
isolated: cliOptions.isolated,
|
||||
userDataDir: cliOptions.userDataDir,
|
||||
launchOptions,
|
||||
contextOptions,
|
||||
|
@ -341,22 +341,22 @@ ${code.join('\n')}
|
||||
|
||||
if (this.config.browser?.cdpEndpoint) {
|
||||
const browser = await playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint);
|
||||
const browserContext = this.config.browser.ephemeral ? await browser.newContext() : browser.contexts()[0];
|
||||
const browserContext = this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0];
|
||||
return { browser, browserContext };
|
||||
}
|
||||
|
||||
return this.config.browser?.ephemeral ?
|
||||
await launchEphemeralContext(this.config.browser) :
|
||||
return this.config.browser?.isolated ?
|
||||
await createIsolatedContext(this.config.browser) :
|
||||
await launchPersistentContext(this.config.browser);
|
||||
}
|
||||
}
|
||||
|
||||
async function launchEphemeralContext(browserConfig: Config['browser']): Promise<BrowserContextAndBrowser> {
|
||||
async function createIsolatedContext(browserConfig: Config['browser']): Promise<BrowserContextAndBrowser> {
|
||||
try {
|
||||
const browserName = browserConfig?.browserName ?? 'chromium';
|
||||
const browserType = playwright[browserName];
|
||||
const browser = await browserType.launch(browserConfig?.launchOptions);
|
||||
const browserContext = await browser.newContext();
|
||||
const browserContext = await browser.newContext(browserConfig?.contextOptions);
|
||||
return { browser, browserContext };
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('Executable doesn\'t exist'))
|
||||
|
@ -28,7 +28,8 @@ program
|
||||
.option('--browser <browser>', 'Browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
|
||||
.option('--caps <caps>', 'Comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.')
|
||||
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
|
||||
.option('--ephemeral', 'Keep the browser profile in memory, do not save it to disk.')
|
||||
.option('--isolated', 'Keep the browser profile in memory, do not save it to disk.')
|
||||
.option('--storage-state <path>', 'Path to the storage state file for isolated sessions.')
|
||||
.option('--executable-path <path>', 'Path to the browser executable.')
|
||||
.option('--headless', 'Run browser in headless mode, headed by default')
|
||||
.option('--device <device>', 'Device to emulate, for example: "iPhone 15"')
|
||||
|
@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import { test, expect } from './fixtures.js';
|
||||
|
||||
test('test reopen browser', async ({ client, server }) => {
|
||||
@ -73,7 +75,7 @@ test('persistent context', async ({ startClient, server }) => {
|
||||
expect(response2).toContainTextContent(`Storage: YES`);
|
||||
});
|
||||
|
||||
test('ephemeral context', async ({ startClient, server }) => {
|
||||
test('isolated context', async ({ startClient, server }) => {
|
||||
server.setContent('/', `
|
||||
<body>
|
||||
</body>
|
||||
@ -83,7 +85,7 @@ test('ephemeral context', async ({ startClient, server }) => {
|
||||
</script>
|
||||
`, 'text/html');
|
||||
|
||||
const client = await startClient({ args: [`--ephemeral`] });
|
||||
const client = await startClient({ args: [`--isolated`] });
|
||||
const response = await client.callTool({
|
||||
name: 'browser_navigate',
|
||||
arguments: { url: server.PREFIX },
|
||||
@ -94,10 +96,40 @@ test('ephemeral context', async ({ startClient, server }) => {
|
||||
name: 'browser_close',
|
||||
});
|
||||
|
||||
const client2 = await startClient({ args: [`--ephemeral`] });
|
||||
const client2 = await startClient({ args: [`--isolated`] });
|
||||
const response2 = await client2.callTool({
|
||||
name: 'browser_navigate',
|
||||
arguments: { url: server.PREFIX },
|
||||
});
|
||||
expect(response2).toContainTextContent(`Storage: NO`);
|
||||
});
|
||||
|
||||
test('isolated context with storage state', async ({ startClient, server, localOutputPath }) => {
|
||||
const storageStatePath = localOutputPath('storage-state.json');
|
||||
await fs.promises.writeFile(storageStatePath, JSON.stringify({
|
||||
origins: [
|
||||
{
|
||||
origin: server.PREFIX,
|
||||
localStorage: [{ name: 'test', value: 'session-value' }],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
server.setContent('/', `
|
||||
<body>
|
||||
</body>
|
||||
<script>
|
||||
document.body.textContent = 'Storage: ' + localStorage.getItem('test');
|
||||
</script>
|
||||
`, 'text/html');
|
||||
|
||||
const client = await startClient({ args: [
|
||||
`--isolated`,
|
||||
`--storage-state=${storageStatePath}`,
|
||||
] });
|
||||
const response = await client.callTool({
|
||||
name: 'browser_navigate',
|
||||
arguments: { url: server.PREFIX },
|
||||
});
|
||||
expect(response).toContainTextContent(`Storage: session-value`);
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user