mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
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:
committed by
GitHub
parent
7afd01673f
commit
02b4492ca6
@@ -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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -305,5 +305,13 @@
|
||||
{
|
||||
"name": "memory_debugging",
|
||||
"flagType": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "blocked_url_pattern_present",
|
||||
"flagType": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "allowed_url_pattern_present",
|
||||
"flagType": "boolean"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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/*',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user