From df09ac6d408f042d1ac444b1c473e0d1b89134cb Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 11 May 2026 19:07:04 +0000 Subject: [PATCH 01/11] Implement url allowlist and blocklist --- src/bin/chrome-devtools-mcp-cli-options.ts | 11 +++ src/browser.ts | 8 ++ src/index.ts | 11 +++ tests/browser.test.ts | 86 ++++++++++++++++++++++ 4 files changed, 116 insertions(+) diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts index f510744d3..9ec69d1c4 100644 --- a/src/bin/chrome-devtools-mcp-cli-options.ts +++ b/src/bin/chrome-devtools-mcp-cli-options.ts @@ -204,6 +204,17 @@ export const cliOptions = { describe: 'Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.', }, + blocklist: { + type: 'array', + describe: + 'URL patterns to block access to. Uses standard URLPattern API. Cannot be used with --blocklist', + }, + allowlist: { + type: 'array', + describe: + 'URL patterns to allow access to (blocks everything else). Uses standard URLPattern API. Cannot be used with --blocklist.', + conflicts: ['blocklist'], + }, ignoreDefaultChromeArg: { type: 'array', describe: diff --git a/src/browser.ts b/src/browser.ts index 20f254865..f40f56f79 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -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 { 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 diff --git a/src/index.ts b/src/index.ts index d3c072fa4..60dbb7fe6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -99,6 +99,13 @@ export async function createMcpServer( chromeArgs.push(`--proxy-server=${serverArgs.proxyServer}`); } const devtools = serverArgs.experimentalDevtools ?? false; + const blocklist = serverArgs.blocklist + ? serverArgs.blocklist.map(String) + : undefined; + const allowlist = serverArgs.allowlist + ? serverArgs.allowlist.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) { diff --git a/tests/browser.test.ts b/tests/browser.test.ts index 85e9c592f..7d4e27321 100644 --- a/tests/browser.test.ts +++ b/tests/browser.test.ts @@ -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,88 @@ describe('browser', () => { await browser.close(); } }); + + describe('Blocking', () => { + const server = serverHooks(); + + it('blocks URLs in blocklist', async () => { + server.addHtmlRoute('/allowed.html', 'Allowed'); + server.addHtmlRoute('/blocked.html', 'Blocked'); + + const browser = await launch({ + headless: true, + isolated: true, + executablePath: 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 fetchResult = await page.evaluate(async url => { + try { + await fetch(url); + return 'SUCCESS'; + } catch (err) { + return err instanceof Error ? err.message : String(err); + } + }, server.getRoute('/blocked.html')); + + assert.strictEqual(fetchResult, 'Failed to fetch'); + } finally { + await browser.close(); + } + }); + + it( + 'blocks URLs not in allowlist', + {skip: 'Requires Chrome 149 or greater'}, + async () => { + server.addHtmlRoute( + '/allowed.html', + 'Allowed', + ); + server.addHtmlRoute( + '/blocked.html', + 'Blocked', + ); + + const browser = await launch({ + headless: true, + isolated: true, + executablePath: 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 fetchResult = await page.evaluate(async url => { + try { + await fetch(url); + return 'SUCCESS'; + } catch (err) { + return err instanceof Error ? err.message : String(err); + } + }, server.getRoute('/blocked.html')); + + assert.strictEqual(fetchResult, 'Failed to fetch'); + } finally { + await browser.close(); + } + }, + ); + }); }); From 790b708576f2ab62f79c4380969abfafd095b6a1 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 11 May 2026 19:16:33 +0000 Subject: [PATCH 02/11] update docs --- README.md | 8 ++++++++ src/telemetry/flag_usage_metrics.json | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/README.md b/README.md index 1ffbafa00..356e66505 100644 --- a/README.md +++ b/README.md @@ -636,6 +636,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 +- **`--blocklist`** + URL patterns to block access to. Uses standard URLPattern API. Cannot be used with --blocklist + - **Type:** array + +- **`--allowlist`** + URL patterns to allow access to (blocks everything else). Uses standard URLPattern API. Cannot be used with --blocklist. + - **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 diff --git a/src/telemetry/flag_usage_metrics.json b/src/telemetry/flag_usage_metrics.json index 9982b1838..3909d4cc6 100644 --- a/src/telemetry/flag_usage_metrics.json +++ b/src/telemetry/flag_usage_metrics.json @@ -295,5 +295,13 @@ { "name": "category_experimental_third_party", "flagType": "boolean" + }, + { + "name": "blocklist_present", + "flagType": "boolean" + }, + { + "name": "allowlist_present", + "flagType": "boolean" } ] From 6b21527ce064c21bf6a780a29b2d65371a9fd547 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Wed, 13 May 2026 14:22:55 +0000 Subject: [PATCH 03/11] Update documentation --- README.md | 4 ++-- src/bin/chrome-devtools-mcp-cli-options.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 356e66505..3b523d0c7 100644 --- a/README.md +++ b/README.md @@ -637,11 +637,11 @@ The Chrome DevTools MCP server supports the following configuration option: - **Type:** array - **`--blocklist`** - URL patterns to block access to. Uses standard URLPattern API. Cannot be used with --blocklist + Restricts network access by blocking specified URL patterns (uses URLPattern API https://urlpattern.spec.whatwg.org/). Silently detaches from targets with blocked URLs upon connection, and blocks runtime requests (including navigations and subresources). - **Type:** array - **`--allowlist`** - URL patterns to allow access to (blocks everything else). Uses standard URLPattern API. Cannot be used with --blocklist. + Restricts network access by allowing only specified URL patterns (uses URLPattern API 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). - **Type:** array - **`--ignoreDefaultChromeArg`/ `--ignore-default-chrome-arg`** diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts index 9ec69d1c4..e6a084bcf 100644 --- a/src/bin/chrome-devtools-mcp-cli-options.ts +++ b/src/bin/chrome-devtools-mcp-cli-options.ts @@ -207,12 +207,13 @@ export const cliOptions = { blocklist: { type: 'array', describe: - 'URL patterns to block access to. Uses standard URLPattern API. Cannot be used with --blocklist', + '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).', + conflicts: ['allowlist'], }, allowlist: { type: 'array', describe: - 'URL patterns to allow access to (blocks everything else). Uses standard URLPattern API. Cannot be used with --blocklist.', + '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).', conflicts: ['blocklist'], }, ignoreDefaultChromeArg: { From e17cf838f1005dd7f0d10e76f7193f8bdeedb316 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Wed, 13 May 2026 15:35:53 +0000 Subject: [PATCH 04/11] Refactor options configuration --- README.md | 8 +++---- src/bin/chrome-devtools-mcp-cli-options.ts | 28 ++++++++++++++++++---- tests/cli.test.ts | 27 +++++++++++++++++++++ 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3b523d0c7..b9a43ba66 100644 --- a/README.md +++ b/README.md @@ -637,12 +637,12 @@ The Chrome DevTools MCP server supports the following configuration option: - **Type:** array - **`--blocklist`** - Restricts network access by blocking specified URL patterns (uses URLPattern API https://urlpattern.spec.whatwg.org/). Silently detaches from targets with blocked URLs upon connection, and blocks runtime requests (including navigations and subresources). - - **Type:** array + 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). Supports comma-separated patterns. + - **Type:** string - **`--allowlist`** - Restricts network access by allowing only specified URL patterns (uses URLPattern API 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). - - **Type:** array + 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). Supports comma-separated patterns. + - **Type:** string - **`--ignoreDefaultChromeArg`/ `--ignore-default-chrome-arg`** Explicitly disable default arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp. diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts index e6a084bcf..ce238b523 100644 --- a/src/bin/chrome-devtools-mcp-cli-options.ts +++ b/src/bin/chrome-devtools-mcp-cli-options.ts @@ -205,16 +205,36 @@ export const cliOptions = { 'Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.', }, blocklist: { - type: 'array', + type: 'string', 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).', + '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). Supports comma-separated patterns.', conflicts: ['allowlist'], + coerce: (value: string | string[] | undefined) => { + if (!value) { + return undefined; + } + const arr = Array.isArray(value) ? value : [value]; + return arr + .flatMap(val => val.split(',')) + .map(s => s.trim()) + .filter(Boolean); + }, }, allowlist: { - type: 'array', + type: 'string', 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).', + '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). Supports comma-separated patterns.', conflicts: ['blocklist'], + coerce: (value: string | string[] | undefined) => { + if (!value) { + return undefined; + } + const arr = Array.isArray(value) ? value : [value]; + return arr + .flatMap(val => val.split(',')) + .map(s => s.trim()) + .filter(Boolean); + }, }, ignoreDefaultChromeArg: { type: 'array', diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 05f2d3b06..3c4faeacd 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -345,4 +345,31 @@ describe('cli args parsing', () => { ); assert.strictEqual(disabledArgs.performanceCrux, false); }); + + it('parses blocklist and allowlist flags with comma separation', async () => { + const defaultArgs = parseArguments('1.0.0', ['node', 'main.js']); + assert.strictEqual(defaultArgs.blocklist, undefined); + assert.strictEqual(defaultArgs.allowlist, undefined); + + const blocklistArgs = parseArguments( + '1.0.0', + [ + 'node', + 'main.js', + '--blocklist=https://example.com/ads/*,https://tracker.com/*', + ], + {}, + ); + assert.deepStrictEqual(blocklistArgs.blocklist, [ + 'https://example.com/ads/*', + 'https://tracker.com/*', + ]); + + const allowlistArgs = parseArguments( + '1.0.0', + ['node', 'main.js', '--allowlist=https://trusted.com/*'], + {}, + ); + assert.deepStrictEqual(allowlistArgs.allowlist, ['https://trusted.com/*']); + }); }); From 82fc71a5ee89c18707bc8f4b47564b9774ce0c2e Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Wed, 13 May 2026 17:50:27 +0000 Subject: [PATCH 05/11] Refactor tests --- tests/security_policies.test.ts | 238 ++++++++++++++++++++++++++++++++ tests/utils.ts | 6 + 2 files changed, 244 insertions(+) create mode 100644 tests/security_policies.test.ts diff --git a/tests/security_policies.test.ts b/tests/security_policies.test.ts new file mode 100644 index 000000000..4632daf3f --- /dev/null +++ b/tests/security_policies.test.ts @@ -0,0 +1,238 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'node:assert/strict'; +import {describe, it} from 'node:test'; + +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('Security Policies Integration', () => { + const server = serverHooks(); + + it('blocks URLs in blocklist', async () => { + server.addHtmlRoute('/allowed.html', 'Allowed'); + server.addHtmlRoute('/blocked.html', 'Blocked'); + + 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', + ); + }, + { + blocklist: [server.getRoute('/blocked.html')], + }, + ); + }); + + it('blocks URLs not in allowlist', async () => { + server.addHtmlRoute('/allowed.html', 'Allowed'); + server.addHtmlRoute('/blocked.html', 'Blocked'); + + 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', + ); + }, + { + allowlist: [server.getRoute('/allowed.html')], + }, + ); + }); + + it('respects blocklist after Lighthouse audits', async () => { + server.addHtmlRoute('/allowed.html', 'Allowed'); + server.addHtmlRoute('/blocked.html', 'Blocked'); + + 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', + ); + }, + { + blocklist: [server.getRoute('/blocked.html')], + }, + ); + }); +}); diff --git a/tests/utils.ts b/tests/utils.ts index 1c362a96e..af31dd785 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -74,6 +74,8 @@ export async function withBrowser( autoOpenDevTools?: boolean; executablePath?: string; args?: string[]; + blocklist?: string[]; + allowlist?: string[]; } = {}, ) { const launchOptions: LaunchOptions = { @@ -86,6 +88,8 @@ export async function withBrowser( handleDevToolsAsPage: true, args: [...(options.args || []), '--screen-info={3840x2160}'], enableExtensions: true, + blocklist: options.blocklist, + allowlist: options.allowlist, }; const key = JSON.stringify(launchOptions); @@ -115,6 +119,8 @@ export async function withMcpContext( performanceCrux?: boolean; executablePath?: string; args?: string[]; + blocklist?: string[]; + allowlist?: string[]; } = {}, args: ParsedArguments = {} as ParsedArguments, ) { From 688644cd34a3dfe732cbdee2edb45307d15bd9c5 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 18 May 2026 11:04:32 +0000 Subject: [PATCH 06/11] Rename cli arguments to allowedUrlPattern and blockedUrlPattern --- README.md | 12 ++--- src/bin/chrome-devtools-mcp-cli-options.ts | 36 ++++----------- src/index.ts | 8 ++-- src/telemetry/flag_usage_metrics.json | 4 +- tests/browser.test.ts | 4 +- tests/cli.test.ts | 53 +++++++++++++++++----- tests/security_policies.test.ts | 6 +-- tests/utils.ts | 12 ++--- 8 files changed, 73 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index b9a43ba66..1fabfa404 100644 --- a/README.md +++ b/README.md @@ -636,13 +636,13 @@ 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 -- **`--blocklist`** - 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). Supports comma-separated patterns. - - **Type:** string +- **`--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 -- **`--allowlist`** - 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). Supports comma-separated patterns. - - **Type:** string +- **`--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. diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts index ce238b523..62bdfcf13 100644 --- a/src/bin/chrome-devtools-mcp-cli-options.ts +++ b/src/bin/chrome-devtools-mcp-cli-options.ts @@ -204,37 +204,17 @@ export const cliOptions = { describe: 'Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.', }, - blocklist: { - type: 'string', + 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). Supports comma-separated patterns.', - conflicts: ['allowlist'], - coerce: (value: string | string[] | undefined) => { - if (!value) { - return undefined; - } - const arr = Array.isArray(value) ? value : [value]; - return arr - .flatMap(val => val.split(',')) - .map(s => s.trim()) - .filter(Boolean); - }, + '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'], }, - allowlist: { - type: 'string', + 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). Supports comma-separated patterns.', - conflicts: ['blocklist'], - coerce: (value: string | string[] | undefined) => { - if (!value) { - return undefined; - } - const arr = Array.isArray(value) ? value : [value]; - return arr - .flatMap(val => val.split(',')) - .map(s => s.trim()) - .filter(Boolean); - }, + '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', diff --git a/src/index.ts b/src/index.ts index 60dbb7fe6..92d044aa7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -99,11 +99,11 @@ export async function createMcpServer( chromeArgs.push(`--proxy-server=${serverArgs.proxyServer}`); } const devtools = serverArgs.experimentalDevtools ?? false; - const blocklist = serverArgs.blocklist - ? serverArgs.blocklist.map(String) + const blocklist = serverArgs.blockedUrlPattern + ? serverArgs.blockedUrlPattern.map(String) : undefined; - const allowlist = serverArgs.allowlist - ? serverArgs.allowlist.map(String) + const allowlist = serverArgs.allowedUrlPattern + ? serverArgs.allowedUrlPattern.map(String) : undefined; const browser = diff --git a/src/telemetry/flag_usage_metrics.json b/src/telemetry/flag_usage_metrics.json index 3909d4cc6..67368747f 100644 --- a/src/telemetry/flag_usage_metrics.json +++ b/src/telemetry/flag_usage_metrics.json @@ -297,11 +297,11 @@ "flagType": "boolean" }, { - "name": "blocklist_present", + "name": "blocked_url_pattern_present", "flagType": "boolean" }, { - "name": "allowlist_present", + "name": "allowed_url_pattern_present", "flagType": "boolean" } ] diff --git a/tests/browser.test.ts b/tests/browser.test.ts index 7d4e27321..30432899e 100644 --- a/tests/browser.test.ts +++ b/tests/browser.test.ts @@ -113,7 +113,7 @@ describe('browser', () => { const browser = await launch({ headless: true, isolated: true, - executablePath: executablePath(), + executablePath: await executablePath(), devtools: false, blocklist: ['*://*:*/blocked.html'], }); @@ -157,7 +157,7 @@ describe('browser', () => { const browser = await launch({ headless: true, isolated: true, - executablePath: executablePath(), + executablePath: await executablePath(), devtools: false, allowlist: ['*://*/allowed.html'], }); diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 3c4faeacd..83f883c5e 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -346,30 +346,61 @@ describe('cli args parsing', () => { assert.strictEqual(disabledArgs.performanceCrux, false); }); - it('parses blocklist and allowlist flags with comma separation', async () => { + it('parses blocked-url-pattern flags as array', async () => { const defaultArgs = parseArguments('1.0.0', ['node', 'main.js']); - assert.strictEqual(defaultArgs.blocklist, undefined); - assert.strictEqual(defaultArgs.allowlist, undefined); + assert.strictEqual(defaultArgs.blockedUrlPattern, undefined); - const blocklistArgs = parseArguments( + 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', - '--blocklist=https://example.com/ads/*,https://tracker.com/*', + '--blocked-url-pattern=https://a.com/*', + '--blocked-url-pattern=https://b.com/*', ], {}, ); - assert.deepStrictEqual(blocklistArgs.blocklist, [ - 'https://example.com/ads/*', - 'https://tracker.com/*', + assert.deepStrictEqual(repeatedArgs.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 allowlistArgs = parseArguments( + const repeatedArgs = parseArguments( '1.0.0', - ['node', 'main.js', '--allowlist=https://trusted.com/*'], + [ + 'node', + 'main.js', + '--allowed-url-pattern=https://a.com/*', + '--allowed-url-pattern=https://b.com/*', + ], {}, ); - assert.deepStrictEqual(allowlistArgs.allowlist, ['https://trusted.com/*']); + assert.deepStrictEqual(repeatedArgs.allowedUrlPattern, [ + 'https://a.com/*', + 'https://b.com/*', + ]); }); }); diff --git a/tests/security_policies.test.ts b/tests/security_policies.test.ts index 4632daf3f..b71ac900d 100644 --- a/tests/security_policies.test.ts +++ b/tests/security_policies.test.ts @@ -75,7 +75,7 @@ describe('Security Policies Integration', () => { ); }, { - blocklist: [server.getRoute('/blocked.html')], + blockedUrlPattern: [server.getRoute('/blocked.html')], }, ); }); @@ -138,7 +138,7 @@ describe('Security Policies Integration', () => { ); }, { - allowlist: [server.getRoute('/allowed.html')], + allowedUrlPattern: [server.getRoute('/allowed.html')], }, ); }); @@ -231,7 +231,7 @@ describe('Security Policies Integration', () => { ); }, { - blocklist: [server.getRoute('/blocked.html')], + blockedUrlPattern: [server.getRoute('/blocked.html')], }, ); }); diff --git a/tests/utils.ts b/tests/utils.ts index af31dd785..826c3c980 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -74,8 +74,8 @@ export async function withBrowser( autoOpenDevTools?: boolean; executablePath?: string; args?: string[]; - blocklist?: string[]; - allowlist?: string[]; + blockedUrlPattern?: string[]; + allowedUrlPattern?: string[]; } = {}, ) { const launchOptions: LaunchOptions = { @@ -88,8 +88,8 @@ export async function withBrowser( handleDevToolsAsPage: true, args: [...(options.args || []), '--screen-info={3840x2160}'], enableExtensions: true, - blocklist: options.blocklist, - allowlist: options.allowlist, + blocklist: options.blockedUrlPattern, + allowlist: options.allowedUrlPattern, }; const key = JSON.stringify(launchOptions); @@ -119,8 +119,8 @@ export async function withMcpContext( performanceCrux?: boolean; executablePath?: string; args?: string[]; - blocklist?: string[]; - allowlist?: string[]; + blockedUrlPattern?: string[]; + allowedUrlPattern?: string[]; } = {}, args: ParsedArguments = {} as ParsedArguments, ) { From 1bc3ce24dca336911617adedb9e4e85370dd3772 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 18 May 2026 12:06:21 +0000 Subject: [PATCH 07/11] prevent netwrok emulation if there is allowlist or blocklist --- src/McpContext.ts | 41 +++++++++++++++++++++++------------------ src/index.ts | 4 ++++ tests/utils.ts | 4 ++++ 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/McpContext.ts b/src/McpContext.ts index 3855c02f4..b1b415727 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -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,24 +347,27 @@ export class McpContext implements Context { const mcpPage = this.#getMcpPage(page); const newSettings: EmulationSettings = {...mcpPage.emulationSettings}; - if (!options.networkConditions) { - await page.emulateNetworkConditions(null); - delete newSettings.networkConditions; - } else if (options.networkConditions === 'Offline') { - await page.emulateNetworkConditions({ - offline: true, - download: 0, - upload: 0, - latency: 0, - }); - newSettings.networkConditions = 'Offline'; - } else if (options.networkConditions in PredefinedNetworkConditions) { - const networkCondition = - PredefinedNetworkConditions[ - options.networkConditions as keyof typeof PredefinedNetworkConditions - ]; - await page.emulateNetworkConditions(networkCondition); - newSettings.networkConditions = options.networkConditions; + // Skip network emulation if blocklist/allowlist is configured, as it is rejected by Puppeteer. + if (!this.#options.hasNetworkBlockOrAllowlist) { + if (!options.networkConditions) { + await page.emulateNetworkConditions(null); + delete newSettings.networkConditions; + } else if (options.networkConditions === 'Offline') { + await page.emulateNetworkConditions({ + offline: true, + download: 0, + upload: 0, + latency: 0, + }); + newSettings.networkConditions = 'Offline'; + } else if (options.networkConditions in PredefinedNetworkConditions) { + const networkCondition = + PredefinedNetworkConditions[ + options.networkConditions as keyof typeof PredefinedNetworkConditions + ]; + await page.emulateNetworkConditions(networkCondition); + newSettings.networkConditions = options.networkConditions; + } } const secondarySession = this.getDevToolsUniverse(mcpPage)?.session; diff --git a/src/index.ts b/src/index.ts index 92d044aa7..325f1fb87 100644 --- a/src/index.ts +++ b/src/index.ts @@ -144,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(); } diff --git a/tests/utils.ts b/tests/utils.ts index 826c3c980..41c188ebd 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -136,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, ); From c6392c13c4aafc7269adfae86e73950998758574 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 18 May 2026 12:10:26 +0000 Subject: [PATCH 08/11] file and description rename --- tests/{security_policies.test.ts => network_blocking.test.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/{security_policies.test.ts => network_blocking.test.ts} (99%) diff --git a/tests/security_policies.test.ts b/tests/network_blocking.test.ts similarity index 99% rename from tests/security_policies.test.ts rename to tests/network_blocking.test.ts index b71ac900d..e18ab9cac 100644 --- a/tests/security_policies.test.ts +++ b/tests/network_blocking.test.ts @@ -14,7 +14,7 @@ import {evaluateScript} from '../src/tools/script.js'; import {serverHooks} from './server.js'; import {withMcpContext} from './utils.js'; -describe('Security Policies Integration', () => { +describe('Network Blocking Integration', () => { const server = serverHooks(); it('blocks URLs in blocklist', async () => { From 2250c7668babdf4cfbccbd8e9c597eb733c8b675 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 18 May 2026 12:31:49 +0000 Subject: [PATCH 09/11] Run tests only on 149 version --- scripts/test.mjs | 3 +++ tests/network_blocking.test.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/scripts/test.mjs b/scripts/test.mjs index 0d427847f..46841f09b 100644 --- a/scripts/test.mjs +++ b/scripts/test.mjs @@ -103,6 +103,9 @@ async function runTests(attempt) { }); } +const chromePath = _installChrome('149.0.7827.14'); +process.env.CHROME_M149_EXECUTABLE_PATH = chromePath; + const maxAttempts = shouldRetry ? 3 : 1; let exitCode = 1; diff --git a/tests/network_blocking.test.ts b/tests/network_blocking.test.ts index e18ab9cac..c7315db59 100644 --- a/tests/network_blocking.test.ts +++ b/tests/network_blocking.test.ts @@ -76,6 +76,7 @@ describe('Network Blocking Integration', () => { }, { blockedUrlPattern: [server.getRoute('/blocked.html')], + executablePath: process.env.CHROME_M149_EXECUTABLE_PATH, }, ); }); @@ -139,6 +140,7 @@ describe('Network Blocking Integration', () => { }, { allowedUrlPattern: [server.getRoute('/allowed.html')], + executablePath: process.env.CHROME_M149_EXECUTABLE_PATH, }, ); }); @@ -232,6 +234,7 @@ describe('Network Blocking Integration', () => { }, { blockedUrlPattern: [server.getRoute('/blocked.html')], + executablePath: process.env.CHROME_M149_EXECUTABLE_PATH, }, ); }); From e6a761e05b8ff2e3913fd0c8b5a3329b71bbf890 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 18 May 2026 18:33:09 +0000 Subject: [PATCH 10/11] Prevent devtools from override netwrok conditions --- src/DevtoolsUtils.ts | 36 ++++++++++++++++++++++++++++++++++ tests/network_blocking.test.ts | 1 - 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/DevtoolsUtils.ts b/src/DevtoolsUtils.ts index a0e043eff..ece97bf90 100644 --- a/src/DevtoolsUtils.ts +++ b/src/DevtoolsUtils.ts @@ -30,6 +30,41 @@ 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, + }); +} + DevTools.I18n.DevToolsLocale.DevToolsLocale.instance({ create: true, data: { @@ -146,6 +181,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, diff --git a/tests/network_blocking.test.ts b/tests/network_blocking.test.ts index c7315db59..3204bc1a0 100644 --- a/tests/network_blocking.test.ts +++ b/tests/network_blocking.test.ts @@ -76,7 +76,6 @@ describe('Network Blocking Integration', () => { }, { blockedUrlPattern: [server.getRoute('/blocked.html')], - executablePath: process.env.CHROME_M149_EXECUTABLE_PATH, }, ); }); From 6f1034f159ec4ed47afd492afc69814cff59ac03 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Tue, 19 May 2026 11:40:35 +0200 Subject: [PATCH 11/11] chore: fix network --- src/DevtoolsUtils.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/DevtoolsUtils.ts b/src/DevtoolsUtils.ts index ece97bf90..c09a1d87d 100644 --- a/src/DevtoolsUtils.ts +++ b/src/DevtoolsUtils.ts @@ -63,6 +63,36 @@ if (networkAgentPrototype) { 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({