Skip to content

Commit 15ce618

Browse files
committed
fix: fall back to auto connect after browser url
1 parent c96569b commit 15ce618

5 files changed

Lines changed: 140 additions & 33 deletions

File tree

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,8 @@ export function parseArguments(version: string, argv = process.argv) {
300300
// Yargs will complain
301301
if (
302302
!args.channel &&
303-
!args.browserUrl &&
304-
!args.wsEndpoint &&
305-
!args.executablePath
303+
(args.autoConnect ||
304+
(!args.browserUrl && !args.wsEndpoint && !args.executablePath))
306305
) {
307306
args.channel = 'stable';
308307
}

src/browser.ts

Lines changed: 68 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,24 @@ function makeTargetFilter(enableExtensions = false) {
4343
};
4444
}
4545

46+
function toPuppeteerChannel(channel: Channel): ChromeReleaseChannel {
47+
switch (channel) {
48+
case 'canary':
49+
return 'chrome-canary';
50+
case 'dev':
51+
return 'chrome-dev';
52+
case 'beta':
53+
return 'chrome-beta';
54+
case 'stable':
55+
return 'chrome';
56+
}
57+
}
58+
4659
export async function ensureBrowserConnected(options: {
4760
browserURL?: string;
4861
wsEndpoint?: string;
4962
wsHeaders?: Record<string, string>;
63+
autoConnect?: boolean;
5064
devtools: boolean;
5165
channel?: Channel;
5266
userDataDir?: string;
@@ -57,24 +71,62 @@ export async function ensureBrowserConnected(options: {
5771
return browser;
5872
}
5973

60-
const connectOptions: Parameters<typeof puppeteer.connect>[0] = {
61-
targetFilter: makeTargetFilter(enableExtensions),
62-
defaultViewport: null,
63-
handleDevToolsAsPage: true,
74+
const createConnectOptions = (): Parameters<typeof puppeteer.connect>[0] => {
75+
return {
76+
targetFilter: makeTargetFilter(enableExtensions),
77+
defaultViewport: null,
78+
handleDevToolsAsPage: true,
79+
};
80+
};
81+
82+
const connect = async (
83+
connectOptions: Parameters<typeof puppeteer.connect>[0],
84+
): Promise<Browser> => {
85+
logger('Connecting Puppeteer to ', JSON.stringify(connectOptions));
86+
const connectedBrowser = await puppeteer.connect(connectOptions);
87+
logger('Connected Puppeteer');
88+
return connectedBrowser;
6489
};
6590

66-
let autoConnect = false;
67-
if (options.wsEndpoint) {
68-
connectOptions.browserWSEndpoint = options.wsEndpoint;
69-
if (options.wsHeaders) {
70-
connectOptions.headers = options.wsHeaders;
91+
const connectionError = (autoConnect: boolean, cause: unknown): Error => {
92+
return new Error(
93+
`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.`}`,
94+
{
95+
cause,
96+
},
97+
);
98+
};
99+
100+
if (options.wsEndpoint || options.browserURL) {
101+
const connectOptions = createConnectOptions();
102+
if (options.wsEndpoint) {
103+
connectOptions.browserWSEndpoint = options.wsEndpoint;
104+
if (options.wsHeaders) {
105+
connectOptions.headers = options.wsHeaders;
106+
}
107+
} else {
108+
connectOptions.browserURL = options.browserURL;
71109
}
72-
} else if (options.browserURL) {
73-
connectOptions.browserURL = options.browserURL;
74-
} else if (channel || options.userDataDir) {
110+
try {
111+
browser = await connect(connectOptions);
112+
return browser;
113+
} catch (error) {
114+
if (!options.autoConnect) {
115+
throw connectionError(false, error);
116+
}
117+
logger(
118+
'Direct browser connection failed; falling back to auto-connect',
119+
error,
120+
);
121+
}
122+
}
123+
124+
const connectOptions = createConnectOptions();
125+
let autoConnect = false;
126+
if (channel || options.userDataDir || options.autoConnect) {
75127
const userDataDir = options.userDataDir;
128+
autoConnect = true;
76129
if (userDataDir) {
77-
autoConnect = true;
78130
// TODO: re-expose this logic via Puppeteer.
79131
const portPath = path.join(userDataDir, 'DevToolsActivePort');
80132
try {
@@ -105,31 +157,19 @@ export async function ensureBrowserConnected(options: {
105157
);
106158
}
107159
} else {
108-
if (!channel) {
109-
throw new Error('Channel must be provided if userDataDir is missing');
110-
}
111-
connectOptions.channel = (
112-
channel === 'stable' ? 'chrome' : `chrome-${channel}`
113-
) as ChromeReleaseChannel;
160+
connectOptions.channel = toPuppeteerChannel(channel ?? 'stable');
114161
}
115162
} else {
116163
throw new Error(
117164
'Either browserURL, wsEndpoint, channel or userDataDir must be provided',
118165
);
119166
}
120167

121-
logger('Connecting Puppeteer to ', JSON.stringify(connectOptions));
122168
try {
123-
browser = await puppeteer.connect(connectOptions);
169+
browser = await connect(connectOptions);
124170
} catch (err) {
125-
throw new Error(
126-
`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.`}`,
127-
{
128-
cause: err,
129-
},
130-
);
171+
throw connectionError(autoConnect, err);
131172
}
132-
logger('Connected Puppeteer');
133173
return browser;
134174
}
135175

src/index.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ import {pageIdSchema} from './tools/ToolDefinition.js';
3030
import {createTools} from './tools/tools.js';
3131
import {VERSION} from './version.js';
3232

33+
function resolveChannel(channel: unknown): Channel {
34+
switch (channel) {
35+
case 'canary':
36+
return 'canary';
37+
case 'dev':
38+
return 'dev';
39+
case 'beta':
40+
return 'beta';
41+
case 'stable':
42+
default:
43+
return 'stable';
44+
}
45+
}
46+
3347
export async function createMcpServer(
3448
serverArgs: ReturnType<typeof parseArguments>,
3549
options: {
@@ -106,9 +120,10 @@ export async function createMcpServer(
106120
browserURL: serverArgs.browserUrl,
107121
wsEndpoint: serverArgs.wsEndpoint,
108122
wsHeaders: serverArgs.wsHeaders,
123+
autoConnect: serverArgs.autoConnect,
109124
// Important: only pass channel, if autoConnect is true.
110125
channel: serverArgs.autoConnect
111-
? (serverArgs.channel as Channel)
126+
? resolveChannel(serverArgs.channel)
112127
: undefined,
113128
userDataDir: serverArgs.userDataDir,
114129
devtools,

tests/browser.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@
77
import assert from 'node:assert';
88
import os from 'node:os';
99
import path from 'node:path';
10-
import {describe, it} from 'node:test';
10+
import {afterEach, describe, it} from 'node:test';
1111

1212
import {executablePath} from 'puppeteer';
13+
import sinon from 'sinon';
1314

1415
import {detectDisplay, ensureBrowserConnected, launch} from '../src/browser.js';
16+
import {puppeteer} from '../src/third_party/index.js';
1517

1618
describe('browser', () => {
19+
afterEach(() => {
20+
sinon.restore();
21+
});
22+
1723
it('detects display does not crash', () => {
1824
detectDisplay();
1925
});
@@ -100,4 +106,29 @@ describe('browser', () => {
100106
await browser.close();
101107
}
102108
});
109+
110+
it('falls back to auto-connect when browser url cannot connect', async () => {
111+
const connect = sinon.stub(puppeteer, 'connect');
112+
connect.onFirstCall().rejects(new Error('port unavailable'));
113+
connect.onSecondCall().rejects(new Error('auto-connect unavailable'));
114+
115+
await assert.rejects(
116+
ensureBrowserConnected({
117+
browserURL: 'http://127.0.0.1:9222',
118+
autoConnect: true,
119+
channel: 'stable',
120+
devtools: false,
121+
}),
122+
/Could not connect to Chrome/,
123+
);
124+
125+
assert.strictEqual(connect.callCount, 2);
126+
assert.strictEqual(
127+
connect.firstCall.args[0].browserURL,
128+
'http://127.0.0.1:9222',
129+
);
130+
assert.strictEqual(connect.firstCall.args[0].channel, undefined);
131+
assert.strictEqual(connect.secondCall.args[0].browserURL, undefined);
132+
assert.strictEqual(connect.secondCall.args[0].channel, 'chrome');
133+
});
103134
});

tests/cli.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,28 @@ describe('cli args parsing', () => {
255255
});
256256
});
257257

258+
it('uses stable channel for browser url with auto-connect fallback', async () => {
259+
const args = parseArguments('1.0.0', [
260+
'node',
261+
'main.js',
262+
'--browserUrl',
263+
'http://127.0.0.1:9222',
264+
'--auto-connect',
265+
]);
266+
assert.deepStrictEqual(args, {
267+
...defaultArgs,
268+
_: [],
269+
headless: false,
270+
$0: 'npx chrome-devtools-mcp@latest',
271+
'browser-url': 'http://127.0.0.1:9222',
272+
browserUrl: 'http://127.0.0.1:9222',
273+
u: 'http://127.0.0.1:9222',
274+
channel: 'stable',
275+
'auto-connect': true,
276+
autoConnect: true,
277+
});
278+
});
279+
258280
it('parses usage statistics flag', async () => {
259281
// Test default (should be true).
260282
const defaultArgs = parseArguments('1.0.0', ['node', 'main.js']);

0 commit comments

Comments
 (0)