Skip to content

Commit 0dc113d

Browse files
committed
feat: support saving snapshots to file
1 parent 21af8da commit 0dc113d

5 files changed

Lines changed: 58 additions & 35 deletions

File tree

src/McpResponse.ts

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@ import type {
2424
TextContent,
2525
} from './third_party/index.js';
2626
import {handleDialog} from './tools/pages.js';
27-
import type {ImageContentData, Response} from './tools/ToolDefinition.js';
27+
import type {
28+
ImageContentData,
29+
Response,
30+
SnapshotParams,
31+
} from './tools/ToolDefinition.js';
2832
import {paginate} from './utils/pagination.js';
2933
import type {PaginationOptions} from './utils/types.js';
3034

3135
export class McpResponse implements Response {
3236
#includePages = false;
33-
#includeSnapshot = false;
34-
#includeVerboseSnapshot = false;
37+
#snapshotParams?: SnapshotParams;
3538
#attachedNetworkRequestId?: number;
3639
#attachedConsoleMessageId?: number;
3740
#textResponseLines: string[] = [];
@@ -53,9 +56,8 @@ export class McpResponse implements Response {
5356
this.#includePages = value;
5457
}
5558

56-
setIncludeSnapshot(value: boolean, verbose = false): void {
57-
this.#includeSnapshot = value;
58-
this.#includeVerboseSnapshot = verbose;
59+
includeSnapshot(params?: SnapshotParams): void {
60+
this.#snapshotParams = params;
5961
}
6062

6163
setIncludeNetworkRequests(
@@ -158,12 +160,8 @@ export class McpResponse implements Response {
158160
return this.#images;
159161
}
160162

161-
get includeSnapshot(): boolean {
162-
return this.#includeSnapshot;
163-
}
164-
165-
get includeVersboseSnapshot(): boolean {
166-
return this.#includeVerboseSnapshot;
163+
get snapshotParams(): SnapshotParams | undefined {
164+
return this.#snapshotParams;
167165
}
168166

169167
async handle(
@@ -173,8 +171,22 @@ export class McpResponse implements Response {
173171
if (this.#includePages) {
174172
await context.createPagesSnapshot();
175173
}
176-
if (this.#includeSnapshot) {
177-
await context.createTextSnapshot(this.#includeVerboseSnapshot);
174+
175+
let formattedSnapshot: string | undefined;
176+
if (this.#snapshotParams) {
177+
await context.createTextSnapshot(this.#snapshotParams.verbose);
178+
const snapshot = context.getTextSnapshot();
179+
if (snapshot) {
180+
if (this.#snapshotParams.filePath) {
181+
await context.saveFile(
182+
new TextEncoder().encode(formatA11ySnapshot(snapshot.root)),
183+
this.#snapshotParams.filePath,
184+
);
185+
formattedSnapshot = `Saved screenshot to ${this.#snapshotParams.filePath}.`;
186+
} else {
187+
formattedSnapshot = formatA11ySnapshot(snapshot.root);
188+
}
189+
}
178190
}
179191

180192
const bodies: {
@@ -281,6 +293,7 @@ export class McpResponse implements Response {
281293
bodies,
282294
consoleData,
283295
consoleListData,
296+
formattedSnapshot,
284297
});
285298
}
286299

@@ -294,6 +307,7 @@ export class McpResponse implements Response {
294307
};
295308
consoleData: ConsoleMessageData | undefined;
296309
consoleListData: ConsoleMessageData[] | undefined;
310+
formattedSnapshot: string | undefined;
297311
},
298312
): Array<TextContent | ImageContent> {
299313
const response = [`# ${toolName} response`];
@@ -339,13 +353,9 @@ Call ${handleDialog.name} to handle it before continuing.`);
339353
response.push(...parts);
340354
}
341355

342-
if (this.#includeSnapshot) {
343-
const snapshot = context.getTextSnapshot();
344-
if (snapshot) {
345-
const formattedSnapshot = formatA11ySnapshot(snapshot.root);
346-
response.push('## Page content');
347-
response.push(formattedSnapshot);
348-
}
356+
if (data.formattedSnapshot) {
357+
response.push('## Page content');
358+
response.push(data.formattedSnapshot);
349359
}
350360

351361
response.push(...this.#formatNetworkRequestData(context, data.bodies));

src/tools/ToolDefinition.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ export interface ImageContentData {
4242
mimeType: string;
4343
}
4444

45+
export interface SnapshotParams {
46+
verbose?: boolean;
47+
filePath?: string;
48+
}
49+
4550
export interface Response {
4651
appendResponseLine(value: string): void;
4752
setIncludePages(value: boolean): void;
@@ -59,8 +64,7 @@ export interface Response {
5964
includePreservedMessages?: boolean;
6065
},
6166
): void;
62-
setIncludeSnapshot(value: boolean): void;
63-
setIncludeSnapshot(value: boolean, verbose?: boolean): void;
67+
includeSnapshot(params?: SnapshotParams): void;
6468
attachImage(value: ImageContentData): void;
6569
attachNetworkRequest(reqid: number): void;
6670
attachConsoleMessage(msgid: number): void;

src/tools/input.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const click = defineTool({
4444
? `Successfully double clicked on the element`
4545
: `Successfully clicked on the element`,
4646
);
47-
response.setIncludeSnapshot(true);
47+
response.includeSnapshot(true);
4848
} finally {
4949
void handle.dispose();
5050
}
@@ -73,7 +73,7 @@ export const hover = defineTool({
7373
await handle.asLocator().hover();
7474
});
7575
response.appendResponseLine(`Successfully hovered over the element`);
76-
response.setIncludeSnapshot(true);
76+
response.includeSnapshot(true);
7777
} finally {
7878
void handle.dispose();
7979
}
@@ -159,7 +159,7 @@ export const fill = defineTool({
159159
);
160160
});
161161
response.appendResponseLine(`Successfully filled out the element`);
162-
response.setIncludeSnapshot(true);
162+
response.includeSnapshot(true);
163163
},
164164
});
165165

@@ -184,7 +184,7 @@ export const drag = defineTool({
184184
await toHandle.drop(fromHandle);
185185
});
186186
response.appendResponseLine(`Successfully dragged an element`);
187-
response.setIncludeSnapshot(true);
187+
response.includeSnapshot(true);
188188
} finally {
189189
void fromHandle.dispose();
190190
void toHandle.dispose();
@@ -220,7 +220,7 @@ export const fillForm = defineTool({
220220
});
221221
}
222222
response.appendResponseLine(`Successfully filled out the form`);
223-
response.setIncludeSnapshot(true);
223+
response.includeSnapshot(true);
224224
},
225225
});
226226

@@ -264,7 +264,7 @@ export const uploadFile = defineTool({
264264
);
265265
}
266266
}
267-
response.setIncludeSnapshot(true);
267+
response.includeSnapshot(true);
268268
response.appendResponseLine(`File uploaded from ${filePath}.`);
269269
} finally {
270270
void handle.dispose();
@@ -304,6 +304,6 @@ export const pressKey = defineTool({
304304
response.appendResponseLine(
305305
`Successfully pressed key: ${request.params.key}`,
306306
);
307-
response.setIncludeSnapshot(true);
307+
response.includeSnapshot(true);
308308
},
309309
});

src/tools/snapshot.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,18 @@ identifier (uid). Always use the latest snapshot. Prefer taking a snapshot over
2424
.describe(
2525
'Whether to include all possible information available in the full a11y tree. Default is false.',
2626
),
27+
filePath: zod
28+
.string()
29+
.optional()
30+
.describe(
31+
'The absolute path, or a path relative to the current working directory, to save the snapshot to instead of attaching it to the response.',
32+
),
2733
},
2834
handler: async (request, response) => {
29-
response.setIncludeSnapshot(true, request.params.verbose ?? false);
35+
response.includeSnapshot({
36+
verbose: request.params.verbose ?? false,
37+
filePath: request.params.filePath,
38+
});
3039
},
3140
});
3241

@@ -48,6 +57,6 @@ export const waitFor = defineTool({
4857
`Element with text "${request.params.text}" found.`,
4958
);
5059

51-
response.setIncludeSnapshot(true);
60+
response.includeSnapshot(true);
5261
},
5362
});

tests/McpResponse.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Testing 2`,
5454
await page.setContent(`<!DOCTYPE html>
5555
<button>Click me</button><input type="text" value="Input">`);
5656
await page.focus('button');
57-
response.setIncludeSnapshot(true);
57+
response.includeSnapshot(true);
5858
const result = await response.handle('test', context);
5959
assert.equal(result[0].type, 'text');
6060
assert.strictEqual(
@@ -80,7 +80,7 @@ uid=1_0 RootWebArea
8080
/></label>`,
8181
);
8282
await page.focus('input');
83-
response.setIncludeSnapshot(true);
83+
response.includeSnapshot(true);
8484
const result = await response.handle('test', context);
8585
assert.equal(result[0].type, 'text');
8686
assert.strictEqual(
@@ -99,7 +99,7 @@ uid=1_0 RootWebArea "My test page"
9999
await withBrowser(async (response, context) => {
100100
const page = context.getSelectedPage();
101101
await page.setContent(html`<aside>test</aside>`);
102-
response.setIncludeSnapshot(true, true);
102+
response.includeSnapshot(true, true);
103103
const result = await response.handle('test', context);
104104
assert.equal(result[0].type, 'text');
105105
assert.strictEqual(

0 commit comments

Comments
 (0)