Skip to content

Commit ce72367

Browse files
authored
feat(storage): allow passing storage state for isolated contexts (#409)
Fixes #403 Ref #367
1 parent 949f956 commit ce72367

File tree

6 files changed

+87
-20
lines changed

6 files changed

+87
-20
lines changed

README.md

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ Playwright MCP server supports following arguments. They can be provided in the
117117
- Default: `chrome`
118118
- `--caps <caps>`: Comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.
119119
- `--cdp-endpoint <endpoint>`: CDP endpoint to connect to
120+
- `--isolated`: Keep the browser profile in memory, do not save it to disk
120121
- `--executable-path <path>`: Path to the browser executable
121122
- `--headless`: Run browser in headless mode (headed by default)
122123
- `--device`: Emulate mobile device
@@ -131,15 +132,45 @@ Playwright MCP server supports following arguments. They can be provided in the
131132

132133
### User profile
133134

134-
Playwright MCP will launch the browser with the new profile, located at
135+
You can run Playwright MCP with persistent profile like a regular browser (default), or in the isolated contexts for the testing sessions.
135136

137+
**Persistent profile**
138+
139+
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.
140+
Persistent profile is located at the following locations and you can override it with the `--user-data-dir` argument.
141+
142+
```bash
143+
# Windows
144+
%USERPROFILE%\AppData\Local\ms-playwright\mcp-{channel}-profile
145+
146+
# macOS
147+
- ~/Library/Caches/ms-playwright/mcp-{channel}-profile
148+
149+
# Linux
150+
- ~/.cache/ms-playwright/mcp-{channel}-profile
136151
```
137-
- `%USERPROFILE%\AppData\Local\ms-playwright\mcp-{channel}-profile` on Windows
138-
- `~/Library/Caches/ms-playwright/mcp-{channel}-profile` on macOS
139-
- `~/.cache/ms-playwright/mcp-{channel}-profile` on Linux
140-
```
141152

142-
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.
153+
**Isolated**
154+
155+
In the isolated mode, each session is started in the isolated profile. Every time you ask MCP to close the browser,
156+
the session is closed and all the storage state for this session is lost. You can provide initial storage state
157+
to the browser via the config's `contextOptions` or via the `--storage-state` argument. Learn more about the storage
158+
state [here](https://playwright.dev/docs/auth).
159+
160+
```js
161+
{
162+
"mcpServers": {
163+
"playwright": {
164+
"command": "npx",
165+
"args": [
166+
"@playwright/mcp@latest",
167+
"--isolated",
168+
"--storage-state={path/to/storage.json}
169+
]
170+
}
171+
}
172+
}
173+
```
143174
144175
### Configuration file
145176
@@ -161,7 +192,7 @@ npx @playwright/mcp@latest --config path/to/config.json
161192
browserName?: 'chromium' | 'firefox' | 'webkit';
162193
163194
// Keep the browser profile in memory, do not save it to disk.
164-
ephemeral?: boolean;
195+
isolated?: boolean;
165196
166197
// Path to user data directory for browser profile persistence
167198
userDataDir?: string;

config.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export type Config = {
3131
/**
3232
* Keep the browser profile in memory, do not save it to disk.
3333
*/
34-
ephemeral?: boolean;
34+
isolated?: boolean;
3535

3636
/**
3737
* Path to a user data directory for browser profile persistence.

src/config.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ export type CLIOptions = {
2828
browser?: string;
2929
caps?: string;
3030
cdpEndpoint?: string;
31-
ephemeral?: boolean;
31+
isolated?: boolean;
3232
executablePath?: string;
3333
headless?: boolean;
3434
device?: string;
3535
userDataDir?: string;
36+
storageState?: string;
3637
port?: number;
3738
host?: string;
3839
vision?: boolean;
@@ -102,12 +103,14 @@ export async function configFromCLIOptions(cliOptions: CLIOptions): Promise<Conf
102103
if (browserName === 'chromium')
103104
(launchOptions as any).cdpPort = await findFreePort();
104105

105-
const contextOptions: BrowserContextOptions | undefined = cliOptions.device ? devices[cliOptions.device] : undefined;
106+
const contextOptions: BrowserContextOptions = cliOptions.device ? devices[cliOptions.device] : {};
107+
if (cliOptions.storageState)
108+
contextOptions.storageState = cliOptions.storageState;
106109

107110
return {
108111
browser: {
109112
browserName,
110-
ephemeral: cliOptions.ephemeral,
113+
isolated: cliOptions.isolated,
111114
userDataDir: cliOptions.userDataDir,
112115
launchOptions,
113116
contextOptions,

src/context.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -341,22 +341,22 @@ ${code.join('\n')}
341341

342342
if (this.config.browser?.cdpEndpoint) {
343343
const browser = await playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint);
344-
const browserContext = this.config.browser.ephemeral ? await browser.newContext() : browser.contexts()[0];
344+
const browserContext = this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0];
345345
return { browser, browserContext };
346346
}
347347

348-
return this.config.browser?.ephemeral ?
349-
await launchEphemeralContext(this.config.browser) :
348+
return this.config.browser?.isolated ?
349+
await createIsolatedContext(this.config.browser) :
350350
await launchPersistentContext(this.config.browser);
351351
}
352352
}
353353

354-
async function launchEphemeralContext(browserConfig: Config['browser']): Promise<BrowserContextAndBrowser> {
354+
async function createIsolatedContext(browserConfig: Config['browser']): Promise<BrowserContextAndBrowser> {
355355
try {
356356
const browserName = browserConfig?.browserName ?? 'chromium';
357357
const browserType = playwright[browserName];
358358
const browser = await browserType.launch(browserConfig?.launchOptions);
359-
const browserContext = await browser.newContext();
359+
const browserContext = await browser.newContext(browserConfig?.contextOptions);
360360
return { browser, browserContext };
361361
} catch (error: any) {
362362
if (error.message.includes('Executable doesn\'t exist'))

src/program.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ program
2828
.option('--browser <browser>', 'Browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
2929
.option('--caps <caps>', 'Comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.')
3030
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
31-
.option('--ephemeral', 'Keep the browser profile in memory, do not save it to disk.')
31+
.option('--isolated', 'Keep the browser profile in memory, do not save it to disk.')
32+
.option('--storage-state <path>', 'Path to the storage state file for isolated sessions.')
3233
.option('--executable-path <path>', 'Path to the browser executable.')
3334
.option('--headless', 'Run browser in headless mode, headed by default')
3435
.option('--device <device>', 'Device to emulate, for example: "iPhone 15"')

tests/launch.spec.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17+
import fs from 'fs';
18+
1719
import { test, expect } from './fixtures.js';
1820

1921
test('test reopen browser', async ({ client, server }) => {
@@ -73,7 +75,7 @@ test('persistent context', async ({ startClient, server }) => {
7375
expect(response2).toContainTextContent(`Storage: YES`);
7476
});
7577

76-
test('ephemeral context', async ({ startClient, server }) => {
78+
test('isolated context', async ({ startClient, server }) => {
7779
server.setContent('/', `
7880
<body>
7981
</body>
@@ -83,7 +85,7 @@ test('ephemeral context', async ({ startClient, server }) => {
8385
</script>
8486
`, 'text/html');
8587

86-
const client = await startClient({ args: [`--ephemeral`] });
88+
const client = await startClient({ args: [`--isolated`] });
8789
const response = await client.callTool({
8890
name: 'browser_navigate',
8991
arguments: { url: server.PREFIX },
@@ -94,10 +96,40 @@ test('ephemeral context', async ({ startClient, server }) => {
9496
name: 'browser_close',
9597
});
9698

97-
const client2 = await startClient({ args: [`--ephemeral`] });
99+
const client2 = await startClient({ args: [`--isolated`] });
98100
const response2 = await client2.callTool({
99101
name: 'browser_navigate',
100102
arguments: { url: server.PREFIX },
101103
});
102104
expect(response2).toContainTextContent(`Storage: NO`);
103105
});
106+
107+
test('isolated context with storage state', async ({ startClient, server, localOutputPath }) => {
108+
const storageStatePath = localOutputPath('storage-state.json');
109+
await fs.promises.writeFile(storageStatePath, JSON.stringify({
110+
origins: [
111+
{
112+
origin: server.PREFIX,
113+
localStorage: [{ name: 'test', value: 'session-value' }],
114+
},
115+
],
116+
}));
117+
118+
server.setContent('/', `
119+
<body>
120+
</body>
121+
<script>
122+
document.body.textContent = 'Storage: ' + localStorage.getItem('test');
123+
</script>
124+
`, 'text/html');
125+
126+
const client = await startClient({ args: [
127+
`--isolated`,
128+
`--storage-state=${storageStatePath}`,
129+
] });
130+
const response = await client.callTool({
131+
name: 'browser_navigate',
132+
arguments: { url: server.PREFIX },
133+
});
134+
expect(response).toContainTextContent(`Storage: session-value`);
135+
});

0 commit comments

Comments
 (0)