feat: add an option to turn off js execution tools (#2627)

While we keep the evaluate script available by default since it is
useful for many users, we give an option to disable it.
This commit is contained in:
Alex Rudenko
2026-08-31 09:10:30 +00:00
committed by GitHub
parent fd67970881
commit a78566ee7c
12 changed files with 217 additions and 7 deletions
+5
View File
@@ -768,6 +768,11 @@ The Chrome DevTools MCP server supports the following configuration option:
- **Type:** boolean
- **Default:** `true`
- **`--javascriptEvaluation`/ `--javascript-evaluation`**
Set to false to disable tools to evaluate JavaScript scripts.
- **Type:** boolean
- **Default:** `true`
- **`--screenshotFormat`/ `--screenshot-format`**
Override the default output format used by take_screenshot when the caller does not specify one. JPEG and WebP are ~3-5x smaller than PNG, which helps reduce context size in AI conversations. Unset preserves the existing default ("png").
- **Type:** string
+5 -2
View File
@@ -10,7 +10,7 @@ import path from 'node:path';
import {Client} from '@modelcontextprotocol/sdk/client/index.js';
import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js';
import {parseArguments} from '../build/src/config/mcp-options.js';
import {mcpOptions, parseArguments} from '../build/src/config/mcp-options.js';
import {buildFlag} from '../build/src/index.js';
import {
labels,
@@ -174,7 +174,10 @@ async function generateCli() {
const conditions = toolNameToConditions.get(tool.name) || [];
for (const condition of conditions) {
requiredFlags.push(`--${condition}=true`);
const option = mcpOptions[condition as keyof typeof mcpOptions];
if (!option || !('default' in option) || option.default !== true) {
requiredFlags.push(`--${condition}=true`);
}
}
if (requiredFlags.length > 0) {
+16 -3
View File
@@ -95,9 +95,19 @@ function addCrossLinks(text: string, tools: ToolWithAnnotations[]): string {
return result;
}
function hasOffByDefaultConditions(tool: ToolWithAnnotations): boolean {
for (const condition of tool.annotations?.conditions || []) {
const option = mcpOptions[condition as keyof typeof mcpOptions];
if (!option || !('default' in option) || option.default !== true) {
return true;
}
}
return false;
}
function sortTools(a: ToolWithAnnotations, b: ToolWithAnnotations): number {
const aHasConditions = Boolean(a.annotations?.conditions?.length > 0);
const bHasConditions = Boolean(b.annotations?.conditions?.length > 0);
const aHasConditions = hasOffByDefaultConditions(a);
const bHasConditions = hasOffByDefaultConditions(b);
if (aHasConditions && !bHasConditions) {
return 1;
@@ -357,7 +367,10 @@ async function generateReference(
const conditions = tool.annotations?.conditions || [];
for (const condition of conditions) {
requiredFlags.push(`--${condition}=true`);
const option = mcpOptions[condition as keyof typeof mcpOptions];
if (!option || !('default' in option) || option.default !== true) {
requiredFlags.push(`--${condition}=true`);
}
}
if (requiredFlags.length > 0) {
+1 -1
View File
@@ -39,7 +39,7 @@ function buildDisabledMessage(
): string {
const reason = categoryLabel
? `is in category ${categoryLabel} which`
: `requires experimental feature ${flag} and`;
: `requires ${flag.startsWith('--experimental') ? 'experimental feature' : 'flag'} ${flag} and`;
return `Tool ${toolName} ${reason} is currently disabled. Enable it by running chrome-devtools start ${flag}=true. For more information check the README.`;
}
+9
View File
@@ -280,6 +280,11 @@ export const mcpOptions = {
describe:
'Set to false to opt-out of usage statistics collection. Google collects usage data to improve the tool, handled under the Google Privacy Policy (https://policies.google.com/privacy). This is independent from Chrome browser metrics. Disabled if `CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS` or `CI` env variables are set.',
},
javascriptEvaluation: {
type: 'boolean',
default: true,
describe: 'Set to false to disable tools to evaluate JavaScript scripts.',
},
clearcutEndpoint: {
type: 'string',
hidden: true,
@@ -538,6 +543,10 @@ export function parser(
'$0 --no-performance-crux',
'Disable CrUX (field data) integration in performance tools.',
],
[
'$0 --no-javascript-evaluation',
'Disable tools to evaluate JavaScript scripts.',
],
[
'$0 --slim',
'Only 3 tools: navigation, JavaScript execution and screenshot',
+8
View File
@@ -389,5 +389,13 @@
{
"name": "page_id_routing",
"flagType": "boolean"
},
{
"name": "javascript_evaluation_present",
"flagType": "boolean"
},
{
"name": "javascript_evaluation",
"flagType": "boolean"
}
]
+1
View File
@@ -21,6 +21,7 @@ export const evaluateScript = defineTool(cliArgs => {
annotations: {
category: ToolCategory.DEBUGGING,
readOnlyHint: false,
conditions: ['javascriptEvaluation'],
},
schema: {
...(cliArgs?.pageIdRouting
+1
View File
@@ -79,6 +79,7 @@ export const evaluate = definePageTool({
annotations: {
category: ToolCategory.DEBUGGING,
readOnlyHint: false,
conditions: ['javascriptEvaluation'],
},
schema: {
script: zod.string().describe(`JS script to run on the page`),
+119
View File
@@ -23,6 +23,7 @@ import type {
DefinedPageTool,
ToolDefinition,
} from '../src/tools/ToolDefinition.js';
import {createTools} from '../src/tools/tools.js';
import {getMockBrowser} from './utils.js';
import {Mutex} from '../src/third_party/index.js';
@@ -352,6 +353,124 @@ describe('ToolHandler', () => {
assert.strictEqual(handlerCalled, false);
});
it('registers evaluate_script by default and disables it when javascriptEvaluation is false', async () => {
const mockContext = sinon.createStubInstance(McpContext);
const toolMutex = new Mutex();
const defaultServerArgs = parseArguments('1.0.0', ['node', 'script.js'], {
CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true',
});
const defaultTool = createTools(defaultServerArgs).find(
t => t.name === 'evaluate_script',
);
if (!defaultTool) {
assert.fail('evaluate_script not found');
}
const defaultHandler = new ToolHandler(
defaultTool,
defaultServerArgs,
async () => mockContext,
toolMutex,
);
assert.strictEqual(defaultHandler.shouldRegister, true);
const disabledServerArgs = parseArguments(
'1.0.0',
['node', 'script.js', '--no-javascript-evaluation'],
{CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true'},
);
const disabledTool = createTools(disabledServerArgs).find(
t => t.name === 'evaluate_script',
);
if (!disabledTool) {
assert.fail('evaluate_script not found');
}
const disabledHandler = new ToolHandler(
disabledTool,
disabledServerArgs,
async () => mockContext,
toolMutex,
);
assert.strictEqual(disabledHandler.shouldRegister, false);
const disabledResult = await disabledHandler.handle({function: '() => 1'});
assert.strictEqual(disabledResult.isError, true);
assert.match(
disabledResult.content[0].type === 'text'
? disabledResult.content[0].text
: '',
/Tool evaluate_script requires flag --javascriptEvaluation and is currently disabled/,
);
const cliServerArgs = parseArguments(
'1.0.0',
['node', 'script.js', '--no-javascript-evaluation', '--viaCli'],
{CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true'},
);
const cliTool = createTools(cliServerArgs).find(
t => t.name === 'evaluate_script',
);
if (!cliTool) {
assert.fail('evaluate_script not found');
}
const cliHandler = new ToolHandler(
cliTool,
cliServerArgs,
async () => mockContext,
toolMutex,
);
assert.strictEqual(cliHandler.shouldRegister, true);
const cliResult = await cliHandler.handle({function: '() => 1'});
assert.strictEqual(cliResult.isError, true);
assert.match(
cliResult.content[0].type === 'text' ? cliResult.content[0].text : '',
/Tool evaluate_script requires flag --javascriptEvaluation and is currently disabled/,
);
});
it('disables slim evaluate tool when javascriptEvaluation is false', async () => {
const mockContext = sinon.createStubInstance(McpContext);
const toolMutex = new Mutex();
const defaultServerArgs = parseArguments(
'1.0.0',
['node', 'script.js', '--slim'],
{CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true'},
);
const defaultTool = createTools(defaultServerArgs).find(
t => t.name === 'evaluate',
);
if (!defaultTool) {
assert.fail('evaluate not found');
}
const defaultHandler = new ToolHandler(
defaultTool,
defaultServerArgs,
async () => mockContext,
toolMutex,
);
assert.strictEqual(defaultHandler.shouldRegister, true);
const disabledServerArgs = parseArguments(
'1.0.0',
['node', 'script.js', '--slim', '--javascriptEvaluation=false'],
{CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true'},
);
const disabledTool = createTools(disabledServerArgs).find(
t => t.name === 'evaluate',
);
if (!disabledTool) {
assert.fail('evaluate not found');
}
const disabledHandler = new ToolHandler(
disabledTool,
disabledServerArgs,
async () => mockContext,
toolMutex,
);
assert.strictEqual(disabledHandler.shouldRegister, false);
});
it('validates files specified in verifyFilesSchema and rewrites input with validated paths/URLs', async () => {
let handlerCalled = false;
let receivedParams: Record<string, unknown> | undefined;
+15
View File
@@ -25,6 +25,7 @@ describe('cli args parsing', () => {
autoConnect: undefined,
performanceCrux: true,
usageStatistics: true,
javascriptEvaluation: true,
redactNetworkHeaders: false,
allowUnrestrictedPaths: false,
memoryDebugging: false,
@@ -245,6 +246,20 @@ describe('cli args parsing', () => {
assert.strictEqual(disabledArgs.usageStatistics, false);
});
it('parses javascript evaluation flag', async () => {
// Test default (should be true).
const defaultArgs = parseArguments(['main.js'], {});
assert.strictEqual(defaultArgs.javascriptEvaluation, true);
// Test enabling it
const enabledArgs = parseArguments(['--javascript-evaluation']);
assert.strictEqual(enabledArgs.javascriptEvaluation, true);
// Test disabling it
const disabledArgs = parseArguments(['--no-javascript-evaluation']);
assert.strictEqual(disabledArgs.javascriptEvaluation, false);
});
it('respects env variable', async () => {
// Test default (should be true).
const defaultArgs = parseArguments(['main.js'], {
@@ -107,6 +107,25 @@ describe('chrome-devtools', () => {
);
});
it('fails to invoke evaluate_script when javascriptEvaluation is disabled', async () => {
await runCli(['start', '--no-javascript-evaluation'], sessionId);
const result = await runCli(['evaluate_script', '() => 1'], sessionId);
assert.strictEqual(result.status, 0);
assert(
result.stdout.includes(
'Tool evaluate_script requires flag --javascriptEvaluation and is currently disabled',
),
'error message is unexpected: ' + result.stdout,
);
assert(
result.stdout.includes(
'chrome-devtools start --javascriptEvaluation=true',
),
'restart command suggestion is missing: ' + result.stdout,
);
});
it('can record a performance trace', async () => {
const startResult = await runCli(
['start', '--performanceCrux=false'],
+18 -1
View File
@@ -21,6 +21,7 @@ import {
} from '@modelcontextprotocol/sdk/types.js';
import {executablePath} from 'puppeteer';
import {mcpOptions} from '../src/config/mcp-options.js';
import type {ToolCategory} from '../src/tools/categories.js';
import {OFF_BY_DEFAULT_CATEGORIES} from '../src/tools/categories.js';
import type {ToolDefinition} from '../src/tools/ToolDefinition.js';
@@ -204,6 +205,17 @@ describe('e2e', () => {
);
});
it('can disable javascript evaluation tools', async () => {
await withClient(
async client => {
const {tools} = await client.listTools();
const evaluateScript = tools.find(t => t.name === 'evaluate_script');
assert.strictEqual(evaluateScript, undefined);
},
['--no-javascript-evaluation'],
);
});
it('updates roots when client notifies', async () => {
const roots = [{uri: 'file:///test-root', name: 'test-root'}];
let resolvePromise: () => void;
@@ -490,7 +502,12 @@ function toolShouldBeSkipped(
filteredOutCategories: ToolCategory[],
) {
if (tool.annotations?.conditions) {
return true;
for (const condition of tool.annotations.conditions) {
const option = mcpOptions[condition as keyof typeof mcpOptions];
if (!option || !('default' in option) || option.default !== true) {
return true;
}
}
}
if (
tool.annotations?.category &&