Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 54 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Chrome DevTools for reliable automation, in-depth debugging, and performance ana

## Key features

- **Multi-session support**: Run multiple isolated Chrome instances simultaneously,
each identified by a unique `sessionId`. Perfect for parallel testing, A/B
comparisons, and multi-account workflows.
- **Get performance insights**: Uses [Chrome
DevTools](https://github.com/ChromeDevTools/devtools-frontend) to record
traces and extract actionable performance insights.
Expand Down Expand Up @@ -344,15 +347,22 @@ Check the performance of https://developers.chrome.com

Your MCP client should open the browser and record a performance trace.

> [!IMPORTANT]
> All tools require a `sessionId` parameter. You must call `create_session` first to obtain one. The returned `sessionId` must be passed to every subsequent tool call.

> [!NOTE]
> The MCP server will start the browser automatically once the MCP client uses a tool that requires a running browser instance. Connecting to the Chrome DevTools MCP server on its own will not automatically start the browser.
> Each session launches an isolated Chrome instance. Multiple sessions can run simultaneously for parallel testing. Use `list_sessions` to see active sessions and `close_session` to clean up when done.

## Tools

If you run into any issues, checkout our [troubleshooting guide](./docs/troubleshooting.md).

<!-- BEGIN AUTO GENERATED TOOLS -->

- **Session management** (3 tools)
- [`create_session`](docs/tool-reference.md#create_session)
- [`list_sessions`](docs/tool-reference.md#list_sessions)
- [`close_session`](docs/tool-reference.md#close_session)
- **Input automation** (8 tools)
- [`click`](docs/tool-reference.md#click)
- [`drag`](docs/tool-reference.md#drag)
Expand Down Expand Up @@ -527,10 +537,51 @@ You can also run `npx chrome-devtools-mcp@latest --help` to see all available co

## Concepts

### Multi-session support

The Chrome DevTools MCP server supports running multiple Chrome browser sessions simultaneously. Each session is an isolated Chrome instance with its own pages, cookies, and state.

#### Workflow

1. **Create a session** — call `create_session` to launch a new Chrome instance. You receive a unique `sessionId`.
2. **Use tools** — pass the `sessionId` to every tool call (`click`, `navigate_page`, `take_screenshot`, etc.).
3. **Close the session** — call `close_session` when done to shut down the Chrome instance and free resources.

```
# Step 1: Create two sessions
create_session(label="desktop", viewport="1920x1080") → sessionId: "a1b2c3d4"
create_session(label="mobile", viewport="375x812", headless=true) → sessionId: "e5f6g7h8"

# Step 2: Use tools with the session ID
navigate_page(sessionId="a1b2c3d4", url="https://example.com")
navigate_page(sessionId="e5f6g7h8", url="https://example.com")
take_screenshot(sessionId="a1b2c3d4")
take_screenshot(sessionId="e5f6g7h8")

# Step 3: Clean up
close_session(sessionId="a1b2c3d4")
close_session(sessionId="e5f6g7h8")
```

#### Session parameters

| Parameter | Type | Description |
| ---------- | ------- | ---------------------------------------------------------- |
| `headless` | boolean | Run in headless (no UI) mode. Default: `false`. |
| `viewport` | string | Initial viewport size, e.g. `"1280x720"`. |
| `label` | string | Human-readable label, e.g. `"login-test"`. |
| `url` | string | URL to navigate to after creation. Default: `about:blank`. |

#### Session isolation

- Each session uses a temporary user data directory that is automatically cleaned up when the session closes.
- Sessions do not share cookies, localStorage, or any browser state.
- Operations within a session are serialized (mutex-protected), but different sessions run in parallel.
- If a browser disconnects unexpectedly, the session is automatically purged.

### User data directory

`chrome-devtools-mcp` starts a Chrome's stable channel instance using the following user
data directory:
When connecting to a running Chrome instance (via `--browser-url` or `--autoConnect`), Chrome uses the following user data directory:

- Linux / macOS: `$HOME/.cache/chrome-devtools-mcp/chrome-profile-$CHANNEL`
- Windows: `%HOMEPATH%/.cache/chrome-devtools-mcp/chrome-profile-$CHANNEL`
Expand Down
14 changes: 13 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

205 changes: 205 additions & 0 deletions src/SessionManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import crypto from 'node:crypto';

import type {Channel} from './browser.js';
import {launch} from './browser.js';
import {logger} from './logger.js';
import {McpContext} from './McpContext.js';
import {Mutex} from './Mutex.js';
import type {Browser} from './third_party/index.js';

export interface SessionInfo {
sessionId: string;
browser: Browser;
context: McpContext;
mutex: Mutex;
createdAt: Date;
label?: string;
}

export interface CreateSessionOptions {
headless?: boolean;
executablePath?: string;
channel?: Channel;
userDataDir?: string;
viewport?: {width: number; height: number};
chromeArgs?: string[];
ignoreDefaultChromeArgs?: string[];
acceptInsecureCerts?: boolean;
devtools?: boolean;
enableExtensions?: boolean;
label?: string;
}

export interface McpContextOptions {
experimentalDevToolsDebugging: boolean;
experimentalIncludeAllPages?: boolean;
performanceCrux: boolean;
}

export class SessionManager {
readonly #sessions = new Map<string, SessionInfo>();
readonly #contextOptions: McpContextOptions;
#shuttingDown = false;

constructor(contextOptions: McpContextOptions) {
this.#contextOptions = contextOptions;
}

async createSession(options: CreateSessionOptions): Promise<SessionInfo> {
if (this.#shuttingDown) {
throw new Error('Server is shutting down. Cannot create new sessions.');
}

const sessionId = crypto.randomUUID().slice(0, 8);
logger(`Creating session ${sessionId}`);

let browser: Browser | undefined;
try {
browser = await launch({
headless: options.headless ?? false,
executablePath: options.executablePath,
channel: options.channel,
userDataDir: options.userDataDir,
// Always isolated to avoid profile conflicts between concurrent sessions
isolated: true,
viewport: options.viewport,
chromeArgs: options.chromeArgs ?? [],
ignoreDefaultChromeArgs: options.ignoreDefaultChromeArgs ?? [],
acceptInsecureCerts: options.acceptInsecureCerts,
devtools: options.devtools ?? false,
enableExtensions: options.enableExtensions,
});

const context = await McpContext.from(
browser,
logger,
this.#contextOptions,
);
const mutex = new Mutex();

const session: SessionInfo = {
sessionId,
browser,
context,
mutex,
createdAt: new Date(),
label: options.label,
};

browser.on('disconnected', () => {
logger(`Session ${sessionId} browser disconnected unexpectedly`);
this.#purgeDisconnectedSession(sessionId);
});

this.#sessions.set(sessionId, session);
logger(`Session ${sessionId} created`);
return session;
} catch (err) {
if (browser?.connected) {
try {
await browser.close();
} catch (closeErr) {
logger(`Failed to close browser after creation failure:`, closeErr);
}
}
throw err;
}
}

getSession(sessionId: string): SessionInfo {
const session = this.#sessions.get(sessionId);
if (!session) {
const available = [...this.#sessions.keys()].join(', ');
throw new Error(
`Session "${sessionId}" not found. Available sessions: ${available || 'none. Create one with create_session.'}`,
);
}
if (!session.browser.connected) {
this.#purgeDisconnectedSession(sessionId);
throw new Error(
`Session "${sessionId}" browser is disconnected. Create a new session.`,
);
}
return session;
}

listSessions(): Array<{
sessionId: string;
createdAt: string;
label?: string;
connected: boolean;
}> {
const result: Array<{
sessionId: string;
createdAt: string;
label?: string;
connected: boolean;
}> = [];

for (const [, session] of this.#sessions) {
result.push({
sessionId: session.sessionId,
createdAt: session.createdAt.toISOString(),
label: session.label,
connected: session.browser.connected,
});
}
return result;
}

async closeSession(sessionId: string): Promise<void> {
const session = this.#sessions.get(sessionId);
if (!session) {
throw new Error(`Session "${sessionId}" not found.`);
}

logger(`Closing session ${sessionId} (acquiring mutex)`);
const guard = await session.mutex.acquire();
try {
session.context.dispose();
if (session.browser.connected) {
await session.browser.close();
}
} catch (err) {
logger(`Error closing session ${sessionId}:`, err);
} finally {
guard.dispose();
this.#sessions.delete(sessionId);
logger(`Session ${sessionId} closed`);
}
}

async closeAllSessions(): Promise<void> {
this.#shuttingDown = true;
const ids = [...this.#sessions.keys()];
await Promise.allSettled(ids.map(id => this.closeSession(id)));
}

get sessionCount(): number {
return this.#sessions.size;
}

get isShuttingDown(): boolean {
return this.#shuttingDown;
}

#purgeDisconnectedSession(sessionId: string): void {
const session = this.#sessions.get(sessionId);
if (!session) {
return;
}
try {
session.context.dispose();
} catch (err) {
logger(`Error disposing context for disconnected session ${sessionId}:`, err);
}
this.#sessions.delete(sessionId);
logger(`Purged disconnected session ${sessionId}`);
}
}
Loading