chromedevtools--chrome-devtools-mcp
6e56c028cf
## Summary validatePath() in McpContext returned immediately, with no restriction at all, whenever roots() returned undefined. roots() only returns undefined when the connecting MCP client never negotiates the optional roots capability during initialize, which any minimal client can trigger simply by omitting it from its declared capabilities. Since roots() already always appends the OS temp directory to whatever explicit roots are configured, this change makes it return that same default (temp directory only) instead of undefined when no roots have been set. This removes the early return in validatePath() entirely, so path validation now runs unconditionally rather than being conditional on whether the connecting client happened to negotiate a capability it was never required to declare per the MCP spec. Any filePath-accepting tool (take_screenshot, saveFile, and the performance/Lighthouse export tools that route through the same check) had its only path-traversal guard silently disabled for the lifetime of a connection whenever the client omitted the optional roots capability. Since this server is designed to let an LLM drive a browser, and browsed page content is not trusted input, this meant a client that simply doesn't implement roots (a plausible, non-adversarial default for lightweight or custom MCP clients) removed the only boundary preventing the connected agent from writing to any path the process can reach. Added a test that exercises the actual default state of roots (never calling setRoots()) directly, since the existing tests always call setRoots(), even with an empty array, before validating. Verified locally with a minimal MCP client that declares no capabilities: before this change, take_screenshot with a filePath outside any root wrote a real file to an arbitrary path with no error; after this change, the same call is rejected with the existing Access denied error. Also verified that a client that does declare roots is unaffected, and that writes to the OS temp directory continue to succeed with no roots negotiated, matching prior behavior for that path.
441 行
11 KiB
TypeScript
441 行
11 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import assert from 'node:assert';
|
|
import {describe, it} from 'node:test';
|
|
|
|
import {parseArguments} from '../src/bin/chrome-devtools-mcp-cli-options.js';
|
|
|
|
describe('cli args parsing', () => {
|
|
const defaultArgs = {
|
|
'category-emulation': true,
|
|
categoryEmulation: true,
|
|
'category-performance': true,
|
|
categoryPerformance: true,
|
|
'category-network': true,
|
|
categoryNetwork: true,
|
|
'category-extensions': false,
|
|
categoryExtensions: false,
|
|
'category-experimental-third-party': false,
|
|
categoryExperimentalThirdParty: false,
|
|
'auto-connect': undefined,
|
|
autoConnect: undefined,
|
|
'performance-crux': true,
|
|
performanceCrux: true,
|
|
'usage-statistics': true,
|
|
usageStatistics: true,
|
|
'redact-network-headers': false,
|
|
redactNetworkHeaders: false,
|
|
'allow-unrestricted-paths': false,
|
|
allowUnrestrictedPaths: false,
|
|
};
|
|
|
|
it('parses with default args', async () => {
|
|
const args = parseArguments('1.0.0', ['node', 'main.js'], {});
|
|
assert.deepStrictEqual(args, {
|
|
...defaultArgs,
|
|
_: [],
|
|
headless: false,
|
|
$0: 'npx chrome-devtools-mcp@latest',
|
|
channel: 'stable',
|
|
});
|
|
});
|
|
|
|
it('parses with browser url', async () => {
|
|
const args = parseArguments(
|
|
'1.0.0',
|
|
['node', 'main.js', '--browserUrl', 'http://localhost:3000'],
|
|
{},
|
|
);
|
|
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',
|
|
});
|
|
});
|
|
|
|
it('parses with user data dir', async () => {
|
|
const args = parseArguments(
|
|
'1.0.0',
|
|
['node', 'main.js', '--user-data-dir', '/tmp/chrome-profile'],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(args, {
|
|
...defaultArgs,
|
|
_: [],
|
|
headless: false,
|
|
$0: 'npx chrome-devtools-mcp@latest',
|
|
channel: 'stable',
|
|
'user-data-dir': '/tmp/chrome-profile',
|
|
userDataDir: '/tmp/chrome-profile',
|
|
});
|
|
});
|
|
|
|
it('parses an empty browser url', async () => {
|
|
const args = parseArguments(
|
|
'1.0.0',
|
|
['node', 'main.js', '--browserUrl', ''],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(args, {
|
|
...defaultArgs,
|
|
_: [],
|
|
headless: false,
|
|
$0: 'npx chrome-devtools-mcp@latest',
|
|
'browser-url': undefined,
|
|
browserUrl: undefined,
|
|
u: undefined,
|
|
channel: 'stable',
|
|
});
|
|
});
|
|
|
|
it('parses with executable path', async () => {
|
|
const args = parseArguments(
|
|
'1.0.0',
|
|
['node', 'main.js', '--executablePath', '/tmp/test 123/chrome'],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(args, {
|
|
...defaultArgs,
|
|
_: [],
|
|
headless: false,
|
|
$0: 'npx chrome-devtools-mcp@latest',
|
|
'executable-path': '/tmp/test 123/chrome',
|
|
e: '/tmp/test 123/chrome',
|
|
executablePath: '/tmp/test 123/chrome',
|
|
});
|
|
});
|
|
|
|
it('parses viewport', async () => {
|
|
const args = parseArguments(
|
|
'1.0.0',
|
|
['node', 'main.js', '--viewport', '888x777'],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(args, {
|
|
...defaultArgs,
|
|
_: [],
|
|
headless: false,
|
|
$0: 'npx chrome-devtools-mcp@latest',
|
|
channel: 'stable',
|
|
viewport: {
|
|
width: 888,
|
|
height: 777,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('parses chrome args', async () => {
|
|
const args = parseArguments(
|
|
'1.0.0',
|
|
[
|
|
'node',
|
|
'main.js',
|
|
`--chrome-arg='--no-sandbox'`,
|
|
`--chrome-arg='--disable-setuid-sandbox'`,
|
|
],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(args, {
|
|
...defaultArgs,
|
|
_: [],
|
|
headless: false,
|
|
$0: 'npx chrome-devtools-mcp@latest',
|
|
channel: 'stable',
|
|
'chrome-arg': ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
chromeArg: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
});
|
|
});
|
|
|
|
it('parses ignore chrome args', async () => {
|
|
const args = parseArguments(
|
|
'1.0.0',
|
|
[
|
|
'node',
|
|
'main.js',
|
|
`--ignore-default-chrome-arg='--disable-extensions'`,
|
|
`--ignore-default-chrome-arg='--disable-cancel-all-touches'`,
|
|
],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(args, {
|
|
...defaultArgs,
|
|
_: [],
|
|
headless: false,
|
|
$0: 'npx chrome-devtools-mcp@latest',
|
|
channel: 'stable',
|
|
'ignore-default-chrome-arg': [
|
|
'--disable-extensions',
|
|
'--disable-cancel-all-touches',
|
|
],
|
|
ignoreDefaultChromeArg: [
|
|
'--disable-extensions',
|
|
'--disable-cancel-all-touches',
|
|
],
|
|
});
|
|
});
|
|
|
|
it('parses wsEndpoint with ws:// protocol', async () => {
|
|
const args = parseArguments(
|
|
'1.0.0',
|
|
[
|
|
'node',
|
|
'main.js',
|
|
'--wsEndpoint',
|
|
'ws://127.0.0.1:9222/devtools/browser/abc123',
|
|
],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(args, {
|
|
...defaultArgs,
|
|
_: [],
|
|
headless: false,
|
|
$0: 'npx chrome-devtools-mcp@latest',
|
|
'ws-endpoint': 'ws://127.0.0.1:9222/devtools/browser/abc123',
|
|
wsEndpoint: 'ws://127.0.0.1:9222/devtools/browser/abc123',
|
|
w: 'ws://127.0.0.1:9222/devtools/browser/abc123',
|
|
});
|
|
});
|
|
|
|
it('parses wsEndpoint with wss:// protocol', async () => {
|
|
const args = parseArguments(
|
|
'1.0.0',
|
|
[
|
|
'node',
|
|
'main.js',
|
|
'--wsEndpoint',
|
|
'wss://example.com:9222/devtools/browser/abc123',
|
|
],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(args, {
|
|
...defaultArgs,
|
|
_: [],
|
|
headless: false,
|
|
$0: 'npx chrome-devtools-mcp@latest',
|
|
'ws-endpoint': 'wss://example.com:9222/devtools/browser/abc123',
|
|
wsEndpoint: 'wss://example.com:9222/devtools/browser/abc123',
|
|
w: 'wss://example.com:9222/devtools/browser/abc123',
|
|
});
|
|
});
|
|
|
|
it('parses wsHeaders with valid JSON', async () => {
|
|
const args = parseArguments(
|
|
'1.0.0',
|
|
[
|
|
'node',
|
|
'main.js',
|
|
'--wsEndpoint',
|
|
'ws://127.0.0.1:9222/devtools/browser/abc123',
|
|
'--wsHeaders',
|
|
'{"Authorization":"Bearer token","X-Custom":"value"}',
|
|
],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(args.wsHeaders, {
|
|
Authorization: 'Bearer token',
|
|
'X-Custom': 'value',
|
|
});
|
|
});
|
|
|
|
it('parses disabled category', async () => {
|
|
const args = parseArguments(
|
|
'1.0.0',
|
|
['node', 'main.js', '--no-category-emulation'],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(args, {
|
|
...defaultArgs,
|
|
_: [],
|
|
headless: false,
|
|
$0: 'npx chrome-devtools-mcp@latest',
|
|
channel: 'stable',
|
|
'category-emulation': false,
|
|
categoryEmulation: false,
|
|
});
|
|
});
|
|
it('parses auto-connect', async () => {
|
|
const args = parseArguments(
|
|
'1.0.0',
|
|
['node', 'main.js', '--auto-connect'],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(args, {
|
|
...defaultArgs,
|
|
_: [],
|
|
headless: false,
|
|
$0: 'npx chrome-devtools-mcp@latest',
|
|
channel: 'stable',
|
|
'auto-connect': true,
|
|
autoConnect: true,
|
|
});
|
|
});
|
|
|
|
it('parses usage statistics flag', async () => {
|
|
// Test default (should be true).
|
|
const defaultArgs = parseArguments('1.0.0', ['node', 'main.js'], {});
|
|
assert.strictEqual(defaultArgs.usageStatistics, true);
|
|
|
|
// Test enabling it
|
|
const enabledArgs = parseArguments(
|
|
'1.0.0',
|
|
['node', 'main.js', '--usage-statistics'],
|
|
{},
|
|
);
|
|
assert.strictEqual(enabledArgs.usageStatistics, true);
|
|
|
|
// Test disabling it
|
|
const disabledArgs = parseArguments(
|
|
'1.0.0',
|
|
['node', 'main.js', '--no-usage-statistics'],
|
|
{},
|
|
);
|
|
assert.strictEqual(disabledArgs.usageStatistics, false);
|
|
});
|
|
|
|
it('respects env variable', async () => {
|
|
// Test default (should be true).
|
|
const defaultArgs = parseArguments('1.0.0', ['node', 'main.js'], {
|
|
CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true',
|
|
});
|
|
assert.strictEqual(defaultArgs.usageStatistics, false);
|
|
|
|
// Test enabling it
|
|
const enabledArgs = parseArguments(
|
|
'1.0.0',
|
|
['node', 'main.js', '--usage-statistics'],
|
|
{
|
|
CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true',
|
|
},
|
|
);
|
|
assert.strictEqual(enabledArgs.usageStatistics, false);
|
|
|
|
// Test disabling it
|
|
const disabledArgs = parseArguments(
|
|
'1.0.0',
|
|
['node', 'main.js', '--no-usage-statistics'],
|
|
{
|
|
CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true',
|
|
},
|
|
);
|
|
assert.strictEqual(disabledArgs.usageStatistics, false);
|
|
});
|
|
|
|
it('parses performance crux flag', async () => {
|
|
const defaultArgs = parseArguments('1.0.0', ['node', 'main.js']);
|
|
assert.strictEqual(defaultArgs.performanceCrux, true);
|
|
|
|
// force enable
|
|
const enabledArgs = parseArguments(
|
|
'1.0.0',
|
|
['node', 'main.js', '--performance-crux'],
|
|
{},
|
|
);
|
|
assert.strictEqual(enabledArgs.performanceCrux, true);
|
|
|
|
const disabledArgs = parseArguments(
|
|
'1.0.0',
|
|
['node', 'main.js', '--no-performance-crux'],
|
|
{},
|
|
);
|
|
assert.strictEqual(disabledArgs.performanceCrux, false);
|
|
});
|
|
|
|
it('parses blocked-url-pattern flags as array', async () => {
|
|
const defaultArgs = parseArguments('1.0.0', ['node', 'main.js']);
|
|
assert.strictEqual(defaultArgs.blockedUrlPattern, undefined);
|
|
|
|
const singleArgs = parseArguments(
|
|
'1.0.0',
|
|
['node', 'main.js', '--blocked-url-pattern=https://example.com/*'],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(singleArgs.blockedUrlPattern, [
|
|
'https://example.com/*',
|
|
]);
|
|
|
|
const repeatedArgs = parseArguments(
|
|
'1.0.0',
|
|
[
|
|
'node',
|
|
'main.js',
|
|
'--blocked-url-pattern=https://a.com/*',
|
|
'--blocked-url-pattern=https://b.com/*',
|
|
],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(repeatedArgs.blockedUrlPattern, [
|
|
'https://a.com/*',
|
|
'https://b.com/*',
|
|
]);
|
|
|
|
const spaceSeparatedArgs = parseArguments(
|
|
'1.0.0',
|
|
[
|
|
'node',
|
|
'main.js',
|
|
'--blocked-url-pattern',
|
|
'https://a.com/*',
|
|
'https://b.com/*',
|
|
],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(spaceSeparatedArgs.blockedUrlPattern, [
|
|
'https://a.com/*',
|
|
'https://b.com/*',
|
|
]);
|
|
});
|
|
|
|
it('parses allowed-url-pattern flags as array', async () => {
|
|
const defaultArgs = parseArguments('1.0.0', ['node', 'main.js']);
|
|
assert.strictEqual(defaultArgs.allowedUrlPattern, undefined);
|
|
|
|
const singleArgs = parseArguments(
|
|
'1.0.0',
|
|
['node', 'main.js', '--allowed-url-pattern=https://example.com/*'],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(singleArgs.allowedUrlPattern, [
|
|
'https://example.com/*',
|
|
]);
|
|
|
|
const repeatedArgs = parseArguments(
|
|
'1.0.0',
|
|
[
|
|
'node',
|
|
'main.js',
|
|
'--allowed-url-pattern=https://a.com/*',
|
|
'--allowed-url-pattern=https://b.com/*',
|
|
],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(repeatedArgs.allowedUrlPattern, [
|
|
'https://a.com/*',
|
|
'https://b.com/*',
|
|
]);
|
|
|
|
const spaceSeparatedArgs = parseArguments(
|
|
'1.0.0',
|
|
[
|
|
'node',
|
|
'main.js',
|
|
'--allowed-url-pattern',
|
|
'https://a.com/*',
|
|
'https://b.com/*',
|
|
],
|
|
{},
|
|
);
|
|
assert.deepStrictEqual(spaceSeparatedArgs.allowedUrlPattern, [
|
|
'https://a.com/*',
|
|
'https://b.com/*',
|
|
]);
|
|
});
|
|
});
|