项目文件夹

文件
Natasha Gorshunova 02b4492ca6 feat: support allowedUrlPattern & blockedUrlPattern Options (#2037)
## Support for Network Blocklists and Allowlists
(`--blocked-url-pattern` & `--allowed-url-pattern` arguments)

This PR adds support for CLI options to restrict network access in the
browser session via URL patterns.

### Key Features & How It Works

- **Pattern Matching:** Utilizes the [URLPattern
Standard](https://urlpattern.spec.whatwg.org/) for pattern matching.
- **Target Detachment:** Silently detaches from targets (pages/tabs)
whose URLs match blocked patterns (or do not match allowed patterns)
upon connection.
- **Runtime Blocking:** Prevents navigations and blocks runtime requests
(such as fetch/XHR and subresources) if they violate the pattern rules.
- **Mutual Exclusivity:** `--blocked-url-pattern` and
`--allowed-url-pattern` conflict with each other and cannot be
configured simultaneously.
- **Browser Requirements:**
  - **`--allowed-url-pattern`**: Requires **Chrome 149+**.
- **`--blocked-url-pattern`**: Works on Chrome versions older than 149,
but **Chrome 149+ is highly recommended**.

### Important Limitations & Side Effects

- **Network Emulation/Throttling Conflict:** Network throttling is
disabled when a network blocklist/allowlist is configured, to avoid
conflicting with Puppeteer's underlying blocking mechanisms.
- Using the `emulate` tool to modify `networkConditions` (e.g. setting
to `Offline`) will throw an error: *`Network throttling is not supported
when network blocking (allowlist/blocklist) is configured.`*
- Other emulation settings (e.g., `cpuThrottlingRate`, `geolocation`,
`viewport`) are unaffected and remain fully functional.

---

### Configuration Examples

#### 1. Blocking specific domains or endpoints (Blocklist)
Add the `--blocked-url-pattern` options to the `args` list in your MCP
settings file:

```json
{
  "mcpServers": {
    "chrome-devtools": {
      "command": "npx",
      "args": [
        "chrome-devtools-mcp@latest",
        "--blocked-url-pattern=*://*.blocked-example.com/*",
        "--blocked-url-pattern=*://*.another-blocked-example.com/*"
      ]
    }
  }
}
```

#### 2. Restricting access to authorized domains (Allowlist)
Add the `--allowed-url-pattern` options to restrict the browser to
permitted hosts (requires Chrome 149+):

```json
{
  "mcpServers": {
    "chrome-devtools": {
      "command": "npx",
      "args": [
        "chrome-devtools-mcp@latest",
        "--allowed-url-pattern=https://*.allowed-example.com/*",
        "--allowed-url-pattern=https://*.another-allowed-example.com/*"
      ]
    }
  }
}
```

---------

Co-authored-by: Natallia Harshunova <nharshunova@chromium.org>
Co-authored-by: Alex Rudenko <alexrudenko@chromium.org>
2026-06-08 09:20:00 +00:00

439 行
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,
};
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/*',
]);
});
});