mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-27 00:52:27 +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`
|
- Default: `chrome`
|
||||||
- `--caps <caps>`: Comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.
|
- `--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
|
- `--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
|
- `--executable-path <path>`: Path to the browser executable
|
||||||
- `--headless`: Run browser in headless mode (headed by default)
|
- `--headless`: Run browser in headless mode (headed by default)
|
||||||
- `--device`: Emulate mobile device
|
- `--device`: Emulate mobile device
|
||||||
@ -131,15 +132,45 @@ Playwright MCP server supports following arguments. They can be provided in the
|
|||||||
|
|
||||||
### User profile
|
### 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.
|
||||||
|
|
||||||
```
|
**Persistent profile**
|
||||||
- `%USERPROFILE%\AppData\Local\ms-playwright\mcp-{channel}-profile` on Windows
|
|
||||||
- `~/Library/Caches/ms-playwright/mcp-{channel}-profile` on macOS
|
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.
|
||||||
- `~/.cache/ms-playwright/mcp-{channel}-profile` on Linux
|
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
|
### Configuration file
|
||||||
|
|
||||||
@ -161,7 +192,7 @@ npx @playwright/mcp@latest --config path/to/config.json
|
|||||||
browserName?: 'chromium' | 'firefox' | 'webkit';
|
browserName?: 'chromium' | 'firefox' | 'webkit';
|
||||||
|
|
||||||
// Keep the browser profile in memory, do not save it to disk.
|
// 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
|
// Path to user data directory for browser profile persistence
|
||||||
userDataDir?: string;
|
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.
|
* 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.
|
* Path to a user data directory for browser profile persistence.
|
||||||
|
@ -28,11 +28,12 @@ export type CLIOptions = {
|
|||||||
browser?: string;
|
browser?: string;
|
||||||
caps?: string;
|
caps?: string;
|
||||||
cdpEndpoint?: string;
|
cdpEndpoint?: string;
|
||||||
ephemeral?: boolean;
|
isolated?: boolean;
|
||||||
executablePath?: string;
|
executablePath?: string;
|
||||||
headless?: boolean;
|
headless?: boolean;
|
||||||
device?: string;
|
device?: string;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
|
storageState?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
host?: string;
|
host?: string;
|
||||||
vision?: boolean;
|
vision?: boolean;
|
||||||
@ -102,12 +103,14 @@ export async function configFromCLIOptions(cliOptions: CLIOptions): Promise<Conf
|
|||||||
if (browserName === 'chromium')
|
if (browserName === 'chromium')
|
||||||
(launchOptions as any).cdpPort = await findFreePort();
|
(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 {
|
return {
|
||||||
browser: {
|
browser: {
|
||||||
browserName,
|
browserName,
|
||||||
ephemeral: cliOptions.ephemeral,
|
isolated: cliOptions.isolated,
|
||||||
userDataDir: cliOptions.userDataDir,
|
userDataDir: cliOptions.userDataDir,
|
||||||
launchOptions,
|
launchOptions,
|
||||||
contextOptions,
|
contextOptions,
|
||||||
|
@ -341,22 +341,22 @@ ${code.join('\n')}
|
|||||||
|
|
||||||
if (this.config.browser?.cdpEndpoint) {
|
if (this.config.browser?.cdpEndpoint) {
|
||||||
const browser = await playwright.chromium.connectOverCDP(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 { browser, browserContext };
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.config.browser?.ephemeral ?
|
return this.config.browser?.isolated ?
|
||||||
await launchEphemeralContext(this.config.browser) :
|
await createIsolatedContext(this.config.browser) :
|
||||||
await launchPersistentContext(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 {
|
try {
|
||||||
const browserName = browserConfig?.browserName ?? 'chromium';
|
const browserName = browserConfig?.browserName ?? 'chromium';
|
||||||
const browserType = playwright[browserName];
|
const browserType = playwright[browserName];
|
||||||
const browser = await browserType.launch(browserConfig?.launchOptions);
|
const browser = await browserType.launch(browserConfig?.launchOptions);
|
||||||
const browserContext = await browser.newContext();
|
const browserContext = await browser.newContext(browserConfig?.contextOptions);
|
||||||
return { browser, browserContext };
|
return { browser, browserContext };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.message.includes('Executable doesn\'t exist'))
|
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('--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('--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('--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('--executable-path <path>', 'Path to the browser executable.')
|
||||||
.option('--headless', 'Run browser in headless mode, headed by default')
|
.option('--headless', 'Run browser in headless mode, headed by default')
|
||||||
.option('--device <device>', 'Device to emulate, for example: "iPhone 15"')
|
.option('--device <device>', 'Device to emulate, for example: "iPhone 15"')
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
import { test, expect } from './fixtures.js';
|
import { test, expect } from './fixtures.js';
|
||||||
|
|
||||||
test('test reopen browser', async ({ client, server }) => {
|
test('test reopen browser', async ({ client, server }) => {
|
||||||
@ -73,7 +75,7 @@ test('persistent context', async ({ startClient, server }) => {
|
|||||||
expect(response2).toContainTextContent(`Storage: YES`);
|
expect(response2).toContainTextContent(`Storage: YES`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ephemeral context', async ({ startClient, server }) => {
|
test('isolated context', async ({ startClient, server }) => {
|
||||||
server.setContent('/', `
|
server.setContent('/', `
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
@ -83,7 +85,7 @@ test('ephemeral context', async ({ startClient, server }) => {
|
|||||||
</script>
|
</script>
|
||||||
`, 'text/html');
|
`, 'text/html');
|
||||||
|
|
||||||
const client = await startClient({ args: [`--ephemeral`] });
|
const client = await startClient({ args: [`--isolated`] });
|
||||||
const response = await client.callTool({
|
const response = await client.callTool({
|
||||||
name: 'browser_navigate',
|
name: 'browser_navigate',
|
||||||
arguments: { url: server.PREFIX },
|
arguments: { url: server.PREFIX },
|
||||||
@ -94,10 +96,40 @@ test('ephemeral context', async ({ startClient, server }) => {
|
|||||||
name: 'browser_close',
|
name: 'browser_close',
|
||||||
});
|
});
|
||||||
|
|
||||||
const client2 = await startClient({ args: [`--ephemeral`] });
|
const client2 = await startClient({ args: [`--isolated`] });
|
||||||
const response2 = await client2.callTool({
|
const response2 = await client2.callTool({
|
||||||
name: 'browser_navigate',
|
name: 'browser_navigate',
|
||||||
arguments: { url: server.PREFIX },
|
arguments: { url: server.PREFIX },
|
||||||
});
|
});
|
||||||
expect(response2).toContainTextContent(`Storage: NO`);
|
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