From 777333ee7fb82856f02d4fe5d6a31e5695f3ef2d Mon Sep 17 00:00:00 2001 From: aberemia24 Date: Wed, 1 Oct 2025 19:36:39 +0300 Subject: [PATCH 1/2] feat: add filePath parameter to take_snapshot and evaluate_script Add optional filePath parameter to save tool output directly to files instead of including in response. Features: - take_snapshot can save accessibility tree to file - evaluate_script can save JSON result to file - Reduces token usage for large outputs - Enables offline analysis and processing - Backwards compatible - tools work as before when filePath is omitted Implementation: - Added filePath optional parameter to both tool schemas - Write formatted output to file using writeFile from node:fs/promises - Updated Context interface to expose createTextSnapshot and getTextSnapshot - Includes comprehensive tests for file saving functionality Resolves #153 --- src/tools/ToolDefinition.ts | 3 +++ src/tools/script.ts | 24 ++++++++++++++++++++---- src/tools/snapshot.ts | 33 ++++++++++++++++++++++++++++++--- tests/tools/script.test.ts | 35 +++++++++++++++++++++++++++++++++++ tests/tools/snapshot.test.ts | 29 +++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 7 deletions(-) diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index fe2fae7b..0bad0f07 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -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'; @@ -79,6 +80,8 @@ export type Context = Readonly<{ filename: string, ): Promise<{filename: string}>; waitForEventsAfterAction(action: () => Promise): Promise; + createTextSnapshot(): Promise; + getTextSnapshot(): TextSnapshot | null; }>; export function defineTool( diff --git a/src/tools/script.ts b/src/tools/script.ts index be46de55..1c0a5175 100644 --- a/src/tools/script.ts +++ b/src/tools/script.ts @@ -3,6 +3,8 @@ * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ +import {writeFile} from 'node:fs/promises'; + import type {JSHandle} from 'puppeteer-core'; import z from 'zod'; @@ -42,6 +44,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(); @@ -59,10 +67,18 @@ 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) { + await writeFile(request.params.filePath, result); + response.appendResponseLine( + `Saved script result to ${request.params.filePath}.`, + ); + } 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(() => { diff --git a/src/tools/snapshot.ts b/src/tools/snapshot.ts index 427e4f79..04eb8cba 100644 --- a/src/tools/snapshot.ts +++ b/src/tools/snapshot.ts @@ -4,9 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +import {writeFile} from 'node:fs/promises'; + 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'; @@ -18,9 +21,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) { + await writeFile(request.params.filePath, formattedSnapshot); + response.appendResponseLine( + `Saved snapshot to ${request.params.filePath}.`, + ); + } else { + response.setIncludeSnapshot(true); + } }, }); diff --git a/tests/tools/script.test.ts b/tests/tools/script.test.ts index bad9a902..d3680011 100644 --- a/tests/tools/script.test.ts +++ b/tests/tools/script.test.ts @@ -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'; @@ -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`
{"key":"value"}
`); + + 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); + }); }); }); diff --git a/tests/tools/snapshot.test.ts b/tests/tools/snapshot.test.ts index 31857ff5..209d4a94 100644 --- a/tests/tools/snapshot.test.ts +++ b/tests/tools/snapshot.test.ts @@ -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'; @@ -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``); + + 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 () => { From acaabdcd8e33dfd407be45f4f755d8746eb8e94f Mon Sep 17 00:00:00 2001 From: aberemia24 Date: Thu, 2 Oct 2025 14:15:22 +0300 Subject: [PATCH 2/2] refactor: use context.saveFile() for file writing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates take_snapshot and evaluate_script to use context.saveFile() instead of direct writeFile() calls, following the pattern established in PR #250. Changes: - Removed direct fs/promises imports - Convert string data to Uint8Array using TextEncoder - Use context.saveFile() for consistent file writing - Update response messages to use returned filename 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/tools/script.ts | 7 ++++--- src/tools/snapshot.ts | 10 ++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/tools/script.ts b/src/tools/script.ts index 1c0a5175..4807b089 100644 --- a/src/tools/script.ts +++ b/src/tools/script.ts @@ -3,7 +3,6 @@ * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import {writeFile} from 'node:fs/promises'; import type {JSHandle} from 'puppeteer-core'; import z from 'zod'; @@ -69,9 +68,11 @@ Example with arguments: \`(el) => { ); if (request.params.filePath) { - await writeFile(request.params.filePath, result); + const encoder = new TextEncoder(); + const data = encoder.encode(result); + const file = await context.saveFile(data, request.params.filePath); response.appendResponseLine( - `Saved script result to ${request.params.filePath}.`, + `Saved script result to ${file.filename}.`, ); } else { response.appendResponseLine('Script ran on page and returned:'); diff --git a/src/tools/snapshot.ts b/src/tools/snapshot.ts index 04eb8cba..57a29363 100644 --- a/src/tools/snapshot.ts +++ b/src/tools/snapshot.ts @@ -4,8 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {writeFile} from 'node:fs/promises'; - import {Locator} from 'puppeteer-core'; import z from 'zod'; @@ -41,10 +39,10 @@ identifier (uid). Always use the latest snapshot. Prefer taking a snapshot over const formattedSnapshot = formatA11ySnapshot(snapshot.root); if (request.params.filePath) { - await writeFile(request.params.filePath, formattedSnapshot); - response.appendResponseLine( - `Saved snapshot to ${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); }