mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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.`;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -389,5 +389,13 @@
|
||||
{
|
||||
"name": "page_id_routing",
|
||||
"flagType": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "javascript_evaluation_present",
|
||||
"flagType": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "javascript_evaluation",
|
||||
"flagType": "boolean"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -21,6 +21,7 @@ export const evaluateScript = defineTool(cliArgs => {
|
||||
annotations: {
|
||||
category: ToolCategory.DEBUGGING,
|
||||
readOnlyHint: false,
|
||||
conditions: ['javascriptEvaluation'],
|
||||
},
|
||||
schema: {
|
||||
...(cliArgs?.pageIdRouting
|
||||
|
||||
@@ -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`),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user