Skip to content

Commit c29e1e6

Browse files
jaidhyaniJai
authored andcommitted
fix: tolerate unresponsive tabs when connecting to existing browser
When connecting to a running browser via --browserUrl, browser.pages() hangs indefinitely if any tab is unresponsive (discarded by memory management, dead localhost, etc). This blocks all tool calls since page enumeration runs on every request. Two changes: 1. Add protocolTimeout (15s) to puppeteer.connect() so CDP commands fail with an error instead of hanging forever. 2. Wrap browser.pages() in #getAllPages() with a 10s timeout. If it fails, fall back to per-target enumeration: iterate browser.targets(), call target.page() on each with a 5s timeout, and skip tabs that don't respond. This way one unresponsive tab doesn't block the entire server. Tested with Chrome/Brave 145, 28 open tabs (5 discarded). Without this fix the server hangs forever on the first tool call. With the fix it returns 23 responsive pages in ~12s and logs the 5 it skipped. Related: puppeteer/puppeteer#5633, puppeteer/puppeteer#14708, #870, #978
1 parent 9236834 commit c29e1e6

5 files changed

Lines changed: 65 additions & 3 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,11 @@ The Chrome DevTools MCP server supports the following configuration option:
560560
If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.
561561
- **Type:** boolean
562562

563+
- **`--skipUnresponsiveTabs`/ `--skip-unresponsive-tabs`**
564+
When connecting to an existing browser, skip tabs that don't respond to CDP commands (e.g. discarded or sleeping tabs) instead of hanging indefinitely. Recommended when using `--browserUrl` or `--wsEndpoint` with a browser that has many open tabs.
565+
- **Type:** boolean
566+
- **Default:** false
567+
563568
- **`--experimentalVision`/ `--experimental-vision`**
564569
Whether to enable coordinate-based tools such as click_at(x,y). Usually requires a computer-use model able to produce accurate coordinates by looking at screenshots.
565570
- **Type:** boolean
@@ -725,6 +730,8 @@ trace.
725730

726731
You can connect to a running Chrome instance by using the `--browser-url` option. This is useful if you are running the MCP server in a sandboxed environment that does not allow starting a new Chrome instance.
727732

733+
If you have many tabs open, some may be discarded by the browser to save memory. These tabs don't respond to CDP commands, which can cause the MCP server to hang on startup. Use `--skip-unresponsive-tabs` to automatically detect and skip these tabs.
734+
728735
Here is a step-by-step guide on how to connect to a running Chrome instance:
729736

730737
**Step 1: Configure the MCP client**

src/McpContext.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ interface McpContextOptions {
5656
experimentalIncludeAllPages?: boolean;
5757
// Whether CrUX data should be fetched.
5858
performanceCrux: boolean;
59+
// Whether to skip unresponsive tabs when enumerating pages.
60+
skipUnresponsiveTabs?: boolean;
5961
}
6062

6163
const DEFAULT_TIMEOUT = 5_000;
@@ -562,9 +564,53 @@ export class McpContext implements Context {
562564
isolatedContextNames: Map<Page, string>;
563565
}> {
564566
const defaultCtx = this.browser.defaultBrowserContext();
565-
const allPages = await this.browser.pages(
566-
this.#options.experimentalIncludeAllPages,
567-
);
567+
568+
let allPages: Page[];
569+
if (this.#options.skipUnresponsiveTabs) {
570+
// Tolerant page enumeration: browser.pages() hangs indefinitely when
571+
// any tab is unresponsive (e.g. discarded/sleeping tabs). Fall back to
572+
// per-target enumeration so that one bad tab doesn't block the server.
573+
try {
574+
allPages = await Promise.race([
575+
this.browser.pages(this.#options.experimentalIncludeAllPages),
576+
new Promise<never>((_, reject) =>
577+
setTimeout(
578+
() => reject(new Error('browser.pages() timed out')),
579+
10_000,
580+
),
581+
),
582+
]);
583+
} catch {
584+
this.logger(
585+
'browser.pages() timed out — falling back to per-target enumeration',
586+
);
587+
const pageTargets = this.browser
588+
.targets()
589+
.filter(t => t.type() === 'page');
590+
allPages = [];
591+
await Promise.all(
592+
pageTargets.map(async target => {
593+
try {
594+
const page = await Promise.race([
595+
target.page(),
596+
new Promise<never>((_, reject) =>
597+
setTimeout(() => reject(new Error('timeout')), 5_000),
598+
),
599+
]);
600+
if (page) {
601+
allPages.push(page);
602+
}
603+
} catch {
604+
this.logger(`Skipping unresponsive tab: ${target.url()}`);
605+
}
606+
}),
607+
);
608+
}
609+
} else {
610+
allPages = await this.browser.pages(
611+
this.#options.experimentalIncludeAllPages,
612+
);
613+
}
568614

569615
const allTargets = this.browser.targets();
570616
const extensionTargets = allTargets.filter(target => {

src/bin/chrome-devtools-mcp-cli-options.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ export const cliOptions = {
147147
type: 'boolean',
148148
description: `If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.`,
149149
},
150+
skipUnresponsiveTabs: {
151+
type: 'boolean',
152+
description: `When connecting to an existing browser, skip tabs that don't respond to CDP commands (e.g. discarded or sleeping tabs) instead of hanging. Recommended when using --browserUrl with many open tabs.`,
153+
default: false,
154+
},
150155
experimentalPageIdRouting: {
151156
type: 'boolean',
152157
describe:

src/browser.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export async function ensureBrowserConnected(options: {
5151
channel?: Channel;
5252
userDataDir?: string;
5353
enableExtensions?: boolean;
54+
skipUnresponsiveTabs?: boolean;
5455
}) {
5556
const {channel, enableExtensions} = options;
5657
if (browser?.connected) {
@@ -61,6 +62,7 @@ export async function ensureBrowserConnected(options: {
6162
targetFilter: makeTargetFilter(enableExtensions),
6263
defaultViewport: null,
6364
handleDevToolsAsPage: true,
65+
...(options.skipUnresponsiveTabs ? {protocolTimeout: 15_000} : {}),
6466
};
6567

6668
let autoConnect = false;

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export async function createMcpServer(
8686
: undefined,
8787
userDataDir: serverArgs.userDataDir,
8888
devtools,
89+
skipUnresponsiveTabs: serverArgs.skipUnresponsiveTabs,
8990
})
9091
: await ensureBrowserLaunched({
9192
headless: serverArgs.headless,
@@ -108,6 +109,7 @@ export async function createMcpServer(
108109
experimentalDevToolsDebugging: devtools,
109110
experimentalIncludeAllPages: serverArgs.experimentalIncludeAllPages,
110111
performanceCrux: serverArgs.performanceCrux,
112+
skipUnresponsiveTabs: serverArgs.skipUnresponsiveTabs,
111113
});
112114
}
113115
return context;

0 commit comments

Comments
 (0)