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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,12 +285,13 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
- [`hover`](docs/tool-reference.md#hover)
- [`press_key`](docs/tool-reference.md#press_key)
- [`upload_file`](docs/tool-reference.md#upload_file)
- **Navigation automation** (6 tools)
- **Navigation automation** (7 tools)
- [`close_page`](docs/tool-reference.md#close_page)
- [`list_pages`](docs/tool-reference.md#list_pages)
- [`navigate_page`](docs/tool-reference.md#navigate_page)
- [`new_page`](docs/tool-reference.md#new_page)
- [`select_page`](docs/tool-reference.md#select_page)
- [`switch_browser`](docs/tool-reference.md#switch_browser)
- [`wait_for`](docs/tool-reference.md#wait_for)
- **Emulation** (2 tools)
- [`emulate`](docs/tool-reference.md#emulate)
Expand Down Expand Up @@ -383,6 +384,11 @@ The Chrome DevTools MCP server supports the following configuration option:
- **Type:** boolean
- **Default:** `true`

- **`--noLaunch`**
Do not launch or connect to a browser automatically. Use switch_browser tool to connect manually.
- **Type:** boolean
- **Default:** `false`

<!-- END AUTO GENERATED OPTIONS -->

Pass them via the `args` property in the JSON configuration. For example:
Expand Down
14 changes: 13 additions & 1 deletion docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
- [`hover`](#hover)
- [`press_key`](#press_key)
- [`upload_file`](#upload_file)
- **[Navigation automation](#navigation-automation)** (6 tools)
- **[Navigation automation](#navigation-automation)** (7 tools)
- [`close_page`](#close_page)
- [`list_pages`](#list_pages)
- [`navigate_page`](#navigate_page)
- [`new_page`](#new_page)
- [`select_page`](#select_page)
- [`switch_browser`](#switch_browser)
- [`wait_for`](#wait_for)
- **[Emulation](#emulation)** (2 tools)
- [`emulate`](#emulate)
Expand Down Expand Up @@ -176,6 +177,17 @@

---

### `switch_browser`

**Description:** Connect to a different browser instance. Disconnects from the current browser (if any) and establishes a new connection. Accepts either HTTP URLs (e.g., http://127.0.0.1:9222) or WebSocket endpoints (e.g., ws://127.0.0.1:9222/devtools/browser/&lt;id&gt;).

**Parameters:**

- **timeout** (number) _(optional)_: Connection timeout in milliseconds. Defaults to 10000 (10 seconds). If the connection cannot be established within this time, an error will be thrown.
- **url** (string) **(required)**: Browser connection URL. Can be an HTTP URL (e.g., http://127.0.0.1:9222) which will be auto-converted to WebSocket, or a direct WebSocket endpoint (e.g., ws://127.0.0.1:52862/devtools/browser/&lt;id&gt;).

---

### `wait_for`

**Description:** Wait for the specified text to appear on the selected page.
Expand Down
2 changes: 1 addition & 1 deletion release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"extra-files": [
{
"type": "generic",
"path": "src/main.ts"
"path": "src/config.ts"
},
{
"type": "json",
Expand Down
4 changes: 3 additions & 1 deletion src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,9 @@ export class McpContext implements Context {
});

if (!this.#selectedPage || this.#pages.indexOf(this.#selectedPage) === -1) {
this.selectPage(this.#pages[0]);
if (this.#pages.length > 0) {
this.selectPage(this.#pages[0]);
}
}

await this.detectOpenDevToolsWindows();
Expand Down
29 changes: 26 additions & 3 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ import {puppeteer} from './third_party/index.js';

let browser: Browser | undefined;

export async function disconnectBrowser(): Promise<void> {
if (browser?.connected) {
await browser.close();
browser = undefined;
}
}

export function getBrowser(): Browser | undefined {
return browser;
}

export function setBrowser(newBrowser: Browser | undefined): void {
browser = newBrowser;
}

function makeTargetFilter() {
const ignoredPrefixes = new Set([
'chrome://',
Expand Down Expand Up @@ -67,9 +82,17 @@ export async function ensureBrowserConnected(options: {
}

logger('Connecting Puppeteer to ', JSON.stringify(connectOptions));
browser = await puppeteer.connect(connectOptions);
logger('Connected Puppeteer');
return browser;
try {
browser = await puppeteer.connect(connectOptions);
logger('Connected Puppeteer successfully');
logger('Browser object type:', typeof browser);
logger('Browser.connected:', browser?.connected);
return browser;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger('Failed to connect to Puppeteer:', message);
throw new Error(`Puppeteer connection failed: ${message}`);
}
}

interface McpLaunchOptions {
Expand Down
7 changes: 7 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ export const cliOptions = {
default: true,
describe: 'Set to false to exclude tools related to network.',
},
noLaunch: {
type: 'boolean',
default: false,
describe:
'Do not launch or connect to a browser automatically. Use switch_browser tool to connect manually.',
},
} satisfies Record<string, YargsOptions>;

export function parseArguments(version: string, argv = process.argv) {
Expand All @@ -170,6 +176,7 @@ export function parseArguments(version: string, argv = process.argv) {
// We can't set default in the options else
// Yargs will complain
if (
!args.noLaunch &&
!args.channel &&
!args.browserUrl &&
!args.wsEndpoint &&
Expand Down
15 changes: 15 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {parseArguments} from './cli.js';

// If moved update release-please config
// x-release-please-start-version
const VERSION = '0.10.2';
// x-release-please-end

export const args = parseArguments(VERSION);
export {VERSION};
98 changes: 98 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {ensureBrowserConnected} from './browser.js';
import {logger} from './logger.js';
import {McpContext} from './McpContext.js';

let context: McpContext | undefined;

export interface SetContextOptions {
browserURL?: string;
wsEndpoint?: string;
devtools?: boolean;
experimentalIncludeAllPages?: boolean;
timeout?: number;
}

export async function setContext(
options: SetContextOptions,
): Promise<McpContext> {
const {
browserURL,
wsEndpoint,
devtools = false,
experimentalIncludeAllPages = false,
timeout,
} = options;

logger('setContext called with:', {
browserURL,
wsEndpoint,
devtools,
timeout,
});

const connectPromise = ensureBrowserConnected({
browserURL,
wsEndpoint,
devtools,
});

let browser;
logger('Starting browser connection, timeout:', timeout);
if (timeout) {
let timeoutId: NodeJS.Timeout | undefined;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(
() =>
reject(
new Error(
`Failed to connect to browser within ${timeout}ms. Please check that the browser is running and accessible at the provided URL.`,
),
),
timeout,
);
});

try {
browser = await Promise.race([connectPromise, timeoutPromise]);
} finally {
// Clear timeout to prevent it from firing after connection succeeds
if (timeoutId) {
clearTimeout(timeoutId);
}
}
} else {
browser = await connectPromise;
}

logger('Browser connection completed, browser type:', typeof browser);
logger('Browser connected status:', browser?.connected);

if (!browser) {
throw new Error(
'Failed to connect to browser: browser object is undefined',
);
}

logger('Creating McpContext from browser...');
context = await McpContext.from(browser, logger, {
experimentalDevToolsDebugging: devtools,
experimentalIncludeAllPages,
});

logger('McpContext created successfully');
return context;
}

export function getContext(): McpContext | undefined {
return context;
}

export function setContextInstance(newContext: McpContext | undefined): void {
context = newContext;
}
85 changes: 51 additions & 34 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import './polyfill.js';

import type {Channel} from './browser.js';
import {ensureBrowserConnected, ensureBrowserLaunched} from './browser.js';
import {parseArguments} from './cli.js';
import {args, VERSION} from './config.js';
import {
getContext as getContextInstance,
setContextInstance,
} from './context.js';
import {features} from './features.js';
import {loadIssueDescriptions} from './issue-descriptions.js';
import {logger, saveLogsToFile} from './logger.js';
Expand All @@ -31,15 +35,9 @@ import * as performanceTools from './tools/performance.js';
import * as screenshotTools from './tools/screenshot.js';
import * as scriptTools from './tools/script.js';
import * as snapshotTools from './tools/snapshot.js';
import * as switchBrowserTool from './tools/switch_browser.js';
import type {ToolDefinition} from './tools/ToolDefinition.js';

// If moved update release-please config
// x-release-please-start-version
const VERSION = '0.10.2';
// x-release-please-end

export const args = parseArguments(VERSION);

const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;

logger(`Starting Chrome DevTools MCP Server v${VERSION}`);
Expand All @@ -55,39 +53,57 @@ server.server.setRequestHandler(SetLevelRequestSchema, () => {
return {};
});

let context: McpContext;
async function getContext(): Promise<McpContext> {
let context = getContextInstance();

if (args.noLaunch && !context) {
throw new Error(
'No browser connected. Use the switch_browser tool to connect to a browser instance.',
);
}

const extraArgs: string[] = (args.chromeArg ?? []).map(String);
if (args.proxyServer) {
extraArgs.push(`--proxy-server=${args.proxyServer}`);
}
const devtools = args.experimentalDevtools ?? false;
const browser =
args.browserUrl || args.wsEndpoint
? await ensureBrowserConnected({
browserURL: args.browserUrl,
wsEndpoint: args.wsEndpoint,
wsHeaders: args.wsHeaders,
devtools,
})
: await ensureBrowserLaunched({
headless: args.headless,
executablePath: args.executablePath,
channel: args.channel as Channel,
isolated: args.isolated,
logFile,
viewport: args.viewport,
args: extraArgs,
acceptInsecureCerts: args.acceptInsecureCerts,
devtools,
});

if (context?.browser !== browser) {
context = await McpContext.from(browser, logger, {
experimentalDevToolsDebugging: devtools,
experimentalIncludeAllPages: args.experimentalIncludeAllPages,
});

if (!args.noLaunch) {
const browser =
args.browserUrl || args.wsEndpoint
? await ensureBrowserConnected({
browserURL: args.browserUrl,
wsEndpoint: args.wsEndpoint,
wsHeaders: args.wsHeaders,
devtools,
})
: await ensureBrowserLaunched({
headless: args.headless,
executablePath: args.executablePath,
channel: args.channel as Channel,
isolated: args.isolated,
logFile,
viewport: args.viewport,
args: extraArgs,
acceptInsecureCerts: args.acceptInsecureCerts,
devtools,
});

if (context?.browser !== browser) {
context = await McpContext.from(browser, logger, {
experimentalDevToolsDebugging: devtools,
experimentalIncludeAllPages: args.experimentalIncludeAllPages,
});
setContextInstance(context);
}
}

if (!context) {
throw new Error(
'Failed to initialize browser context. This should not happen.',
);
}

return context;
}

Expand Down Expand Up @@ -181,6 +197,7 @@ const tools = [
...Object.values(screenshotTools),
...Object.values(scriptTools),
...Object.values(snapshotTools),
...Object.values(switchBrowserTool),
] as ToolDefinition[];

tools.sort((a, b) => {
Expand Down
Loading