Skip to content

Commit c1ab72f

Browse files
committed
Sync with upstream and add multi-browser support in TypeScript
Rebased on ChromeDevTools/chrome-devtools-mcp v0.20.3 (678 commits). Multi-browser modifications now in TypeScript source: - src/browser.ts: browser registry (Map<id, Browser>), switchActiveBrowser, listBrowsers, addBrowser, getActiveBrowserId - src/index.ts: contextRegistry for per-browser McpContext - src/tools/multibrowser.ts: add_browser, switch_browser, list_browsers tools - src/tools/tools.ts: register multi-browser tools (full mode) - src/tools/slim/tools.ts: register multi-browser tools (slim mode)
1 parent 9a47b65 commit c1ab72f

5 files changed

Lines changed: 205 additions & 2 deletions

File tree

src/browser.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,81 @@ import {puppeteer} from './third_party/index.js';
2020

2121
let browser: Browser | undefined;
2222

23+
// Multi-browser registry
24+
const browserRegistry = new Map<string, Browser>();
25+
let activeBrowserId = 'default';
26+
27+
export function switchActiveBrowser(browserId: string): Browser {
28+
if (!browserRegistry.has(browserId)) {
29+
const available = [...browserRegistry.keys()];
30+
if (available.length === 0) {
31+
throw new Error(
32+
`Browser '${browserId}' not found. No browsers are currently registered.`,
33+
);
34+
}
35+
throw new Error(
36+
`Browser '${browserId}' not found. Available browsers: ${available.join(', ')}`,
37+
);
38+
}
39+
const targetBrowser = browserRegistry.get(browserId)!;
40+
if (!targetBrowser?.connected) {
41+
throw new Error(
42+
`Browser '${browserId}' is disconnected. Remove it and add again with add_browser.`,
43+
);
44+
}
45+
activeBrowserId = browserId;
46+
browser = targetBrowser;
47+
return browser;
48+
}
49+
50+
export interface BrowserInfo {
51+
id: string;
52+
connected: boolean;
53+
active: boolean;
54+
}
55+
56+
export function listBrowsers(): BrowserInfo[] {
57+
const result: BrowserInfo[] = [];
58+
for (const [id, b] of browserRegistry.entries()) {
59+
result.push({
60+
id,
61+
connected: b?.connected ?? false,
62+
active: id === activeBrowserId,
63+
});
64+
}
65+
return result;
66+
}
67+
68+
export async function addBrowser(
69+
browserId: string,
70+
options: {browserURL?: string; wsEndpoint?: string; enableExtensions?: boolean},
71+
): Promise<Browser> {
72+
const connectOptions: Parameters<typeof puppeteer.connect>[0] = {
73+
targetFilter: makeTargetFilter(options.enableExtensions ?? false),
74+
defaultViewport: null,
75+
handleDevToolsAsPage: true,
76+
};
77+
if (options.browserURL) {
78+
connectOptions.browserURL = options.browserURL;
79+
} else if (options.wsEndpoint) {
80+
connectOptions.browserWSEndpoint = options.wsEndpoint;
81+
} else {
82+
throw new Error('Either browserURL or wsEndpoint must be provided');
83+
}
84+
logger('Adding browser', browserId, JSON.stringify(connectOptions));
85+
const newBrowser = await puppeteer.connect(connectOptions);
86+
browserRegistry.set(browserId, newBrowser);
87+
if (!browser || browserRegistry.size === 1) {
88+
browser = newBrowser;
89+
activeBrowserId = browserId;
90+
}
91+
return newBrowser;
92+
}
93+
94+
export function getActiveBrowserId(): string {
95+
return activeBrowserId;
96+
}
97+
2398
function makeTargetFilter(enableExtensions = false) {
2499
const ignoredPrefixes = new Set(['chrome://', 'chrome-untrusted://']);
25100
if (!enableExtensions) {
@@ -130,6 +205,7 @@ export async function ensureBrowserConnected(options: {
130205
);
131206
}
132207
logger('Connected Puppeteer');
208+
browserRegistry.set(activeBrowserId, browser);
133209
return browser;
134210
}
135211

@@ -267,6 +343,7 @@ export async function ensureBrowserLaunched(
267343
return browser;
268344
}
269345
browser = await launch(options);
346+
browserRegistry.set(activeBrowserId, browser);
270347
return browser;
271348
}
272349

src/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type fs from 'node:fs';
88

99
import type {parseArguments} from './bin/chrome-devtools-mcp-cli-options.js';
1010
import type {Channel} from './browser.js';
11-
import {ensureBrowserConnected, ensureBrowserLaunched} from './browser.js';
11+
import {ensureBrowserConnected, ensureBrowserLaunched, getActiveBrowserId} from './browser.js';
1212
import {loadIssueDescriptions} from './issue-descriptions.js';
1313
import {logger} from './logger.js';
1414
import {McpContext} from './McpContext.js';
@@ -65,6 +65,7 @@ export async function createMcpServer(
6565
};
6666

6767
let context: McpContext;
68+
const contextRegistry = new Map<string, McpContext>();
6869
async function getContext(): Promise<McpContext> {
6970
const chromeArgs: string[] = (serverArgs.chromeArg ?? []).map(String);
7071
const ignoreDefaultChromeArgs: string[] = (
@@ -103,12 +104,17 @@ export async function createMcpServer(
103104
viaCli: serverArgs.viaCli,
104105
});
105106

106-
if (context?.browser !== browser) {
107+
const browserId = getActiveBrowserId();
108+
const existingContext = contextRegistry.get(browserId);
109+
if (existingContext?.browser === browser && browser?.connected) {
110+
context = existingContext;
111+
} else if (context?.browser !== browser) {
107112
context = await McpContext.from(browser, logger, {
108113
experimentalDevToolsDebugging: devtools,
109114
experimentalIncludeAllPages: serverArgs.experimentalIncludeAllPages,
110115
performanceCrux: serverArgs.performanceCrux,
111116
});
117+
contextRegistry.set(browserId, context);
112118
}
113119
return context;
114120
}

src/tools/multibrowser.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {
8+
switchActiveBrowser,
9+
listBrowsers,
10+
addBrowser,
11+
} from '../browser.js';
12+
import {zod} from '../third_party/index.js';
13+
import {ToolCategory} from './categories.js';
14+
import {defineTool} from './ToolDefinition.js';
15+
16+
export const switch_browser = defineTool({
17+
name: 'switch_browser',
18+
description:
19+
'Switch the active browser instance. All subsequent tool calls will operate on the selected browser.',
20+
annotations: {
21+
category: ToolCategory.NAVIGATION,
22+
readOnlyHint: false,
23+
},
24+
schema: {
25+
browserId: zod
26+
.string()
27+
.describe(
28+
'The ID of the browser to switch to. Call list_browsers to see available browsers.',
29+
),
30+
},
31+
handler: async (request, response) => {
32+
const {browserId} = request.params;
33+
switchActiveBrowser(browserId);
34+
response.appendResponseLine(
35+
`Switched to browser '${browserId}'. All future tool calls will target this browser.`,
36+
);
37+
response.appendResponseLine(`Active browser: ${browserId}`);
38+
},
39+
});
40+
41+
export const list_browsers = defineTool({
42+
name: 'list_browsers',
43+
description:
44+
'List all connected browser instances with their IDs and connection status.',
45+
annotations: {
46+
category: ToolCategory.NAVIGATION,
47+
readOnlyHint: true,
48+
},
49+
schema: {},
50+
handler: async (_request, response) => {
51+
const browsers = listBrowsers();
52+
if (browsers.length === 0) {
53+
response.appendResponseLine('No browsers connected.');
54+
return;
55+
}
56+
response.appendResponseLine('## Connected browsers');
57+
for (const b of browsers) {
58+
const marker = b.active ? ' [selected]' : '';
59+
const status = b.connected ? 'connected' : 'disconnected';
60+
response.appendResponseLine(`- ${b.id}: ${status}${marker}`);
61+
}
62+
},
63+
});
64+
65+
export const add_browser = defineTool({
66+
name: 'add_browser',
67+
description:
68+
'Connect to an additional Chrome browser instance on a different debugging port.',
69+
annotations: {
70+
category: ToolCategory.NAVIGATION,
71+
readOnlyHint: false,
72+
},
73+
schema: {
74+
browserId: zod
75+
.string()
76+
.describe(
77+
'A unique ID for this browser instance (e.g. "admin", "test-user", "dev").',
78+
),
79+
browserUrl: zod
80+
.string()
81+
.describe(
82+
'The debugging URL of the Chrome instance (e.g. "http://127.0.0.1:9224").',
83+
),
84+
switchTo: zod
85+
.boolean()
86+
.optional()
87+
.describe(
88+
'Whether to immediately switch to this browser after connecting. Default is true.',
89+
),
90+
},
91+
handler: async (request, response) => {
92+
const {browserId, browserUrl, switchTo = true} = request.params;
93+
try {
94+
await addBrowser(browserId, {browserURL: browserUrl});
95+
response.appendResponseLine(
96+
`Browser '${browserId}' connected successfully at ${browserUrl}.`,
97+
);
98+
if (switchTo) {
99+
switchActiveBrowser(browserId);
100+
response.appendResponseLine(`Switched to browser '${browserId}'.`);
101+
}
102+
const browsers = listBrowsers();
103+
response.appendResponseLine('\n## All browsers');
104+
for (const b of browsers) {
105+
const marker = b.active ? ' [selected]' : '';
106+
response.appendResponseLine(
107+
`- ${b.id}: ${b.connected ? 'connected' : 'disconnected'}${marker}`,
108+
);
109+
}
110+
} catch (err) {
111+
throw new Error(
112+
`Failed to connect to browser '${browserId}' at ${browserUrl}: ${(err as Error).message}`,
113+
);
114+
}
115+
},
116+
});

src/tools/slim/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {zod} from '../../third_party/index.js';
99
import {ToolCategory} from '../categories.js';
1010
import {definePageTool} from '../ToolDefinition.js';
1111

12+
export {switch_browser, list_browsers, add_browser} from '../multibrowser.js';
13+
1214
export const screenshot = definePageTool({
1315
name: 'screenshot',
1416
description: `Takes a screenshot`,

src/tools/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import * as screenshotTools from './screenshot.js';
2020
import * as scriptTools from './script.js';
2121
import * as slimTools from './slim/tools.js';
2222
import * as snapshotTools from './snapshot.js';
23+
import * as multibrowserTools from './multibrowser.js';
2324
import type {ToolDefinition} from './ToolDefinition.js';
2425

2526
export const createTools = (args: ParsedArguments) => {
@@ -39,6 +40,7 @@ export const createTools = (args: ParsedArguments) => {
3940
...Object.values(screenshotTools),
4041
...Object.values(scriptTools),
4142
...Object.values(snapshotTools),
43+
...Object.values(multibrowserTools),
4244
];
4345

4446
const tools = [];

0 commit comments

Comments
 (0)