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
15 changes: 13 additions & 2 deletions src/bin/chrome-devtools-mcp-cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@
import type {YargsOptions} from '../third_party/index.js';
import {yargs, hideBin} from '../third_party/index.js';

/**
* 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. This function replaces `localhost` with `127.0.0.1`
* to match Chrome's default binding while preserving the original URL format.
*/
function resolveLocalhostToIPv4(url: string): string {
return url.replace(/\/\/localhost([:/])/i, '//127.0.0.1$1');
}

export const cliOptions = {
autoConnect: {
type: 'boolean',
Expand Down Expand Up @@ -36,7 +47,7 @@ export const cliOptions = {
} catch {
throw new Error(`Provided browserUrl ${url} is not valid URL.`);
}
return url;
return resolveLocalhostToIPv4(url);
},
},
wsEndpoint: {
Expand All @@ -56,7 +67,7 @@ export const cliOptions = {
`Provided wsEndpoint ${url} must use ws:// or wss:// protocol.`,
);
}
return url;
return resolveLocalhostToIPv4(url);
} catch (error) {
if ((error as Error).message.includes('ws://')) {
throw error;
Expand Down
60 changes: 57 additions & 3 deletions tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,58 @@ describe('cli args parsing', () => {
'--browserUrl',
'http://localhost:3000',
]);
// localhost is resolved to 127.0.0.1 to avoid IPv6 resolution issues
assert.deepStrictEqual(args, {
...defaultArgs,
_: [],
headless: false,
$0: 'npx chrome-devtools-mcp@latest',
'browser-url': 'http://localhost:3000',
browserUrl: 'http://localhost:3000',
u: 'http://localhost:3000',
'browser-url': 'http://127.0.0.1:3000',
browserUrl: 'http://127.0.0.1:3000',
u: 'http://127.0.0.1:3000',
});
});

it('resolves localhost to 127.0.0.1 in browser url', async () => {
const args = parseArguments('1.0.0', [
'node',
'main.js',
'--browserUrl',
'http://localhost:9222',
]);
assert.strictEqual(args.browserUrl, 'http://127.0.0.1:9222');
});

it('resolves LOCALHOST to 127.0.0.1 in browser url', async () => {
const args = parseArguments('1.0.0', [
'node',
'main.js',
'--browserUrl',
'http://LOCALHOST:9222',
]);
assert.strictEqual(args.browserUrl, 'http://127.0.0.1:9222');
});

it('preserves explicit 127.0.0.1 in browser url', async () => {
const args = parseArguments('1.0.0', [
'node',
'main.js',
'--browserUrl',
'http://127.0.0.1:9222',
]);
assert.strictEqual(args.browserUrl, 'http://127.0.0.1:9222');
});

it('preserves explicit [::1] in browser url', async () => {
const args = parseArguments('1.0.0', [
'node',
'main.js',
'--browserUrl',
'http://[::1]:9222',
]);
assert.strictEqual(args.browserUrl, 'http://[::1]:9222');
});

it('parses with user data dir', async () => {
const args = parseArguments('1.0.0', [
'node',
Expand Down Expand Up @@ -207,6 +248,19 @@ describe('cli args parsing', () => {
});
});

it('resolves localhost to 127.0.0.1 in wsEndpoint', async () => {
const args = parseArguments('1.0.0', [
'node',
'main.js',
'--wsEndpoint',
'ws://localhost:9222/devtools/browser/abc123',
]);
assert.strictEqual(
args.wsEndpoint,
'ws://127.0.0.1:9222/devtools/browser/abc123',
);
});

it('parses wsHeaders with valid JSON', async () => {
const args = parseArguments('1.0.0', [
'node',
Expand Down