Skip to content
Closed
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
77 changes: 77 additions & 0 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,81 @@ import {puppeteer} from './third_party/index.js';

let browser: Browser | undefined;

// Multi-browser registry
const browserRegistry = new Map<string, Browser>();
let activeBrowserId = 'default';

export function switchActiveBrowser(browserId: string): Browser {
if (!browserRegistry.has(browserId)) {
const available = [...browserRegistry.keys()];
if (available.length === 0) {
throw new Error(
`Browser '${browserId}' not found. No browsers are currently registered.`,
);
}
throw new Error(
`Browser '${browserId}' not found. Available browsers: ${available.join(', ')}`,
);
}
const targetBrowser = browserRegistry.get(browserId)!;
if (!targetBrowser?.connected) {
throw new Error(
`Browser '${browserId}' is disconnected. Remove it and add again with add_browser.`,
);
}
activeBrowserId = browserId;
browser = targetBrowser;
return browser;
}

export interface BrowserInfo {
id: string;
connected: boolean;
active: boolean;
}

export function listBrowsers(): BrowserInfo[] {
const result: BrowserInfo[] = [];
for (const [id, b] of browserRegistry.entries()) {
result.push({
id,
connected: b?.connected ?? false,
active: id === activeBrowserId,
});
}
return result;
}

export async function addBrowser(
browserId: string,
options: {browserURL?: string; wsEndpoint?: string; enableExtensions?: boolean},
): Promise<Browser> {
const connectOptions: Parameters<typeof puppeteer.connect>[0] = {
targetFilter: makeTargetFilter(options.enableExtensions ?? false),
defaultViewport: null,
handleDevToolsAsPage: true,
};
if (options.browserURL) {
connectOptions.browserURL = options.browserURL;
} else if (options.wsEndpoint) {
connectOptions.browserWSEndpoint = options.wsEndpoint;
} else {
throw new Error('Either browserURL or wsEndpoint must be provided');
}
logger('Adding browser', browserId, JSON.stringify(connectOptions));
const newBrowser = await puppeteer.connect(connectOptions);
browserRegistry.set(browserId, newBrowser);
if (!browser || browserRegistry.size === 1) {
browser = newBrowser;
activeBrowserId = browserId;
}
return newBrowser;
}

export function getActiveBrowserId(): string {
return activeBrowserId;
}

function makeTargetFilter(enableExtensions = false) {
const ignoredPrefixes = new Set(['chrome://', 'chrome-untrusted://']);
if (!enableExtensions) {
Expand Down Expand Up @@ -130,6 +205,7 @@ export async function ensureBrowserConnected(options: {
);
}
logger('Connected Puppeteer');
browserRegistry.set(activeBrowserId, browser);
return browser;
}

Expand Down Expand Up @@ -267,6 +343,7 @@ export async function ensureBrowserLaunched(
return browser;
}
browser = await launch(options);
browserRegistry.set(activeBrowserId, browser);
return browser;
}

Expand Down
10 changes: 8 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type fs from 'node:fs';

import type {parseArguments} from './bin/chrome-devtools-mcp-cli-options.js';
import type {Channel} from './browser.js';
import {ensureBrowserConnected, ensureBrowserLaunched} from './browser.js';
import {ensureBrowserConnected, ensureBrowserLaunched, getActiveBrowserId} from './browser.js';
import {loadIssueDescriptions} from './issue-descriptions.js';
import {logger} from './logger.js';
import {McpContext} from './McpContext.js';
Expand Down Expand Up @@ -65,6 +65,7 @@ export async function createMcpServer(
};

let context: McpContext;
const contextRegistry = new Map<string, McpContext>();
async function getContext(): Promise<McpContext> {
const chromeArgs: string[] = (serverArgs.chromeArg ?? []).map(String);
const ignoreDefaultChromeArgs: string[] = (
Expand Down Expand Up @@ -103,12 +104,17 @@ export async function createMcpServer(
viaCli: serverArgs.viaCli,
});

if (context?.browser !== browser) {
const browserId = getActiveBrowserId();
const existingContext = contextRegistry.get(browserId);
if (existingContext?.browser === browser && browser?.connected) {
context = existingContext;
} else if (context?.browser !== browser) {
context = await McpContext.from(browser, logger, {
experimentalDevToolsDebugging: devtools,
experimentalIncludeAllPages: serverArgs.experimentalIncludeAllPages,
performanceCrux: serverArgs.performanceCrux,
});
contextRegistry.set(browserId, context);
}
return context;
}
Expand Down
116 changes: 116 additions & 0 deletions src/tools/multibrowser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {
switchActiveBrowser,
listBrowsers,
addBrowser,
} from '../browser.js';
import {zod} from '../third_party/index.js';
import {ToolCategory} from './categories.js';
import {defineTool} from './ToolDefinition.js';

export const switch_browser = defineTool({
name: 'switch_browser',
description:
'Switch the active browser instance. All subsequent tool calls will operate on the selected browser.',
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: false,
},
schema: {
browserId: zod
.string()
.describe(
'The ID of the browser to switch to. Call list_browsers to see available browsers.',
),
},
handler: async (request, response) => {
const {browserId} = request.params;
switchActiveBrowser(browserId);
response.appendResponseLine(
`Switched to browser '${browserId}'. All future tool calls will target this browser.`,
);
response.appendResponseLine(`Active browser: ${browserId}`);
},
});

export const list_browsers = defineTool({
name: 'list_browsers',
description:
'List all connected browser instances with their IDs and connection status.',
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: true,
},
schema: {},
handler: async (_request, response) => {
const browsers = listBrowsers();
if (browsers.length === 0) {
response.appendResponseLine('No browsers connected.');
return;
}
response.appendResponseLine('## Connected browsers');
for (const b of browsers) {
const marker = b.active ? ' [selected]' : '';
const status = b.connected ? 'connected' : 'disconnected';
response.appendResponseLine(`- ${b.id}: ${status}${marker}`);
}
},
});

export const add_browser = defineTool({
name: 'add_browser',
description:
'Connect to an additional Chrome browser instance on a different debugging port.',
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: false,
},
schema: {
browserId: zod
.string()
.describe(
'A unique ID for this browser instance (e.g. "admin", "test-user", "dev").',
),
browserUrl: zod
.string()
.describe(
'The debugging URL of the Chrome instance (e.g. "http://127.0.0.1:9224").',
),
switchTo: zod
.boolean()
.optional()
.describe(
'Whether to immediately switch to this browser after connecting. Default is true.',
),
},
handler: async (request, response) => {
const {browserId, browserUrl, switchTo = true} = request.params;
try {
await addBrowser(browserId, {browserURL: browserUrl});
response.appendResponseLine(
`Browser '${browserId}' connected successfully at ${browserUrl}.`,
);
if (switchTo) {
switchActiveBrowser(browserId);
response.appendResponseLine(`Switched to browser '${browserId}'.`);
}
const browsers = listBrowsers();
response.appendResponseLine('\n## All browsers');
for (const b of browsers) {
const marker = b.active ? ' [selected]' : '';
response.appendResponseLine(
`- ${b.id}: ${b.connected ? 'connected' : 'disconnected'}${marker}`,
);
}
} catch (err) {
throw new Error(
`Failed to connect to browser '${browserId}' at ${browserUrl}: ${(err as Error).message}`,
);
}
},
});
2 changes: 2 additions & 0 deletions src/tools/slim/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {zod} from '../../third_party/index.js';
import {ToolCategory} from '../categories.js';
import {definePageTool} from '../ToolDefinition.js';

export {switch_browser, list_browsers, add_browser} from '../multibrowser.js';

export const screenshot = definePageTool({
name: 'screenshot',
description: `Takes a screenshot`,
Expand Down
2 changes: 2 additions & 0 deletions src/tools/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as screenshotTools from './screenshot.js';
import * as scriptTools from './script.js';
import * as slimTools from './slim/tools.js';
import * as snapshotTools from './snapshot.js';
import * as multibrowserTools from './multibrowser.js';
import type {ToolDefinition} from './ToolDefinition.js';

export const createTools = (args: ParsedArguments) => {
Expand All @@ -39,6 +40,7 @@ export const createTools = (args: ParsedArguments) => {
...Object.values(screenshotTools),
...Object.values(scriptTools),
...Object.values(snapshotTools),
...Object.values(multibrowserTools),
];

const tools = [];
Expand Down
Loading