From c5f4ad1c4e4e266bba387067d224cccb4fac242a Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 11 May 2026 19:07:04 +0000 Subject: [PATCH 01/15] 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 b65929e12..571109459 100644 --- a/src/bin/chrome-devtools-mcp-cli-options.ts +++ b/src/bin/chrome-devtools-mcp-cli-options.ts @@ -205,6 +205,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 fbdf3ed6b..6b077ad23 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 fdd5790c1..e4532483b 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 bc4acfccc7577d5da017b811415ede4c9b1868ca Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Wed, 13 May 2026 17:50:27 +0000 Subject: [PATCH 02/15] 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 c23fbc8cd694a6c2fbfae2e4ea05eec2fa61394a Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 18 May 2026 11:04:32 +0000 Subject: [PATCH 03/15] Rename cli arguments to allowedUrlPattern and blockedUrlPattern --- README.md | 8 +++ src/bin/chrome-devtools-mcp-cli-options.ts | 11 ++-- src/index.ts | 8 +-- src/telemetry/flag_usage_metrics.json | 8 +++ tests/browser.test.ts | 4 +- tests/cli.test.ts | 58 ++++++++++++++++++++++ tests/security_policies.test.ts | 6 +-- tests/utils.ts | 12 ++--- 8 files changed, 95 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index cfe7dd3ea..ea3f4e962 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts index 571109459..0cfb3c2b8 100644 --- a/src/bin/chrome-devtools-mcp-cli-options.ts +++ b/src/bin/chrome-devtools-mcp-cli-options.ts @@ -205,16 +205,17 @@ export const cliOptions = { describe: 'Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.', }, - blocklist: { + blockedUrlPattern: { 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). Accepts an array of patterns.', + conflicts: ['allowedUrlPattern'], }, - allowlist: { + allowedUrlPattern: { type: 'array', describe: - 'URL patterns to allow access to (blocks everything else). Uses standard URLPattern API. Cannot be used with --blocklist.', - conflicts: ['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). Accepts an array of patterns.', + conflicts: ['blockedUrlPattern'], }, ignoreDefaultChromeArg: { type: 'array', diff --git a/src/index.ts b/src/index.ts index e4532483b..66c5af77c 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 941baad2e..a273b5ebe 100644 --- a/src/telemetry/flag_usage_metrics.json +++ b/src/telemetry/flag_usage_metrics.json @@ -305,5 +305,13 @@ { "name": "memory_debugging", "flagType": "boolean" + }, + { + "name": "blocked_url_pattern_present", + "flagType": "boolean" + }, + { + "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 05f2d3b06..83f883c5e 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -345,4 +345,62 @@ 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/*', + ]); + }); + + 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/*', + ]); + }); }); 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 b42991cc5e03ec30e8d72a29a77004df4ece4b0e Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 18 May 2026 12:06:21 +0000 Subject: [PATCH 04/15] 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 3e7e410a8..6ddac3d98 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 66c5af77c..fe725af62 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 70c8d44d61316ca1d7497bc06d04c14b867c568c Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 18 May 2026 12:10:26 +0000 Subject: [PATCH 05/15] 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 b17507da5bd1f8e40809bff47e63b419149c723c Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 18 May 2026 12:31:49 +0000 Subject: [PATCH 06/15] 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 1c826b53acc68aba22eaf73488fe197efd339612 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 18 May 2026 18:33:09 +0000 Subject: [PATCH 07/15] 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 a14a3cebec114764a170a774bfd1ff5eeea83f13 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Tue, 19 May 2026 11:40:35 +0200 Subject: [PATCH 08/15] 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({ From 615bef54487893267d14fdffc355605b1b717f37 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Wed, 3 Jun 2026 14:20:34 +0000 Subject: [PATCH 09/15] refactor test to be more readable --- tests/network_blocking.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/network_blocking.test.ts b/tests/network_blocking.test.ts index 3204bc1a0..2450eff68 100644 --- a/tests/network_blocking.test.ts +++ b/tests/network_blocking.test.ts @@ -21,6 +21,7 @@ describe('Network Blocking Integration', () => { server.addHtmlRoute('/allowed.html', 'Allowed'); server.addHtmlRoute('/blocked.html', 'Blocked'); + const blockedUrlPattern= [server.getRoute('/blocked.html')]; await withMcpContext( async (response, context) => { const allowedUrl = server.getRoute('/allowed.html'); @@ -75,7 +76,7 @@ describe('Network Blocking Integration', () => { ); }, { - blockedUrlPattern: [server.getRoute('/blocked.html')], + blockedUrlPattern, }, ); }); @@ -84,6 +85,8 @@ describe('Network Blocking Integration', () => { server.addHtmlRoute('/allowed.html', 'Allowed'); server.addHtmlRoute('/blocked.html', 'Blocked'); + const allowedUrlPattern= [server.getRoute('/allowed.html')]; + await withMcpContext( async (response, context) => { const allowedUrl = server.getRoute('/allowed.html'); @@ -138,8 +141,7 @@ describe('Network Blocking Integration', () => { ); }, { - allowedUrlPattern: [server.getRoute('/allowed.html')], - executablePath: process.env.CHROME_M149_EXECUTABLE_PATH, + allowedUrlPattern, }, ); }); @@ -148,6 +150,7 @@ describe('Network Blocking Integration', () => { server.addHtmlRoute('/allowed.html', 'Allowed'); server.addHtmlRoute('/blocked.html', 'Blocked'); + const blockedUrlPattern = [server.getRoute('/blocked.html')]; await withMcpContext( async (response, context) => { const allowedUrl = server.getRoute('/allowed.html'); @@ -232,8 +235,7 @@ describe('Network Blocking Integration', () => { ); }, { - blockedUrlPattern: [server.getRoute('/blocked.html')], - executablePath: process.env.CHROME_M149_EXECUTABLE_PATH, + blockedUrlPattern }, ); }); From 88945c80986dc27ed381b734f81d24d1c22d55e0 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Wed, 3 Jun 2026 14:27:27 +0000 Subject: [PATCH 10/15] FORMAT --- tests/network_blocking.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/network_blocking.test.ts b/tests/network_blocking.test.ts index 2450eff68..6dd13dd8b 100644 --- a/tests/network_blocking.test.ts +++ b/tests/network_blocking.test.ts @@ -21,7 +21,7 @@ describe('Network Blocking Integration', () => { server.addHtmlRoute('/allowed.html', 'Allowed'); server.addHtmlRoute('/blocked.html', 'Blocked'); - const blockedUrlPattern= [server.getRoute('/blocked.html')]; + const blockedUrlPattern = [server.getRoute('/blocked.html')]; await withMcpContext( async (response, context) => { const allowedUrl = server.getRoute('/allowed.html'); @@ -85,7 +85,7 @@ describe('Network Blocking Integration', () => { server.addHtmlRoute('/allowed.html', 'Allowed'); server.addHtmlRoute('/blocked.html', 'Blocked'); - const allowedUrlPattern= [server.getRoute('/allowed.html')]; + const allowedUrlPattern = [server.getRoute('/allowed.html')]; await withMcpContext( async (response, context) => { @@ -235,7 +235,7 @@ describe('Network Blocking Integration', () => { ); }, { - blockedUrlPattern + blockedUrlPattern, }, ); }); From ade09a4cc83012ce867cdd837b81f352f8e7a012 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Wed, 3 Jun 2026 14:59:41 +0000 Subject: [PATCH 11/15] add a test to parse arguments --- tests/cli.test.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 83f883c5e..25f07d18e 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -373,6 +373,22 @@ describe('cli args parsing', () => { '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 () => { @@ -402,5 +418,21 @@ describe('cli args parsing', () => { '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/*', + ]); }); }); From 484c10ea9e900720afec996561b7b5e9dd639816 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Wed, 3 Jun 2026 15:11:06 +0000 Subject: [PATCH 12/15] refactor test --- tests/browser.test.ts | 86 +++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 48 deletions(-) diff --git a/tests/browser.test.ts b/tests/browser.test.ts index 30432899e..3c4ed5976 100644 --- a/tests/browser.test.ts +++ b/tests/browser.test.ts @@ -126,64 +126,54 @@ describe('browser', () => { assert.strictEqual(content, 'Allowed'); // Fetch of blocked URL from the page - const fetchResult = await page.evaluate(async url => { + const fetchSucceeded = await page.evaluate(async url => { try { await fetch(url); - return 'SUCCESS'; - } catch (err) { - return err instanceof Error ? err.message : String(err); + return true; + } catch { + return false; } }, server.getRoute('/blocked.html')); - assert.strictEqual(fetchResult, 'Failed to fetch'); + assert.strictEqual(fetchSucceeded, false); } 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', - ); + it('blocks URLs not in allowlist', async () => { + server.addHtmlRoute('/allowed.html', 'Allowed'); + server.addHtmlRoute('/blocked.html', 'Blocked'); - 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 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(); - } - }, - ); + 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(); + } + }); }); }); From b722f842759676fc4b16b91949b5df4c5e917613 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Wed, 3 Jun 2026 16:38:38 +0000 Subject: [PATCH 13/15] Throw an error if trying to set network condition if allowlist or blocklist are set --- src/McpContext.ts | 44 ++++++++++++++++++---------------- tests/network_blocking.test.ts | 41 +++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 20 deletions(-) diff --git a/src/McpContext.ts b/src/McpContext.ts index 6ddac3d98..4346070d2 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -347,27 +347,31 @@ export class McpContext implements Context { const mcpPage = this.#getMcpPage(page); const newSettings: EmulationSettings = {...mcpPage.emulationSettings}; - // 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; + // 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') { + 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/tests/network_blocking.test.ts b/tests/network_blocking.test.ts index 6dd13dd8b..fbc2279b3 100644 --- a/tests/network_blocking.test.ts +++ b/tests/network_blocking.test.ts @@ -7,6 +7,7 @@ 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'; @@ -239,4 +240,44 @@ describe('Network Blocking Integration', () => { }, ); }); + + 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, + }, + ); + }); }); From 8b6578a43783d5c8ef78dbabb09f91b16b57bedd Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Wed, 3 Jun 2026 16:56:45 +0000 Subject: [PATCH 14/15] remove custom chrome version --- scripts/test.mjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/test.mjs b/scripts/test.mjs index 46841f09b..dd28644f0 100644 --- a/scripts/test.mjs +++ b/scripts/test.mjs @@ -103,8 +103,6 @@ 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; From 86eeb4f80670dcfe52170288e8ecbd657c2a38f6 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Wed, 3 Jun 2026 17:02:24 +0000 Subject: [PATCH 15/15] format --- scripts/test.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/test.mjs b/scripts/test.mjs index dd28644f0..0d427847f 100644 --- a/scripts/test.mjs +++ b/scripts/test.mjs @@ -103,7 +103,6 @@ async function runTests(attempt) { }); } - const maxAttempts = shouldRetry ? 3 : 1; let exitCode = 1;