Skip to content

Commit 3d77076

Browse files
committed
fix: add proactive tool rejection when dialog is open
1 parent f1afe84 commit 3d77076

22 files changed

Lines changed: 120 additions & 15 deletions

src/McpPage.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ export class McpPage implements ContextPage {
8181
this.#dialog = undefined;
8282
}
8383

84+
throwIfDialogOpen(): void {
85+
if (this.#dialog) {
86+
throw new Error(
87+
`A dialog is open (${this.#dialog.type()}: ${this.#dialog.message()}).`,
88+
);
89+
}
90+
}
91+
8492
getInPageTools(): ToolGroup<ToolDefinition> | undefined {
8593
return this.inPageTools;
8694
}

src/index.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -233,14 +233,17 @@ export async function createMcpServer(
233233

234234
response.setRedactNetworkHeaders(serverArgs.redactNetworkHeaders);
235235
try {
236+
const page =
237+
serverArgs.experimentalPageIdRouting &&
238+
params.pageId &&
239+
!serverArgs.slim
240+
? context.getPageById(params.pageId)
241+
: context.getSelectedMcpPage();
242+
response.setPage(page);
243+
if (tool.blockedByDialog) {
244+
page.throwIfDialogOpen();
245+
}
236246
if ('pageScoped' in tool && tool.pageScoped) {
237-
const page =
238-
serverArgs.experimentalPageIdRouting &&
239-
params.pageId &&
240-
!serverArgs.slim
241-
? context.getPageById(params.pageId)
242-
: context.getSelectedMcpPage();
243-
response.setPage(page);
244247
await tool.handler(
245248
{
246249
params,

src/tools/ToolDefinition.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface BaseToolDefinition<
4646
conditions?: string[];
4747
};
4848
schema: Schema;
49+
blockedByDialog: boolean;
4950
}
5051

5152
export interface ToolDefinition<
@@ -255,6 +256,7 @@ export type ContextPage = Readonly<{
255256

256257
getDialog(): Dialog | undefined;
257258
clearDialog(): void;
259+
throwIfDialogOpen(): void;
258260
waitForEventsAfterAction(
259261
action: () => Promise<unknown>,
260262
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},

src/tools/console.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export const listConsoleMessages = definePageTool(cliArgs => {
7878
'Set to true to return the preserved messages over the last 3 navigations.',
7979
),
8080
},
81+
blockedByDialog: false,
8182
handler: async (request, response) => {
8283
response.setIncludeConsoleData(true, {
8384
pageSize: request.params.pageSize,
@@ -103,6 +104,7 @@ export const getConsoleMessage = definePageTool({
103104
'The msgid of a console message on the page from the listed console messages',
104105
),
105106
},
107+
blockedByDialog: false,
106108
handler: async (request, response) => {
107109
response.attachConsoleMessage(request.params.msgid);
108110
},

src/tools/emulation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export const emulate = definePageTool({
6666
`Emulate device viewports '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]'. 'touch' and 'mobile' to emulate mobile devices. 'landscape' to emulate landscape mode.`,
6767
),
6868
},
69+
blockedByDialog: true,
6970
handler: async (request, _response, context) => {
7071
const page = request.page;
7172
await context.emulate(request.params, page.pptrPage);

src/tools/extensions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const installExtension = defineTool({
2121
.string()
2222
.describe('Absolute path to the unpacked extension folder.'),
2323
},
24+
blockedByDialog: false,
2425
handler: async (request, response, context) => {
2526
const {path} = request.params;
2627
const id = await context.installExtension(path);
@@ -38,6 +39,7 @@ export const uninstallExtension = defineTool({
3839
schema: {
3940
id: zod.string().describe('ID of the extension to uninstall.'),
4041
},
42+
blockedByDialog: false,
4143
handler: async (request, response, context) => {
4244
const {id} = request.params;
4345
await context.uninstallExtension(id);
@@ -54,6 +56,7 @@ export const listExtensions = defineTool({
5456
readOnlyHint: true,
5557
},
5658
schema: {},
59+
blockedByDialog: false,
5760
handler: async (_request, response, _context) => {
5861
response.setListExtensions();
5962
},
@@ -69,6 +72,7 @@ export const reloadExtension = defineTool({
6972
schema: {
7073
id: zod.string().describe('ID of the extension to reload.'),
7174
},
75+
blockedByDialog: false,
7276
handler: async (request, response, context) => {
7377
const {id} = request.params;
7478
const extension = await context.getExtension(id);
@@ -90,6 +94,7 @@ export const triggerExtensionAction = defineTool({
9094
schema: {
9195
id: zod.string().describe('ID of the extension to trigger the action for.'),
9296
},
97+
blockedByDialog: false,
9398
handler: async (request, response, context) => {
9499
const {id} = request.params;
95100
await context.triggerExtensionAction(id);

src/tools/inPage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const listInPageTools = definePageTool({
5151
conditions: ['inPageTools'],
5252
},
5353
schema: {},
54+
blockedByDialog: false,
5455
handler: async (_request, response, _context) => {
5556
response.setListInPageTools();
5657
},
@@ -71,6 +72,7 @@ export const executeInPageTool = definePageTool({
7172
.optional()
7273
.describe('The JSON-stringified parameters to pass to the tool'),
7374
},
75+
blockedByDialog: false,
7476
handler: async (request, response) => {
7577
const toolName = request.params.toolName;
7678
let params: Record<string, unknown> = {};

src/tools/input.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export const click = definePageTool({
5858
dblClick: dblClickSchema,
5959
includeSnapshot: includeSnapshotSchema,
6060
},
61+
blockedByDialog: true,
6162
handler: async (request, response) => {
6263
const uid = request.params.uid;
6364
const handle = await request.page.getElementByUid(uid);
@@ -97,6 +98,7 @@ export const clickAt = definePageTool({
9798
dblClick: dblClickSchema,
9899
includeSnapshot: includeSnapshotSchema,
99100
},
101+
blockedByDialog: true,
100102
handler: async (request, response) => {
101103
const page = request.page;
102104
await page.waitForEventsAfterAction(async () => {
@@ -130,6 +132,7 @@ export const hover = definePageTool({
130132
),
131133
includeSnapshot: includeSnapshotSchema,
132134
},
135+
blockedByDialog: true,
133136
handler: async (request, response) => {
134137
const uid = request.params.uid;
135138
const handle = await request.page.getElementByUid(uid);
@@ -233,6 +236,7 @@ export const fill = definePageTool({
233236
value: zod.string().describe('The value to fill in'),
234237
includeSnapshot: includeSnapshotSchema,
235238
},
239+
blockedByDialog: true,
236240
handler: async (request, response, context) => {
237241
const page = request.page;
238242
await page.waitForEventsAfterAction(async () => {
@@ -261,6 +265,7 @@ export const typeText = definePageTool({
261265
text: zod.string().describe('The text to type'),
262266
submitKey: submitKeySchema,
263267
},
268+
blockedByDialog: true,
264269
handler: async (request, response) => {
265270
const page = request.page;
266271
await page.waitForEventsAfterAction(async () => {
@@ -289,6 +294,7 @@ export const drag = definePageTool({
289294
to_uid: zod.string().describe('The uid of the element to drop into'),
290295
includeSnapshot: includeSnapshotSchema,
291296
},
297+
blockedByDialog: true,
292298
handler: async (request, response) => {
293299
const fromHandle = await request.page.getElementByUid(
294300
request.params.from_uid,
@@ -330,6 +336,7 @@ export const fillForm = definePageTool({
330336
.describe('Elements from snapshot to fill out.'),
331337
includeSnapshot: includeSnapshotSchema,
332338
},
339+
blockedByDialog: true,
333340
handler: async (request, response, context) => {
334341
const page = request.page;
335342
for (const element of request.params.elements) {
@@ -365,6 +372,7 @@ export const uploadFile = definePageTool({
365372
filePath: zod.string().describe('The local path of the file to upload'),
366373
includeSnapshot: includeSnapshotSchema,
367374
},
375+
blockedByDialog: true,
368376
handler: async (request, response, context) => {
369377
const {uid, filePath} = request.params;
370378
context.validatePath(filePath);
@@ -415,6 +423,7 @@ export const pressKey = definePageTool({
415423
),
416424
includeSnapshot: includeSnapshotSchema,
417425
},
426+
blockedByDialog: true,
418427
handler: async (request, response) => {
419428
const page = request.page;
420429
const tokens = parseKey(request.params.key);

src/tools/lighthouse.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const lighthouseAudit = definePageTool({
4343
.optional()
4444
.describe('Directory for reports. If omitted, uses temporary files.'),
4545
},
46+
blockedByDialog: true,
4647
handler: async (request, response, context) => {
4748
const page = request.page;
4849
const categories = ['accessibility', 'seo', 'best-practices'];

src/tools/memory.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const takeMemorySnapshot = definePageTool({
2222
.string()
2323
.describe('A path to a .heapsnapshot file to save the heapsnapshot to.'),
2424
},
25+
blockedByDialog: true,
2526
handler: async (request, response, context) => {
2627
const page = request.page;
2728
context.validatePath(request.params.filePath);
@@ -48,6 +49,7 @@ export const exploreMemorySnapshot = defineTool({
4849
schema: {
4950
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
5051
},
52+
blockedByDialog: false,
5153
handler: async (request, response, context) => {
5254
context.validatePath(request.params.filePath);
5355
const stats = await context.getHeapSnapshotStats(request.params.filePath);
@@ -79,6 +81,7 @@ export const getMemorySnapshotDetails = defineTool({
7981
.optional()
8082
.describe('The page size for pagination of aggregates.'),
8183
},
84+
blockedByDialog: false,
8285
handler: async (request, response, context) => {
8386
context.validatePath(request.params.filePath);
8487
const aggregates = await context.getHeapSnapshotAggregates(
@@ -111,6 +114,7 @@ export const getNodesByClass = defineTool({
111114
pageIdx: zod.number().optional().describe('The page index for pagination.'),
112115
pageSize: zod.number().optional().describe('The page size for pagination.'),
113116
},
117+
blockedByDialog: false,
114118
handler: async (request, response, context) => {
115119
context.validatePath(request.params.filePath);
116120
const nodes = await context.getHeapSnapshotNodesByUid(

0 commit comments

Comments
 (0)