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
27 changes: 21 additions & 6 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,9 +582,18 @@ export class McpContext implements Context {
isolatedContextNames: Map<Page, string>;
}> {
const defaultCtx = this.browser.defaultBrowserContext();
const allPages = await this.browser.pages(
this.#options.experimentalIncludeAllPages,
);
let allPages: Page[];
try {
allPages = await this.browser.pages(
this.#options.experimentalIncludeAllPages,
);
} catch (error) {
this.logger('browser.pages() failed, retrying:', error);
await new Promise(resolve => setTimeout(resolve, 100));
allPages = await this.browser.pages(
this.#options.experimentalIncludeAllPages,
);
}

const allTargets = this.browser.targets();
const extensionTargets = allTargets.filter(target => {
Expand Down Expand Up @@ -657,9 +666,15 @@ export class McpContext implements Context {
return;
}

if (await page.hasDevTools()) {
mcpPage.devToolsPage = await page.openDevTools();
} else {
try {
if (await page.hasDevTools()) {
mcpPage.devToolsPage = await page.openDevTools();
} else {
mcpPage.devToolsPage = undefined;
}
} catch (error) {
// Page may have closed between getAllPages() and this check.
this.logger('Error detecting DevTools for page, skipping:', error);
mcpPage.devToolsPage = undefined;
}
}),
Expand Down
116 changes: 82 additions & 34 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ export async function ensureBrowserConnected(options: {
return browser;
}

// Clear stale browser reference to force fresh reconnection.
if (browser) {
logger('Browser disconnected, clearing stale reference');
browser = undefined;
}

const connectOptions: Parameters<typeof puppeteer.connect>[0] = {
targetFilter: makeTargetFilter(enableExtensions),
defaultViewport: null,
Expand All @@ -75,35 +81,7 @@ export async function ensureBrowserConnected(options: {
const userDataDir = options.userDataDir;
if (userDataDir) {
autoConnect = true;
// TODO: re-expose this logic via Puppeteer.
const portPath = path.join(userDataDir, 'DevToolsActivePort');
try {
const fileContent = await fs.promises.readFile(portPath, 'utf8');
const [rawPort, rawPath] = fileContent
.split('\n')
.map(line => {
return line.trim();
})
.filter(line => {
return !!line;
});
if (!rawPort || !rawPath) {
throw new Error(`Invalid DevToolsActivePort '${fileContent}' found`);
}
const port = parseInt(rawPort, 10);
if (isNaN(port) || port <= 0 || port > 65535) {
throw new Error(`Invalid port '${rawPort}' found`);
}
const browserWSEndpoint = `ws://127.0.0.1:${port}${rawPath}`;
connectOptions.browserWSEndpoint = browserWSEndpoint;
} catch (error) {
throw new Error(
`Could not connect to Chrome in ${userDataDir}. Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.`,
{
cause: error,
},
);
}
await readDevToolsActivePort(userDataDir, connectOptions);
} else {
if (!channel) {
throw new Error('Channel must be provided if userDataDir is missing');
Expand All @@ -119,18 +97,88 @@ export async function ensureBrowserConnected(options: {
}

logger('Connecting Puppeteer to ', JSON.stringify(connectOptions));
try {
browser = await puppeteer.connect(connectOptions);
} catch (err) {
const retryDelays = [500, 1000, 2000];
let lastConnectError: unknown;
for (let attempt = 0; attempt <= retryDelays.length; attempt++) {
try {
browser = await puppeteer.connect(connectOptions);
lastConnectError = undefined;
break;
} catch (err) {
lastConnectError = err;
if (attempt < retryDelays.length) {
logger(
`Connection attempt ${attempt + 1} failed, retrying in ${retryDelays[attempt]}ms...`,
);
await new Promise(resolve =>
setTimeout(resolve, retryDelays[attempt]),
);
// Re-read DevToolsActivePort in case Chrome restarted with a new port.
if (autoConnect && options.userDataDir) {
try {
await readDevToolsActivePort(options.userDataDir, connectOptions);
} catch {
// Will retry connection with existing endpoint.
}
}
}
}
}
if (lastConnectError) {
throw new Error(
`Could not connect to Chrome. ${autoConnect ? `Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.` : `Check if Chrome is running.`}`,
{
cause: err,
cause: lastConnectError,
},
);
}
logger('Connected Puppeteer');
return browser;
// browser is guaranteed to be set here: either puppeteer.connect succeeded
// or lastConnectError was thrown above.
return browser!;
}

async function readDevToolsActivePort(
userDataDir: string,
connectOptions: Parameters<typeof puppeteer.connect>[0],
): Promise<void> {
// TODO: re-expose this logic via Puppeteer.
const portPath = path.join(userDataDir, 'DevToolsActivePort');
let lastError: unknown;
for (let attempt = 0; attempt < 5; attempt++) {
try {
const fileContent = await fs.promises.readFile(portPath, 'utf8');
const [rawPort, rawPath] = fileContent
.split('\n')
.map(line => line.trim())
.filter(line => !!line);
if (!rawPort || !rawPath) {
throw new Error(`Invalid DevToolsActivePort '${fileContent}' found`);
}
const port = parseInt(rawPort, 10);
if (isNaN(port) || port <= 0 || port > 65535) {
throw new Error(`Invalid port '${rawPort}' found`);
}
connectOptions.browserWSEndpoint = `ws://127.0.0.1:${port}${rawPath}`;
return;
} catch (error) {
// Validation errors (invalid port/format) won't self-resolve — fail immediately.
if (error instanceof Error && (error.message.startsWith('Invalid port') || error.message.startsWith('Invalid DevToolsActivePort'))) {
throw error;
}
lastError = error;
if (attempt < 4) {
logger(
`DevToolsActivePort read attempt ${attempt + 1} failed, retrying in 1s...`,
);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
throw new Error(
`Could not connect to Chrome in ${userDataDir}. Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.`,
{cause: lastError},
);
}

interface McpLaunchOptions {
Expand Down
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export async function createMcpServer(
});

if (context?.browser !== browser) {
context?.dispose();
context = await McpContext.from(browser, logger, {
experimentalDevToolsDebugging: devtools,
experimentalIncludeAllPages: serverArgs.experimentalIncludeAllPages,
Expand Down Expand Up @@ -174,7 +175,11 @@ export async function createMcpServer(
logger(`${tool.name} request: ${JSON.stringify(params, null, ' ')}`);
const context = await getContext();
logger(`${tool.name} context: resolved`);
await context.detectOpenDevToolsWindows();
try {
await context.detectOpenDevToolsWindows();
} catch (error) {
logger('detectOpenDevToolsWindows failed, continuing:', error);
}
const response = serverArgs.slim
? new SlimMcpResponse(serverArgs)
: new McpResponse(serverArgs);
Expand Down