From 50835bbdebae989c1213533daeb36de797cec45a Mon Sep 17 00:00:00 2001 From: masahirokokubo0513 <19896624+masamaru0513@users.noreply.github.com> Date: Wed, 13 May 2026 23:17:05 +0900 Subject: [PATCH 1/4] feat: support filePath in evaluate_script Adds an optional filePath parameter to evaluate_script that saves the script output to a file instead of returning it inline. This reduces token usage for large outputs and enables offline analysis. Follows the same context.saveFile() pattern used by take_snapshot, take_screenshot, and performance tools. Refs #153 --- docs/tool-reference.md | 17 ++++++++-------- src/tools/script.ts | 40 ++++++++++++++++++++++++++++++++------ tests/tools/script.test.ts | 29 +++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 187280d9c..b270183ea 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -345,17 +345,18 @@ so returned values have to be JSON-serializable. **Parameters:** - **function** (string) **(required)**: A JavaScript function declaration to be executed by the tool in the currently selected page. - Example without arguments: `() => { +Example without arguments: `() => { return document.title }` or `async () => { return await fetch("example.com") }`. - Example with arguments: `(el) => { +Example with arguments: `(el) => { return el.innerText; }` - **args** (array) _(optional)_: An optional list of arguments to pass to the function. - **dialogAction** (string) _(optional)_: Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept. +- **filePath** (string) _(optional)_: The absolute or relative path to a file to save the script output to. If omitted, the output is returned inline. --- @@ -556,12 +557,12 @@ in the DevTools Elements panel (if any). ### `list_3p_developer_tools` **Description:** Lists all third-party developer tools the page exposes for providing runtime information. -Third-party developer tools can be called via the '[`execute_3p_developer_tool`](#execute_3p_developer_tool)()' MCP tool. -Alternatively, third-party developer tools can be executed by calling '[`evaluate_script`](#evaluate_script)' and adding the -following command to the script: -'window.\_\_dtmcp.executeTool(toolName, params)' -This might be helpful when the third-party developer tools return non-serializable values or when composing -third-party developer tools with additional functionality. (requires flag: --categoryExperimentalThirdParty=true) + Third-party developer tools can be called via the '[`execute_3p_developer_tool`](#execute_3p_developer_tool)()' MCP tool. + Alternatively, third-party developer tools can be executed by calling '[`evaluate_script`](#evaluate_script)' and adding the + following command to the script: + 'window.__dtmcp.executeTool(toolName, params)' + This might be helpful when the third-party developer tools return non-serializable values or when composing + third-party developer tools with additional functionality. (requires flag: --categoryExperimentalThirdParty=true) **Parameters:** None diff --git a/src/tools/script.ts b/src/tools/script.ts index 1f97f174f..393f16be1 100644 --- a/src/tools/script.ts +++ b/src/tools/script.ts @@ -46,6 +46,12 @@ Example with arguments: \`(el) => { ) .optional() .describe(`An optional list of arguments to pass to the function.`), + filePath: zod + .string() + .optional() + .describe( + 'The absolute or relative path to a file to save the script output to. If omitted, the output is returned inline.', + ), dialogAction: zod .string() .optional() @@ -72,8 +78,11 @@ Example with arguments: \`(el) => { function: fnString, pageId, dialogAction, + filePath, } = request.params; + context.validatePath(filePath); + if (cliArgs?.categoryExtensions && serviceWorkerId) { if (uidArgs && uidArgs.length > 0) { throw new Error( @@ -89,7 +98,10 @@ Example with arguments: \`(el) => { .getSelectedMcpPage() .waitForEventsAfterAction( async () => { - await performEvaluation(worker, fnString, [], response); + await performEvaluation(worker, fnString, [], response, { + filePath, + context, + }); }, {handleDialog: dialogAction ?? 'accept'}, ); @@ -115,7 +127,10 @@ Example with arguments: \`(el) => { const result = await mcpPage.waitForEventsAfterAction( async () => { - await performEvaluation(evaluatable, fnString, args, response); + await performEvaluation(evaluatable, fnString, args, response, { + filePath, + context, + }); }, {handleDialog: dialogAction ?? 'accept'}, ); @@ -132,6 +147,7 @@ const performEvaluation = async ( fnString: string, args: Array>, response: Response, + options?: {filePath?: string; context?: Context}, ) => { const fn = await evaluatable.evaluateHandle(`(${fnString})`); try { @@ -143,10 +159,22 @@ const performEvaluation = async ( fn, ...args, ); - response.appendResponseLine('Script ran on page and returned:'); - response.appendResponseLine('```json'); - response.appendResponseLine(`${result}`); - response.appendResponseLine('```'); + if (options?.filePath && options.context) { + const data = new TextEncoder().encode(result ?? 'undefined'); + const {filename} = await options.context.saveFile( + data, + options.filePath, + '.json', + ); + response.appendResponseLine( + `Script ran on page. Output saved to ${filename}.`, + ); + } else { + response.appendResponseLine('Script ran on page and returned:'); + response.appendResponseLine('```json'); + response.appendResponseLine(`${result}`); + response.appendResponseLine('```'); + } } finally { void fn.dispose(); } diff --git a/tests/tools/script.test.ts b/tests/tools/script.test.ts index c87048fbe..2718deb21 100644 --- a/tests/tools/script.test.ts +++ b/tests/tools/script.test.ts @@ -279,6 +279,35 @@ describe('script', () => { assert.strictEqual(JSON.parse(lineEvaluation), 'I am iframe button'); }); }); + it('saves output to file when filePath is provided', async () => { + const {rm, readFile} = await import('node:fs/promises'); + const {tmpdir} = await import('node:os'); + const {join} = await import('node:path'); + const filePath = join(tmpdir(), 'test-evaluate-script-output.json'); + try { + await withMcpContext(async (response, context) => { + await evaluateScript().handler( + { + params: { + function: String(() => ({hello: 'world'})), + filePath, + }, + }, + response, + context, + ); + assert.strictEqual(response.responseLines.length, 1); + assert.ok( + response.responseLines[0]?.includes('Output saved to'), + `Expected "Output saved to" but got: ${response.responseLines[0]}`, + ); + }); + const content = await readFile(filePath, 'utf-8'); + assert.deepStrictEqual(JSON.parse(content), {hello: 'world'}); + } finally { + await rm(filePath, {force: true}); + } + }); it('evaluates inside extension service worker', async () => { await withMcpContext( async (response, context) => { From b94a532f689d47a08d0ff64e0c8ba09f6b56cb08 Mon Sep 17 00:00:00 2001 From: masahirokokubo0513 <19896624+masamaru0513@users.noreply.github.com> Date: Thu, 14 May 2026 00:53:25 +0900 Subject: [PATCH 2/4] fix: make context required in performEvaluation options context is always provided when filePath is set, so it should not be optional. --- src/tools/script.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/script.ts b/src/tools/script.ts index 393f16be1..0d5bb9ec8 100644 --- a/src/tools/script.ts +++ b/src/tools/script.ts @@ -147,7 +147,7 @@ const performEvaluation = async ( fnString: string, args: Array>, response: Response, - options?: {filePath?: string; context?: Context}, + options?: {filePath: string; context: Context}, ) => { const fn = await evaluatable.evaluateHandle(`(${fnString})`); try { @@ -159,7 +159,7 @@ const performEvaluation = async ( fn, ...args, ); - if (options?.filePath && options.context) { + if (options?.filePath) { const data = new TextEncoder().encode(result ?? 'undefined'); const {filename} = await options.context.saveFile( data, From 9a7799344459caf14850e0eee2b08c28cacc1a63 Mon Sep 17 00:00:00 2001 From: masahirokokubo0513 <19896624+masamaru0513@users.noreply.github.com> Date: Thu, 14 May 2026 13:48:57 +0900 Subject: [PATCH 3/4] fix: run prettier on docs/tool-reference.md --- docs/tool-reference.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/tool-reference.md b/docs/tool-reference.md index b270183ea..238d5981a 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -345,12 +345,12 @@ so returned values have to be JSON-serializable. **Parameters:** - **function** (string) **(required)**: A JavaScript function declaration to be executed by the tool in the currently selected page. -Example without arguments: `() => { + Example without arguments: `() => { return document.title }` or `async () => { return await fetch("example.com") }`. -Example with arguments: `(el) => { + Example with arguments: `(el) => { return el.innerText; }` @@ -557,12 +557,12 @@ in the DevTools Elements panel (if any). ### `list_3p_developer_tools` **Description:** Lists all third-party developer tools the page exposes for providing runtime information. - Third-party developer tools can be called via the '[`execute_3p_developer_tool`](#execute_3p_developer_tool)()' MCP tool. - Alternatively, third-party developer tools can be executed by calling '[`evaluate_script`](#evaluate_script)' and adding the - following command to the script: - 'window.__dtmcp.executeTool(toolName, params)' - This might be helpful when the third-party developer tools return non-serializable values or when composing - third-party developer tools with additional functionality. (requires flag: --categoryExperimentalThirdParty=true) +Third-party developer tools can be called via the '[`execute_3p_developer_tool`](#execute_3p_developer_tool)()' MCP tool. +Alternatively, third-party developer tools can be executed by calling '[`evaluate_script`](#evaluate_script)' and adding the +following command to the script: +'window.\_\_dtmcp.executeTool(toolName, params)' +This might be helpful when the third-party developer tools return non-serializable values or when composing +third-party developer tools with additional functionality. (requires flag: --categoryExperimentalThirdParty=true) **Parameters:** None From fa0e76bc64e6787a74a3c3f50e3ba16ce2d59adf Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Thu, 14 May 2026 07:44:38 +0200 Subject: [PATCH 4/4] chore: format --- src/bin/chrome-devtools-cli-options.ts | 7 +++++++ src/telemetry/tool_call_metrics.json | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/src/bin/chrome-devtools-cli-options.ts b/src/bin/chrome-devtools-cli-options.ts index 695f63a80..a0e315c42 100644 --- a/src/bin/chrome-devtools-cli-options.ts +++ b/src/bin/chrome-devtools-cli-options.ts @@ -187,6 +187,13 @@ export const commands: Commands = { description: 'An optional list of arguments to pass to the function.', required: false, }, + filePath: { + name: 'filePath', + type: 'string', + description: + 'The absolute or relative path to a file to save the script output to. If omitted, the output is returned inline.', + required: false, + }, dialogAction: { name: 'dialogAction', type: 'string', diff --git a/src/telemetry/tool_call_metrics.json b/src/telemetry/tool_call_metrics.json index 164300b77..2da34dc5f 100644 --- a/src/telemetry/tool_call_metrics.json +++ b/src/telemetry/tool_call_metrics.json @@ -111,6 +111,10 @@ { "name": "dialog_action_length", "argType": "number" + }, + { + "name": "file_path_length", + "argType": "number" } ] },