Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/ToolHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,44 @@ function buildUnknownArgumentsMessage(
return `Unknown ${unknownLabel} for tool "${toolName}": ${formatArgumentNames(unknownArgumentNames)}. ${expectedArguments} ${correction} and retry.`;
}

function getFieldPathValue(
params: Record<string, unknown>,
fieldPath: string,
): unknown {
let value: unknown = params;
for (const field of fieldPath.split('.')) {
if (!field) {
throw new Error(`Invalid empty field in file path annotation.`);
}
if (
value === null ||
typeof value !== 'object' ||
!Object.hasOwn(value, field)
) {
return undefined;
}
value = Object.getOwnPropertyDescriptor(value, field)?.value;
}
return value;
}

async function validateFilePathFields(
context: McpContext,
params: Record<string, unknown>,
filePathFields: string[],
): Promise<void> {
for (const fieldPath of filePathFields) {
const value = getFieldPathValue(params, fieldPath);
if (value === undefined) {
continue;
}
if (typeof value !== 'string') {
throw new Error(`File path field "${fieldPath}" must be a string.`);
}
await context.validatePath(value);
}
}

export class ToolHandler {
readonly inputSchema: zod.ZodRawShape;
readonly registeredInputSchema: zod.ZodTypeAny;
Expand Down Expand Up @@ -214,6 +252,11 @@ export class ToolHandler {
const context = await this.getContext();
logger(`${this.tool.name} context: resolved`);
await context.detectOpenDevToolsWindows();
await validateFilePathFields(
context,
params,
this.tool.annotations.filePathFields,
);
const response = this.serverArgs.slim
? new SlimMcpResponse(this.serverArgs)
: new McpResponse(this.serverArgs);
Expand Down
5 changes: 5 additions & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export interface BaseToolDefinition<
annotations: {
title?: string;
category: ToolCategory;
/**
* Request parameter field paths that need workspace root validation before
* the tool handler runs.
*/
filePathFields: string[];
/**
* If true, the tool does not modify its environment.
*/
Expand Down
2 changes: 2 additions & 0 deletions src/tools/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const listConsoleMessages = definePageTool(cliArgs => {
annotations: {
category: ToolCategory.DEBUGGING,
readOnlyHint: true,
filePathFields: [],
},
schema: {
pageSize: zod
Expand Down Expand Up @@ -96,6 +97,7 @@ export const getConsoleMessage = definePageTool({
annotations: {
category: ToolCategory.DEBUGGING,
readOnlyHint: true,
filePathFields: [],
},
schema: {
msgid: zod
Expand Down
1 change: 1 addition & 0 deletions src/tools/emulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const emulate = definePageTool({
annotations: {
category: ToolCategory.EMULATION,
readOnlyHint: false,
filePathFields: [],
},
schema: {
networkConditions: zod
Expand Down
6 changes: 5 additions & 1 deletion src/tools/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const installExtension = defineTool({
annotations: {
category: ToolCategory.EXTENSIONS,
readOnlyHint: false,
filePathFields: ['path'],
},
schema: {
path: zod
Expand All @@ -24,7 +25,6 @@ export const installExtension = defineTool({
blockedByDialog: false,
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}`);
},
Expand All @@ -36,6 +36,7 @@ export const uninstallExtension = defineTool({
annotations: {
category: ToolCategory.EXTENSIONS,
readOnlyHint: false,
filePathFields: [],
},
schema: {
id: zod.string().describe('ID of the extension to uninstall.'),
Expand All @@ -55,6 +56,7 @@ export const listExtensions = defineTool({
annotations: {
category: ToolCategory.EXTENSIONS,
readOnlyHint: true,
filePathFields: [],
},
schema: {},
blockedByDialog: false,
Expand All @@ -69,6 +71,7 @@ export const reloadExtension = defineTool({
annotations: {
category: ToolCategory.EXTENSIONS,
readOnlyHint: false,
filePathFields: [],
},
schema: {
id: zod.string().describe('ID of the extension to reload.'),
Expand All @@ -92,6 +95,7 @@ export const triggerExtensionAction = defineTool({
annotations: {
category: ToolCategory.EXTENSIONS,
readOnlyHint: false,
filePathFields: [],
},
schema: {
id: zod.string().describe('ID of the extension to trigger the action for.'),
Expand Down
12 changes: 10 additions & 2 deletions src/tools/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const click = definePageTool({
annotations: {
category: ToolCategory.INPUT,
readOnlyHint: false,
filePathFields: [],
},
schema: {
uid: zod
Expand Down Expand Up @@ -145,6 +146,7 @@ export const clickAt = definePageTool({
annotations: {
category: ToolCategory.INPUT,
readOnlyHint: false,
filePathFields: [],
conditions: ['experimentalVision'],
},
schema: {
Expand Down Expand Up @@ -179,6 +181,7 @@ export const hover = definePageTool({
annotations: {
category: ToolCategory.INPUT,
readOnlyHint: false,
filePathFields: [],
},
schema: {
uid: zod
Expand Down Expand Up @@ -301,6 +304,7 @@ export const fill = definePageTool({
annotations: {
category: ToolCategory.INPUT,
readOnlyHint: false,
filePathFields: [],
},
schema: {
uid: zod
Expand Down Expand Up @@ -340,6 +344,7 @@ export const typeText = definePageTool({
annotations: {
category: ToolCategory.INPUT,
readOnlyHint: false,
filePathFields: [],
},
schema: {
text: zod.string().describe('The text to type'),
Expand Down Expand Up @@ -369,6 +374,7 @@ export const drag = definePageTool({
annotations: {
category: ToolCategory.INPUT,
readOnlyHint: false,
filePathFields: [],
},
schema: {
from_uid: zod.string().describe('The uid of the element to drag'),
Expand Down Expand Up @@ -405,6 +411,7 @@ export const fillForm = definePageTool({
annotations: {
category: ToolCategory.INPUT,
readOnlyHint: false,
filePathFields: [],
},
schema: {
elements: zod
Expand Down Expand Up @@ -450,6 +457,7 @@ export const uploadFile = definePageTool({
annotations: {
category: ToolCategory.INPUT,
readOnlyHint: false,
filePathFields: ['filePath'],
},
schema: {
uid: zod
Expand All @@ -461,9 +469,8 @@ export const uploadFile = definePageTool({
includeSnapshot: includeSnapshotSchema,
},
blockedByDialog: true,
handler: async (request, response, context) => {
handler: async (request, response, _context) => {
const {uid, filePath} = request.params;
await context.validatePath(filePath);
const handle = (await request.page.getElementByUid(
uid,
)) as ElementHandle<HTMLInputElement>;
Expand Down Expand Up @@ -502,6 +509,7 @@ export const pressKey = definePageTool({
annotations: {
category: ToolCategory.INPUT,
readOnlyHint: false,
filePathFields: [],
},
schema: {
key: zod
Expand Down
4 changes: 1 addition & 3 deletions src/tools/lighthouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const lighthouseAudit = definePageTool({
annotations: {
category: ToolCategory.DEBUGGING,
readOnlyHint: false,
filePathFields: ['outputDirPath'],
},
schema: {
mode: zod
Expand Down Expand Up @@ -58,9 +59,6 @@ export const lighthouseAudit = definePageTool({
device = 'desktop',
outputDirPath,
} = request.params;

await context.validatePath(outputDirPath);

const flags: Flags = {
onlyCategories: categories,
output: formats,
Expand Down
13 changes: 6 additions & 7 deletions src/tools/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ export const takeHeapSnapshot = definePageTool({
annotations: {
category: ToolCategory.MEMORY,
readOnlyHint: false,
filePathFields: ['filePath'],
},
schema: {
filePath: zod
.string()
.describe('A path to a .heapsnapshot file to save the heapsnapshot to.'),
},
blockedByDialog: true,
handler: async (request, response, context) => {
handler: async (request, response, _context) => {
const page = request.page;
await context.validatePath(request.params.filePath);

await page.pptrPage.captureHeapSnapshot({
path: ensureExtension(request.params.filePath, '.heapsnapshot'),
Expand All @@ -44,14 +44,14 @@ export const getHeapSnapshotSummary = defineTool({
annotations: {
category: ToolCategory.MEMORY,
readOnlyHint: true,
filePathFields: ['filePath'],
conditions: ['experimentalMemory'],
},
schema: {
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
},
blockedByDialog: false,
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,
Expand All @@ -68,6 +68,7 @@ export const getHeapSnapshotDetails = defineTool({
annotations: {
category: ToolCategory.MEMORY,
readOnlyHint: true,
filePathFields: ['filePath'],
conditions: ['experimentalMemory'],
},
schema: {
Expand All @@ -83,7 +84,6 @@ export const getHeapSnapshotDetails = defineTool({
},
blockedByDialog: false,
handler: async (request, response, context) => {
await context.validatePath(request.params.filePath);
const aggregates = await context.getHeapSnapshotAggregates(
request.params.filePath,
);
Expand All @@ -102,6 +102,7 @@ export const getHeapSnapshotClassNodes = defineTool({
annotations: {
category: ToolCategory.MEMORY,
readOnlyHint: true,
filePathFields: ['filePath'],
conditions: ['experimentalMemory'],
},
schema: {
Expand All @@ -112,7 +113,6 @@ export const getHeapSnapshotClassNodes = defineTool({
},
blockedByDialog: false,
handler: async (request, response, context) => {
await context.validatePath(request.params.filePath);
const nodes = await context.getHeapSnapshotNodesById(
request.params.filePath,
request.params.id,
Expand All @@ -132,6 +132,7 @@ export const getHeapSnapshotRetainers = defineTool({
annotations: {
category: ToolCategory.MEMORY,
readOnlyHint: true,
filePathFields: ['filePath'],
conditions: ['experimentalMemory'],
},
blockedByDialog: false,
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/tools/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const listNetworkRequests = definePageTool({
annotations: {
category: ToolCategory.NETWORK,
readOnlyHint: true,
filePathFields: [],
},
schema: {
pageSize: zod
Expand Down Expand Up @@ -93,6 +94,7 @@ export const getNetworkRequest = definePageTool({
annotations: {
category: ToolCategory.NETWORK,
readOnlyHint: false,
filePathFields: ['requestFilePath', 'responseFilePath'],
},
schema: {
reqid: zod
Expand All @@ -116,8 +118,6 @@ export const getNetworkRequest = definePageTool({
},
blockedByDialog: true,
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,
Expand Down
Loading