Skip to content

Commit 1d9a0f8

Browse files
committed
fix: resolve localhost to 127.0.0.1 in browserUrl and wsEndpoint
On macOS, Node.js dns.lookup('localhost') non-deterministically resolves to ::1 (IPv6) or 127.0.0.1 (IPv4) depending on network state. Chrome's debug port typically binds only one address family, causing intermittent connection failures when users pass --browserUrl http://localhost:9222. This adds a resolveLocalhostToIPv4() helper that normalizes localhost to 127.0.0.1 in both --browserUrl and --wsEndpoint coerce functions, while preserving explicit 127.0.0.1 and [::1] addresses.
1 parent 9236834 commit 1d9a0f8

2 files changed

Lines changed: 70 additions & 5 deletions

File tree

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@
77
import type {YargsOptions} from '../third_party/index.js';
88
import {yargs, hideBin} from '../third_party/index.js';
99

10+
/**
11+
* On macOS, Node.js `dns.lookup('localhost')` non-deterministically resolves
12+
* to `::1` (IPv6) or `127.0.0.1` (IPv4) depending on network state. Chrome's
13+
* debug port typically binds only one address family, causing intermittent
14+
* connection failures. This function replaces `localhost` with `127.0.0.1`
15+
* to match Chrome's default binding while preserving the original URL format.
16+
*/
17+
function resolveLocalhostToIPv4(url: string): string {
18+
return url.replace(/\/\/localhost([:/])/i, '//127.0.0.1$1');
19+
}
20+
1021
export const cliOptions = {
1122
autoConnect: {
1223
type: 'boolean',
@@ -36,7 +47,7 @@ export const cliOptions = {
3647
} catch {
3748
throw new Error(`Provided browserUrl ${url} is not valid URL.`);
3849
}
39-
return url;
50+
return resolveLocalhostToIPv4(url);
4051
},
4152
},
4253
wsEndpoint: {
@@ -56,7 +67,7 @@ export const cliOptions = {
5667
`Provided wsEndpoint ${url} must use ws:// or wss:// protocol.`,
5768
);
5869
}
59-
return url;
70+
return resolveLocalhostToIPv4(url);
6071
} catch (error) {
6172
if ((error as Error).message.includes('ws://')) {
6273
throw error;

tests/cli.test.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,58 @@ describe('cli args parsing', () => {
4343
'--browserUrl',
4444
'http://localhost:3000',
4545
]);
46+
// localhost is resolved to 127.0.0.1 to avoid IPv6 resolution issues
4647
assert.deepStrictEqual(args, {
4748
...defaultArgs,
4849
_: [],
4950
headless: false,
5051
$0: 'npx chrome-devtools-mcp@latest',
51-
'browser-url': 'http://localhost:3000',
52-
browserUrl: 'http://localhost:3000',
53-
u: 'http://localhost:3000',
52+
'browser-url': 'http://127.0.0.1:3000',
53+
browserUrl: 'http://127.0.0.1:3000',
54+
u: 'http://127.0.0.1:3000',
5455
});
5556
});
5657

58+
it('resolves localhost to 127.0.0.1 in browser url', async () => {
59+
const args = parseArguments('1.0.0', [
60+
'node',
61+
'main.js',
62+
'--browserUrl',
63+
'http://localhost:9222',
64+
]);
65+
assert.strictEqual(args.browserUrl, 'http://127.0.0.1:9222');
66+
});
67+
68+
it('resolves LOCALHOST to 127.0.0.1 in browser url', async () => {
69+
const args = parseArguments('1.0.0', [
70+
'node',
71+
'main.js',
72+
'--browserUrl',
73+
'http://LOCALHOST:9222',
74+
]);
75+
assert.strictEqual(args.browserUrl, 'http://127.0.0.1:9222');
76+
});
77+
78+
it('preserves explicit 127.0.0.1 in browser url', async () => {
79+
const args = parseArguments('1.0.0', [
80+
'node',
81+
'main.js',
82+
'--browserUrl',
83+
'http://127.0.0.1:9222',
84+
]);
85+
assert.strictEqual(args.browserUrl, 'http://127.0.0.1:9222');
86+
});
87+
88+
it('preserves explicit [::1] in browser url', async () => {
89+
const args = parseArguments('1.0.0', [
90+
'node',
91+
'main.js',
92+
'--browserUrl',
93+
'http://[::1]:9222',
94+
]);
95+
assert.strictEqual(args.browserUrl, 'http://[::1]:9222');
96+
});
97+
5798
it('parses with user data dir', async () => {
5899
const args = parseArguments('1.0.0', [
59100
'node',
@@ -207,6 +248,19 @@ describe('cli args parsing', () => {
207248
});
208249
});
209250

251+
it('resolves localhost to 127.0.0.1 in wsEndpoint', async () => {
252+
const args = parseArguments('1.0.0', [
253+
'node',
254+
'main.js',
255+
'--wsEndpoint',
256+
'ws://localhost:9222/devtools/browser/abc123',
257+
]);
258+
assert.strictEqual(
259+
args.wsEndpoint,
260+
'ws://127.0.0.1:9222/devtools/browser/abc123',
261+
);
262+
});
263+
210264
it('parses wsHeaders with valid JSON', async () => {
211265
const args = parseArguments('1.0.0', [
212266
'node',

0 commit comments

Comments
 (0)