refactor: use validate files on the tool level (#2152)

Fixes https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/2138
Closes #2150
This commit is contained in:
Nikolay Vitkov
2026-05-28 19:13:02 +02:00
committed by GitHub
parent 50cecd810b
commit 2e039c09e1
20 changed files with 66 additions and 23 deletions
+6
View File
@@ -220,6 +220,12 @@ export class ToolHandler {
response.setRedactNetworkHeaders(this.serverArgs.redactNetworkHeaders);
try {
if (this.tool.verifyFilesSchema) {
for (const key of this.tool.verifyFilesSchema) {
const filePath = params[key];
await context.validatePath(filePath as string);
}
}
if (isPageScopedTool(this.tool)) {
const pageId =
typeof params.pageId === 'number' ? params.pageId : undefined;
+1
View File
@@ -48,6 +48,7 @@ export interface BaseToolDefinition<
};
schema: Schema;
blockedByDialog: boolean;
verifyFilesSchema: Array<keyof Schema>;
}
export interface ToolDefinition<
+2
View File
@@ -79,6 +79,7 @@ export const listConsoleMessages = definePageTool(cliArgs => {
),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response) => {
response.setIncludeConsoleData(true, {
pageSize: request.params.pageSize,
@@ -105,6 +106,7 @@ export const getConsoleMessage = definePageTool({
),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response) => {
response.attachConsoleMessage(request.params.msgid);
},
+1
View File
@@ -100,6 +100,7 @@ export const emulate = definePageTool({
),
},
blockedByDialog: true,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const page = request.page;
await context.emulate(request.params, page.pptrPage);
+5 -2
View File
@@ -22,9 +22,9 @@ export const installExtension = defineTool({
.describe('Absolute path to the unpacked extension folder.'),
},
blockedByDialog: false,
verifyFilesSchema: ['path'],
handler: async (request, response, context) => {
const {path} = request.params;
await context.validatePath(path);
const id = await context.installExtension(path);
response.appendResponseLine(`Extension installed. Id: ${id}`);
},
@@ -41,6 +41,7 @@ export const uninstallExtension = defineTool({
id: zod.string().describe('ID of the extension to uninstall.'),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const {id} = request.params;
await context.uninstallExtension(id);
@@ -58,6 +59,7 @@ export const listExtensions = defineTool({
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (_request, response, _context) => {
response.setListExtensions();
},
@@ -74,13 +76,13 @@ export const reloadExtension = defineTool({
id: zod.string().describe('ID of the extension to reload.'),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const {id} = request.params;
const extension = await context.getExtension(id);
if (!extension) {
throw new Error(`Extension with ID ${id} not found.`);
}
await context.validatePath(extension.path);
await context.installExtension(extension.path);
response.appendResponseLine('Extension reloaded.');
},
@@ -97,6 +99,7 @@ export const triggerExtensionAction = defineTool({
id: zod.string().describe('ID of the extension to trigger the action for.'),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const {id} = request.params;
await context.triggerExtensionAction(id);
+10 -2
View File
@@ -103,6 +103,7 @@ export const click = definePageTool({
includeSnapshot: includeSnapshotSchema,
},
blockedByDialog: true,
verifyFilesSchema: [],
handler: async (request, response) => {
const uid = request.params.uid;
const handle = await request.page.getElementByUid(uid);
@@ -154,6 +155,7 @@ export const clickAt = definePageTool({
includeSnapshot: includeSnapshotSchema,
},
blockedByDialog: true,
verifyFilesSchema: [],
handler: async (request, response) => {
const page = request.page;
const result = await page.waitForEventsAfterAction(async () => {
@@ -189,6 +191,7 @@ export const hover = definePageTool({
includeSnapshot: includeSnapshotSchema,
},
blockedByDialog: true,
verifyFilesSchema: [],
handler: async (request, response) => {
const uid = request.params.uid;
const handle = await request.page.getElementByUid(uid);
@@ -316,6 +319,7 @@ export const fill = definePageTool({
includeSnapshot: includeSnapshotSchema,
},
blockedByDialog: true,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const page = request.page;
const result = await page.waitForEventsAfterAction(async () => {
@@ -346,6 +350,7 @@ export const typeText = definePageTool({
submitKey: submitKeySchema,
},
blockedByDialog: true,
verifyFilesSchema: [],
handler: async (request, response) => {
const page = request.page;
const result = await page.waitForEventsAfterAction(async () => {
@@ -376,6 +381,7 @@ export const drag = definePageTool({
includeSnapshot: includeSnapshotSchema,
},
blockedByDialog: true,
verifyFilesSchema: [],
handler: async (request, response) => {
const fromHandle = await request.page.getElementByUid(
request.params.from_uid,
@@ -423,6 +429,7 @@ export const fillForm = definePageTool({
includeSnapshot: includeSnapshotSchema,
},
blockedByDialog: true,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const page = request.page;
let lastResult: WaitForEventsResult = {};
@@ -461,9 +468,9 @@ export const uploadFile = definePageTool({
includeSnapshot: includeSnapshotSchema,
},
blockedByDialog: true,
handler: async (request, response, context) => {
verifyFilesSchema: ['filePath'],
handler: async (request, response, _context) => {
const {uid, filePath} = request.params;
await context.validatePath(filePath);
const handle = (await request.page.getElementByUid(
uid,
)) as ElementHandle<HTMLInputElement>;
@@ -512,6 +519,7 @@ export const pressKey = definePageTool({
includeSnapshot: includeSnapshotSchema,
},
blockedByDialog: true,
verifyFilesSchema: [],
handler: async (request, response) => {
const page = request.page;
const tokens = parseKey(request.params.key);
+1 -2
View File
@@ -44,6 +44,7 @@ export const lighthouseAudit = definePageTool({
.describe('Directory for reports. If omitted, uses temporary files.'),
},
blockedByDialog: true,
verifyFilesSchema: ['outputDirPath'],
handler: async (request, response, context) => {
const page = request.page;
const categories = [
@@ -59,8 +60,6 @@ export const lighthouseAudit = definePageTool({
outputDirPath,
} = request.params;
await context.validatePath(outputDirPath);
const flags: Flags = {
onlyCategories: categories,
output: formats,
+6 -7
View File
@@ -23,9 +23,9 @@ export const takeHeapSnapshot = definePageTool({
.describe('A path to a .heapsnapshot file to save the heapsnapshot to.'),
},
blockedByDialog: true,
handler: async (request, response, context) => {
verifyFilesSchema: ['filePath'],
handler: async (request, response) => {
const page = request.page;
await context.validatePath(request.params.filePath);
await page.pptrPage.captureHeapSnapshot({
path: ensureExtension(request.params.filePath, '.heapsnapshot'),
@@ -50,8 +50,8 @@ export const getHeapSnapshotSummary = defineTool({
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
},
blockedByDialog: false,
verifyFilesSchema: ['filePath'],
handler: async (request, response, context) => {
await context.validatePath(request.params.filePath);
const stats = await context.getHeapSnapshotStats(request.params.filePath);
const staticData = await context.getHeapSnapshotStaticData(
request.params.filePath,
@@ -82,8 +82,8 @@ export const getHeapSnapshotDetails = defineTool({
.describe('The page size for pagination of aggregates.'),
},
blockedByDialog: false,
verifyFilesSchema: ['filePath'],
handler: async (request, response, context) => {
await context.validatePath(request.params.filePath);
const aggregates = await context.getHeapSnapshotAggregates(
request.params.filePath,
);
@@ -111,8 +111,8 @@ export const getHeapSnapshotClassNodes = defineTool({
pageSize: zod.number().optional().describe('The page size for pagination.'),
},
blockedByDialog: false,
verifyFilesSchema: ['filePath'],
handler: async (request, response, context) => {
await context.validatePath(request.params.filePath);
const nodes = await context.getHeapSnapshotNodesById(
request.params.filePath,
request.params.id,
@@ -135,6 +135,7 @@ export const getHeapSnapshotRetainers = defineTool({
conditions: ['experimentalMemory'],
},
blockedByDialog: false,
verifyFilesSchema: ['filePath'],
schema: {
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
nodeId: zod.number().describe('The node ID to get retainers for.'),
@@ -142,8 +143,6 @@ export const getHeapSnapshotRetainers = defineTool({
pageSize: zod.number().optional().describe('The page size for pagination.'),
},
handler: async (request, response, context) => {
await context.validatePath(request.params.filePath);
const retainers = await context.getHeapSnapshotRetainers(
request.params.filePath,
request.params.nodeId,
+2 -2
View File
@@ -71,6 +71,7 @@ export const listNetworkRequests = definePageTool({
),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const data = await request.page.getDevToolsData();
response.attachDevToolsData(data);
@@ -115,9 +116,8 @@ export const getNetworkRequest = definePageTool({
),
},
blockedByDialog: true,
verifyFilesSchema: ['requestFilePath', 'responseFilePath'],
handler: async (request, response, context) => {
await context.validatePath(request.params.requestFilePath);
await context.validatePath(request.params.responseFilePath);
if (request.params.reqid) {
response.attachNetworkRequest(request.params.reqid, {
requestFilePath: request.params.requestFilePath,
+8
View File
@@ -85,6 +85,7 @@ export const listPages = defineTool(args => {
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (_request, response) => {
response.setIncludePages(true);
response.setListThirdPartyDeveloperTools();
@@ -112,6 +113,7 @@ export const selectPage = defineTool({
.describe('Whether to focus the page and bring it to the top.'),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const page = context.getPageById(request.params.pageId);
context.selectPage(page);
@@ -137,6 +139,7 @@ export const closePage = defineTool({
.describe('The ID of the page to close. Call list_pages to list pages.'),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response, context) => {
try {
await context.closePage(request.params.pageId);
@@ -189,6 +192,7 @@ export const newPage = defineTool(args => {
...timeoutSchema,
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const page = await context.newPage(
request.params.background,
@@ -256,6 +260,7 @@ export const navigatePage = definePageTool(args => {
...timeoutSchema,
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response) => {
const page = request.page;
const options = {
@@ -391,6 +396,7 @@ export const resizePage = definePageTool({
height: zod.number().describe('Page height'),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response, _context) => {
const page = request.page;
@@ -436,6 +442,7 @@ export const handleDialog = definePageTool({
.describe('Optional prompt text to enter into the dialog.'),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response, _context) => {
const page = request.page;
const dialog = page.getDialog();
@@ -487,6 +494,7 @@ export const getTabId = definePageTool({
),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const page = context.getPageById(request.params.pageId);
const tabId = (page.pptrPage as unknown as CdpPage)._tabId;
+3 -2
View File
@@ -48,8 +48,8 @@ export const startTrace = definePageTool({
filePath: filePathSchema,
},
blockedByDialog: true,
verifyFilesSchema: ['filePath'],
handler: async (request, response, context) => {
await context.validatePath(request.params.filePath);
if (context.isRunningPerformanceTrace()) {
response.appendResponseLine(
'Error: a performance trace is already running. Use performance_stop_trace to stop it. Only one trace can be running at any given time.',
@@ -127,8 +127,8 @@ export const stopTrace = definePageTool({
filePath: filePathSchema,
},
blockedByDialog: true,
verifyFilesSchema: ['filePath'],
handler: async (request, response, context) => {
await context.validatePath(request.params.filePath);
if (!context.isRunningPerformanceTrace()) {
return;
}
@@ -163,6 +163,7 @@ export const analyzeInsight = definePageTool({
),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const lastRecording = context.recordedTraces().at(-1);
if (!lastRecording) {
+2 -1
View File
@@ -39,8 +39,8 @@ export const startScreencast = definePageTool(args => ({
),
},
blockedByDialog: false,
verifyFilesSchema: ['filePath'],
handler: async (request, response, context) => {
await context.validatePath(request.params.filePath);
if (context.getScreenRecorder() !== null) {
response.appendResponseLine(
'Error: a screencast recording is already in progress. Use screencast_stop to stop it before starting a new one.',
@@ -103,6 +103,7 @@ export const stopScreencast = definePageTool({
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (_request, response, context) => {
const data = context.getScreenRecorder();
if (!data) {
+1 -1
View File
@@ -51,8 +51,8 @@ export const screenshot = definePageTool({
),
},
blockedByDialog: true,
verifyFilesSchema: ['filePath'],
handler: async (request, response, context) => {
await context.validatePath(request.params.filePath);
if (request.params.uid && request.params.fullPage) {
throw new Error('Providing both "uid" and "fullPage" is not allowed.');
}
+1 -2
View File
@@ -71,6 +71,7 @@ Example with arguments: \`(el) => {
: {}),
},
blockedByDialog: true,
verifyFilesSchema: ['filePath'],
handler: async (request, response, context) => {
const {
serviceWorkerId,
@@ -81,8 +82,6 @@ Example with arguments: \`(el) => {
filePath,
} = request.params;
await context.validatePath(filePath);
if (cliArgs?.categoryExtensions && serviceWorkerId) {
if (uidArgs && uidArgs.length > 0) {
throw new Error(
+3
View File
@@ -19,6 +19,7 @@ export const screenshot = definePageTool({
},
schema: {},
blockedByDialog: true,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const page = request.page;
const screenshot = await page.pptrPage.screenshot({
@@ -44,6 +45,7 @@ export const navigate = definePageTool({
url: zod.string().describe('URL to navigate to'),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response) => {
const page = request.page;
@@ -82,6 +84,7 @@ export const evaluate = definePageTool({
script: zod.string().describe(`JS script to run on the page`),
},
blockedByDialog: true,
verifyFilesSchema: [],
handler: async (request, response) => {
const page = request.page;
try {
+3 -2
View File
@@ -34,8 +34,8 @@ in the DevTools Elements panel (if any).`,
),
},
blockedByDialog: true,
handler: async (request, response, context) => {
await context.validatePath(request.params.filePath);
verifyFilesSchema: ['filePath'],
handler: async (request, response) => {
response.includeSnapshot({
verbose: request.params.verbose ?? false,
filePath: request.params.filePath,
@@ -60,6 +60,7 @@ export const waitFor = definePageTool({
...timeoutSchema,
},
blockedByDialog: true,
verifyFilesSchema: [],
handler: async (request, response, context) => {
const page = request.page;
await context.waitForTextOnPage(
+2
View File
@@ -51,6 +51,7 @@ export const listThirdPartyDeveloperTools = definePageTool({
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (_request, response, _context) => {
response.setListThirdPartyDeveloperTools();
},
@@ -71,6 +72,7 @@ export const executeThirdPartyDeveloperTool = definePageTool({
.describe('The JSON-stringified parameters to pass to the tool'),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response) => {
const toolName = request.params.toolName;
let params: Record<string, unknown> = {};
+2
View File
@@ -18,6 +18,7 @@ export const listWebMcpTools = definePageTool({
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (_request, response, _context) => {
response.setListWebMcpTools();
},
@@ -38,6 +39,7 @@ export const executeWebMcpTool = definePageTool({
.describe('The JSON-stringified parameters to pass to the WebMCP tool'),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (request, response) => {
const toolName = request.params.toolName;
+4
View File
@@ -37,6 +37,7 @@ describe('ToolHandler', () => {
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: [],
pageScoped: true,
handler: async () => {
handlerCalled = true;
@@ -78,6 +79,7 @@ describe('ToolHandler', () => {
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async () => {
handlerCalled = true;
},
@@ -120,6 +122,7 @@ describe('ToolHandler', () => {
url: zod.string(),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async () => {
handlerCalled = true;
},
@@ -167,6 +170,7 @@ describe('ToolHandler', () => {
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async () => {
handlerCalled = true;
},
+3
View File
@@ -47,6 +47,7 @@ describe('metricsRegistry', () => {
uid: zod.string(), // Should be blocked
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async () => {
// no-op
},
@@ -72,6 +73,7 @@ describe('metricsRegistry', () => {
argEnum: zod.enum(['foo', 'bar']),
},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async () => {
// no-op
},
@@ -93,6 +95,7 @@ describe('metricsRegistry', () => {
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async () => {
// no-op
},