Skip to content
Open
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
3 changes: 3 additions & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import type {Dialog, ElementHandle, Page} from 'puppeteer-core';
import z from 'zod';

import type {TextSnapshot} from '../McpContext.js';
import type {TraceResult} from '../trace-processing/parse.js';

import type {ToolCategories} from './categories.js';
Expand Down Expand Up @@ -79,6 +80,8 @@ export type Context = Readonly<{
filename: string,
): Promise<{filename: string}>;
waitForEventsAfterAction(action: () => Promise<unknown>): Promise<void>;
createTextSnapshot(): Promise<void>;
getTextSnapshot(): TextSnapshot | null;
}>;

export function defineTool<Schema extends z.ZodRawShape>(
Expand Down
25 changes: 21 additions & 4 deletions src/tools/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import type {JSHandle} from 'puppeteer-core';
import z from 'zod';

Expand Down Expand Up @@ -42,6 +43,12 @@ Example with arguments: \`(el) => {
)
.optional()
.describe(`An optional list of arguments to pass to the function.`),
filePath: z
.string()
.optional()
.describe(
'The absolute path, or a path relative to the current working directory, to save the result to instead of including it in the response.',
),
},
handler: async (request, response, context) => {
const page = context.getSelectedPage();
Expand All @@ -59,10 +66,20 @@ Example with arguments: \`(el) => {
},
...args,
);
response.appendResponseLine('Script ran on page and returned:');
response.appendResponseLine('```json');
response.appendResponseLine(`${result}`);
response.appendResponseLine('```');

if (request.params.filePath) {
const encoder = new TextEncoder();
const data = encoder.encode(result);
const file = await context.saveFile(data, request.params.filePath);
response.appendResponseLine(
`Saved script result to ${file.filename}.`,
);
} else {
response.appendResponseLine('Script ran on page and returned:');
response.appendResponseLine('```json');
response.appendResponseLine(`${result}`);
response.appendResponseLine('```');
}
});
} finally {
Promise.allSettled(args.map(arg => arg.dispose())).catch(() => {
Expand Down
31 changes: 28 additions & 3 deletions src/tools/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import {Locator} from 'puppeteer-core';
import z from 'zod';

import {formatA11ySnapshot} from '../formatters/snapshotFormatter.js';
import {ToolCategories} from './categories.js';
import {defineTool, timeoutSchema} from './ToolDefinition.js';

Expand All @@ -18,9 +19,33 @@ identifier (uid). Always use the latest snapshot. Prefer taking a snapshot over
category: ToolCategories.DEBUGGING,
readOnlyHint: true,
},
schema: {},
handler: async (_request, response) => {
response.setIncludeSnapshot(true);
schema: {
filePath: z
.string()
.optional()
.describe(
'The absolute path, or a path relative to the current working directory, to save the snapshot to instead of including it in the response.',
),
},
handler: async (request, response, context) => {
await context.createTextSnapshot();
const snapshot = context.getTextSnapshot();

if (!snapshot) {
response.appendResponseLine('No snapshot data available.');
return;
}

const formattedSnapshot = formatA11ySnapshot(snapshot.root);

if (request.params.filePath) {
const encoder = new TextEncoder();
const data = encoder.encode(formattedSnapshot);
const file = await context.saveFile(data, request.params.filePath);
response.appendResponseLine(`Saved snapshot to ${file.filename}.`);
} else {
response.setIncludeSnapshot(true);
}
},
});

Expand Down
35 changes: 35 additions & 0 deletions tests/tools/script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert';
import fs from 'node:fs/promises';
import path from 'node:path';
import {describe, it} from 'node:test';

import {evaluateScript} from '../../src/tools/script.js';
Expand Down Expand Up @@ -152,5 +154,38 @@ describe('script', () => {
assert.strictEqual(JSON.parse(lineEvaluation), true);
});
});

it('saves result to file when filePath is provided', async () => {
const testFilePath = path.join(process.cwd(), 'test-script-result.json');

await withBrowser(async (response, context) => {
const page = context.getSelectedPage();
await page.setContent(html`<div id="data">{"key":"value"}</div>`);

await evaluateScript.handler(
{
params: {
function: String(() => ({result: 'test', number: 42})),
filePath: testFilePath,
},
},
response,
context,
);

assert.strictEqual(
response.responseLines[0],
`Saved script result to ${testFilePath}.`,
);
assert.strictEqual(response.responseLines.length, 1);

// Verify file was created and contains expected JSON
const fileContent = await fs.readFile(testFilePath, 'utf-8');
const parsed = JSON.parse(fileContent);
assert.deepEqual(parsed, {result: 'test', number: 42});
});

await fs.unlink(testFilePath);
});
});
});
29 changes: 29 additions & 0 deletions tests/tools/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert';
import fs from 'node:fs/promises';
import path from 'node:path';
import {describe, it} from 'node:test';

import {takeSnapshot, waitFor} from '../../src/tools/snapshot.js';
Expand All @@ -17,6 +19,33 @@ describe('snapshot', () => {
assert.ok(response.includeSnapshot);
});
});

it('saves snapshot to file when filePath is provided', async () => {
const testFilePath = path.join(process.cwd(), 'test-snapshot.txt');

await withBrowser(async (response, context) => {
const page = context.getSelectedPage();
await page.setContent(html`<button>Click me</button>`);

await takeSnapshot.handler(
{params: {filePath: testFilePath}},
response,
context,
);

assert.strictEqual(response.includeSnapshot, false);
assert.strictEqual(
response.responseLines[0],
`Saved snapshot to ${testFilePath}.`,
);

// Verify file was created and contains button text
const fileContent = await fs.readFile(testFilePath, 'utf-8');
assert.ok(fileContent.includes('Click me'));
});

await fs.unlink(testFilePath);
});
});
describe('browser_wait_for', () => {
it('should work', async () => {
Expand Down