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>
This commit is contained in:
Natasha Gorshunova
2026-06-08 11:20:00 +02:00
committed by GitHub
parent 7afd01673f
commit 02b4492ca6
11 changed files with 586 additions and 1 deletions
+8
View File
@@ -637,6 +637,14 @@ The Chrome DevTools MCP server supports the following configuration option:
Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
- **Type:** array
- **`--blockedUrlPattern`/ `--blocked-url-pattern`**
Restricts network access by blocking specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Silently detaches from targets with blocked URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns.
- **Type:** array
- **`--allowedUrlPattern`/ `--allowed-url-pattern`**
Restricts network access by allowing only specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Requires Chrome 149+. Silently detaches from targets with unallowed URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns.
- **Type:** array
- **`--ignoreDefaultChromeArg`/ `--ignore-default-chrome-arg`**
Explicitly disable default arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
- **Type:** array
+66
View File
@@ -30,6 +30,71 @@ export class FakeIssuesManager extends DevTools.Common.ObjectWrapper
// DevTools CDP errors can get noisy.
DevTools.ProtocolClient.InspectorBackend.test.suppressRequestErrors = true;
// Stub out Network emulation commands on the DevTools Agent prototype globally.
// This prevents the DevTools Frontend from ever resetting/clearing Puppeteer's
// active network blocking/throttling rules during target setup or session lifetime.
const networkAgentPrototype =
DevTools.ProtocolClient.InspectorBackend.inspectorBackend.agentPrototypes.get(
'Network',
);
if (networkAgentPrototype) {
Object.defineProperty(
networkAgentPrototype,
'invoke_emulateNetworkConditionsByRule',
{
value: () => {
return Promise.resolve({
ruleIds: [],
getError: () => undefined,
});
},
writable: true,
configurable: true,
enumerable: true,
},
);
Object.defineProperty(networkAgentPrototype, 'invoke_overrideNetworkState', {
value: () => {
return Promise.resolve({
getError: () => undefined,
});
},
writable: true,
configurable: true,
enumerable: true,
});
Object.defineProperty(networkAgentPrototype, 'invoke_enable', {
value: () => {
return Promise.resolve({
getError: () => undefined,
});
},
writable: true,
configurable: true,
enumerable: true,
});
Object.defineProperty(networkAgentPrototype, 'invoke_disable', {
value: () => {
return Promise.resolve({
getError: () => undefined,
});
},
writable: true,
configurable: true,
enumerable: true,
});
Object.defineProperty(networkAgentPrototype, 'invoke_setBlockedURLs', {
value: () => {
return Promise.resolve({
getError: () => undefined,
});
},
writable: true,
configurable: true,
enumerable: true,
});
}
DevTools.I18n.DevToolsLocale.DevToolsLocale.instance({
create: true,
data: {
@@ -146,6 +211,7 @@ const DEFAULT_FACTORY: TargetUniverseFactoryFn = async (page: Page) => {
const connection = new PuppeteerDevToolsConnection(session);
const targetManager = universe.context.get(DevTools.TargetManager);
targetManager.observeModels(DevTools.DebuggerModel, SKIP_ALL_PAUSES);
targetManager.observeModels(
DevTools.NetworkManager.NetworkManager,
+10 -1
View File
@@ -60,6 +60,8 @@ interface McpContextOptions {
experimentalIncludeAllPages?: boolean;
// Whether CrUX data should be fetched.
performanceCrux: boolean;
// Whether allowlist/blocklist is configured.
hasNetworkBlockOrAllowlist?: boolean;
}
const DEFAULT_TIMEOUT = 5_000;
@@ -345,7 +347,14 @@ export class McpContext implements Context {
const mcpPage = this.#getMcpPage(page);
const newSettings: EmulationSettings = {...mcpPage.emulationSettings};
if (!options.networkConditions) {
// Skip network emulation if blocklist/allowlist is configured, as it conflicts with blocking rules in Puppeteer.
if (this.#options.hasNetworkBlockOrAllowlist) {
if (options.networkConditions !== undefined) {
throw new Error(
'Network throttling is not supported when network blocking (allowlist/blocklist) is configured.',
);
}
} else if (!options.networkConditions) {
await page.emulateNetworkConditions(null);
delete newSettings.networkConditions;
} else if (options.networkConditions === 'Offline') {
@@ -205,6 +205,18 @@ export const cliOptions = {
describe:
'Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.',
},
blockedUrlPattern: {
type: 'array',
describe:
'Restricts network access by blocking specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Silently detaches from targets with blocked URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns.',
conflicts: ['allowedUrlPattern'],
},
allowedUrlPattern: {
type: 'array',
describe:
'Restricts network access by allowing only specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Requires Chrome 149+. Silently detaches from targets with unallowed URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns.',
conflicts: ['blockedUrlPattern'],
},
ignoreDefaultChromeArg: {
type: 'array',
describe:
+8
View File
@@ -52,6 +52,8 @@ export async function ensureBrowserConnected(options: {
channel?: Channel;
userDataDir?: string;
enableExtensions?: boolean;
blocklist?: string[];
allowlist?: string[];
}) {
const {channel, enableExtensions} = options;
if (browser?.connected) {
@@ -62,6 +64,8 @@ export async function ensureBrowserConnected(options: {
targetFilter: makeTargetFilter(enableExtensions),
defaultViewport: null,
handleDevToolsAsPage: true,
blocklist: options.blocklist,
allowlist: options.allowlist,
};
let autoConnect = false;
@@ -156,6 +160,8 @@ interface McpLaunchOptions {
devtools: boolean;
enableExtensions?: boolean;
viaCli?: boolean;
blocklist?: string[];
allowlist?: string[];
}
export function detectDisplay(): void {
@@ -235,6 +241,8 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
acceptInsecureCerts: options.acceptInsecureCerts,
handleDevToolsAsPage: true,
enableExtensions: options.enableExtensions,
blocklist: options.blocklist,
allowlist: options.allowlist,
});
if (options.logFile) {
// FIXME: we are probably subscribing too late to catch startup logs. We
+15
View File
@@ -99,6 +99,13 @@ export async function createMcpServer(
chromeArgs.push(`--proxy-server=${serverArgs.proxyServer}`);
}
const devtools = serverArgs.experimentalDevtools ?? false;
const blocklist = serverArgs.blockedUrlPattern
? serverArgs.blockedUrlPattern.map(String)
: undefined;
const allowlist = serverArgs.allowedUrlPattern
? serverArgs.allowedUrlPattern.map(String)
: undefined;
const browser =
serverArgs.browserUrl || serverArgs.wsEndpoint || serverArgs.autoConnect
? await ensureBrowserConnected({
@@ -111,6 +118,8 @@ export async function createMcpServer(
: undefined,
userDataDir: serverArgs.userDataDir,
devtools,
blocklist,
allowlist,
})
: await ensureBrowserLaunched({
headless: serverArgs.headless,
@@ -126,6 +135,8 @@ export async function createMcpServer(
devtools,
enableExtensions: serverArgs.categoryExtensions,
viaCli: serverArgs.viaCli,
blocklist,
allowlist,
});
if (context?.browser !== browser) {
@@ -133,6 +144,10 @@ export async function createMcpServer(
experimentalDevToolsDebugging: devtools,
experimentalIncludeAllPages: serverArgs.experimentalIncludeAllPages,
performanceCrux: serverArgs.performanceCrux,
hasNetworkBlockOrAllowlist: Boolean(
(blocklist && blocklist.length > 0) ||
(allowlist && allowlist.length > 0),
),
});
await updateRoots();
}
+8
View File
@@ -305,5 +305,13 @@
{
"name": "memory_debugging",
"flagType": "boolean"
},
{
"name": "blocked_url_pattern_present",
"flagType": "boolean"
},
{
"name": "allowed_url_pattern_present",
"flagType": "boolean"
}
]
+76
View File
@@ -13,6 +13,8 @@ import {executablePath} from 'puppeteer';
import {detectDisplay, ensureBrowserConnected, launch} from '../src/browser.js';
import {serverHooks} from './server.js';
describe('browser', () => {
it('detects display does not crash', () => {
detectDisplay();
@@ -100,4 +102,78 @@ describe('browser', () => {
await browser.close();
}
});
describe('Blocking', () => {
const server = serverHooks();
it('blocks URLs in blocklist', async () => {
server.addHtmlRoute('/allowed.html', '<html><body>Allowed</body></html>');
server.addHtmlRoute('/blocked.html', '<html><body>Blocked</body></html>');
const browser = await launch({
headless: true,
isolated: true,
executablePath: await executablePath(),
devtools: false,
blocklist: ['*://*:*/blocked.html'],
});
try {
const page = await browser.newPage();
// Access allowed URL
await page.goto(server.getRoute('/allowed.html'));
const content = await page.evaluate(() => document.body.textContent);
assert.strictEqual(content, 'Allowed');
// Fetch of blocked URL from the page
const fetchSucceeded = await page.evaluate(async url => {
try {
await fetch(url);
return true;
} catch {
return false;
}
}, server.getRoute('/blocked.html'));
assert.strictEqual(fetchSucceeded, false);
} finally {
await browser.close();
}
});
it('blocks URLs not in allowlist', async () => {
server.addHtmlRoute('/allowed.html', '<html><body>Allowed</body></html>');
server.addHtmlRoute('/blocked.html', '<html><body>Blocked</body></html>');
const browser = await launch({
headless: true,
isolated: true,
executablePath: await executablePath(),
devtools: false,
allowlist: ['*://*:*/allowed.html'],
});
try {
const page = await browser.newPage();
// Access allowed URL
await page.goto(server.getRoute('/allowed.html'));
const content = await page.evaluate(() => document.body.textContent);
assert.strictEqual(content, 'Allowed');
// Fetch of blocked URL from the page
const fetchSucceeded = await page.evaluate(async url => {
try {
await fetch(url);
return true;
} catch {
return false;
}
}, server.getRoute('/blocked.html'));
assert.strictEqual(fetchSucceeded, false);
} finally {
await browser.close();
}
});
});
});
+90
View File
@@ -345,4 +345,94 @@ describe('cli args parsing', () => {
);
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/*',
]);
});
});
+283
View File
@@ -0,0 +1,283 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert/strict';
import {describe, it} from 'node:test';
import {emulate} from '../src/tools/emulation.js';
import {lighthouseAudit} from '../src/tools/lighthouse.js';
import {navigatePage} from '../src/tools/pages.js';
import {evaluateScript} from '../src/tools/script.js';
import {serverHooks} from './server.js';
import {withMcpContext} from './utils.js';
describe('Network Blocking Integration', () => {
const server = serverHooks();
it('blocks URLs in blocklist', async () => {
server.addHtmlRoute('/allowed.html', '<html><body>Allowed</body></html>');
server.addHtmlRoute('/blocked.html', '<html><body>Blocked</body></html>');
const blockedUrlPattern = [server.getRoute('/blocked.html')];
await withMcpContext(
async (response, context) => {
const allowedUrl = server.getRoute('/allowed.html');
await navigatePage().handler(
{
params: {url: allowedUrl},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(
response.responseLines[0],
`Successfully navigated to ${allowedUrl}.`,
);
response.resetResponseLineForTesting();
await evaluateScript().handler(
{
params: {function: String(() => document.body.textContent)},
},
response,
context,
);
assert.strictEqual(
JSON.parse(response.responseLines.at(2)!),
'Allowed',
);
const blockedUrl = server.getRoute('/blocked.html');
response.resetResponseLineForTesting();
await evaluateScript().handler(
{
params: {
function: `async () => {
try {
await fetch("${blockedUrl}");
return 'SUCCESS';
} catch (err) {
return err instanceof Error ? err.message : String(err);
}
}`,
},
},
response,
context,
);
assert.strictEqual(
JSON.parse(response.responseLines.at(2)!),
'Failed to fetch',
);
},
{
blockedUrlPattern,
},
);
});
it('blocks URLs not in allowlist', async () => {
server.addHtmlRoute('/allowed.html', '<html><body>Allowed</body></html>');
server.addHtmlRoute('/blocked.html', '<html><body>Blocked</body></html>');
const allowedUrlPattern = [server.getRoute('/allowed.html')];
await withMcpContext(
async (response, context) => {
const allowedUrl = server.getRoute('/allowed.html');
await navigatePage().handler(
{
params: {url: allowedUrl},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(
response.responseLines[0],
`Successfully navigated to ${allowedUrl}.`,
);
response.resetResponseLineForTesting();
await evaluateScript().handler(
{
params: {function: String(() => document.body.textContent)},
},
response,
context,
);
assert.strictEqual(
JSON.parse(response.responseLines.at(2)!),
'Allowed',
);
const blockedUrl = server.getRoute('/blocked.html');
response.resetResponseLineForTesting();
await evaluateScript().handler(
{
params: {
function: `async () => {
try {
await fetch("${blockedUrl}");
return 'SUCCESS';
} catch (err) {
return err instanceof Error ? err.message : String(err);
}
}`,
},
},
response,
context,
);
assert.strictEqual(
JSON.parse(response.responseLines.at(2)!),
'Failed to fetch',
);
},
{
allowedUrlPattern,
},
);
});
it('respects blocklist after Lighthouse audits', async () => {
server.addHtmlRoute('/allowed.html', '<html><body>Allowed</body></html>');
server.addHtmlRoute('/blocked.html', '<html><body>Blocked</body></html>');
const blockedUrlPattern = [server.getRoute('/blocked.html')];
await withMcpContext(
async (response, context) => {
const allowedUrl = server.getRoute('/allowed.html');
await navigatePage().handler(
{
params: {url: allowedUrl},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(
response.responseLines[0],
`Successfully navigated to ${allowedUrl}.`,
);
const blockedUrl = server.getRoute('/blocked.html');
// Verifies fetch is blocked before Lighthouse audit
response.resetResponseLineForTesting();
await evaluateScript().handler(
{
params: {
function: `async () => {
try {
await fetch("${blockedUrl}");
return 'SUCCESS';
} catch (err) {
return err instanceof Error ? err.message : String(err);
}
}`,
},
},
response,
context,
);
assert.strictEqual(
JSON.parse(response.responseLines.at(2)!),
'Failed to fetch',
'Fetch should be blocked before audit',
);
await lighthouseAudit.handler(
{
params: {
mode: 'navigation',
device: 'desktop',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.equal(
response.attachedLighthouseResult?.summary.mode,
'navigation',
);
// 2. Verify fetch remains blocked AFTER Lighthouse audit
response.resetResponseLineForTesting();
await evaluateScript().handler(
{
params: {
function: `async () => {
try {
await fetch("${blockedUrl}");
return 'SUCCESS';
} catch (err) {
return err instanceof Error ? err.message : String(err);
}
}`,
},
},
response,
context,
);
assert.strictEqual(
JSON.parse(response.responseLines.at(2)!),
'Failed to fetch',
'Fetch should still be blocked after audit',
);
},
{
blockedUrlPattern,
},
);
});
it('throws error when trying to emulate network conditions while blocklist is configured', async () => {
const blockedUrlPattern = ['*://*/*'];
await withMcpContext(
async (response, context) => {
// Attempting to emulate network conditions should throw an error.
await assert.rejects(async () => {
await emulate.handler(
{
params: {
networkConditions: 'Offline',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
}, /Network throttling is not supported when network blocking \(allowlist\/blocklist\) is configured\./);
// Attempting to emulate CPU rate or other things should succeed without errors.
await emulate.handler(
{
params: {
cpuThrottlingRate: 2,
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(
response.responseLines[0],
'Emulation configured successfully',
);
},
{
blockedUrlPattern,
},
);
});
});
+10
View File
@@ -74,6 +74,8 @@ export async function withBrowser(
autoOpenDevTools?: boolean;
executablePath?: string;
args?: string[];
blockedUrlPattern?: string[];
allowedUrlPattern?: string[];
} = {},
) {
const launchOptions: LaunchOptions = {
@@ -86,6 +88,8 @@ export async function withBrowser(
handleDevToolsAsPage: true,
args: [...(options.args || []), '--screen-info={3840x2160}'],
enableExtensions: true,
blocklist: options.blockedUrlPattern,
allowlist: options.allowedUrlPattern,
};
const key = JSON.stringify(launchOptions);
@@ -115,6 +119,8 @@ export async function withMcpContext(
performanceCrux?: boolean;
executablePath?: string;
args?: string[];
blockedUrlPattern?: string[];
allowedUrlPattern?: string[];
} = {},
args: ParsedArguments = {} as ParsedArguments,
) {
@@ -130,6 +136,10 @@ export async function withMcpContext(
{
experimentalDevToolsDebugging: false,
performanceCrux: options.performanceCrux ?? true,
hasNetworkBlockOrAllowlist: Boolean(
(options.blockedUrlPattern && options.blockedUrlPattern.length > 0) ||
(options.allowedUrlPattern && options.allowedUrlPattern.length > 0),
),
},
Locator,
);