diff --git a/README.md b/README.md index 3bb64ae6c..9ca265e5e 100644 --- a/README.md +++ b/README.md @@ -664,6 +664,11 @@ The Chrome DevTools MCP server supports the following configuration option: - **Type:** string - **Default:** `false` +- **`--experimentalScreencastFps`/ `--experimental-screencast-fps`** + Frames per second to use for screencast recording. Lower values can reduce memory pressure on pages that produce frames faster than ffmpeg can encode them. + - **Type:** number + - **Default:** Uses Puppeteer's screencast default. + - **`--categoryExperimentalWebmcp`/ `--category-experimental-webmcp`** Set to true to enable debugging WebMCP tools. Requires Chrome 149+ with the following flags: `--enable-features=WebMCP,DevToolsWebMCPSupport` - **Type:** boolean diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts index c943b7c38..07f0586dc 100644 --- a/src/bin/chrome-devtools-mcp-cli-options.ts +++ b/src/bin/chrome-devtools-mcp-cli-options.ts @@ -203,6 +203,23 @@ export const cliOptions = { describe: 'Path to ffmpeg executable for screencast recording.', implies: 'experimentalScreencast', }, + experimentalScreencastFps: { + type: 'number', + describe: + 'Frames per second to use for screencast recording. Lower values can reduce memory pressure on pages that produce frames faster than ffmpeg can encode them.', + implies: 'experimentalScreencast', + coerce: (value: number | undefined) => { + if (value === undefined) { + return; + } + if (!Number.isInteger(value) || value <= 0) { + throw new Error( + `Invalid experimentalScreencastFps ${value}. Expected a positive integer.`, + ); + } + return value; + }, + }, categoryExperimentalWebmcp: { type: 'boolean', describe: diff --git a/src/telemetry/flag_usage_metrics.json b/src/telemetry/flag_usage_metrics.json index da297928a..25466fb5e 100644 --- a/src/telemetry/flag_usage_metrics.json +++ b/src/telemetry/flag_usage_metrics.json @@ -371,5 +371,9 @@ { "name": "allow_unrestricted_paths", "flagType": "boolean" + }, + { + "name": "experimental_screencast_fps_present", + "flagType": "boolean" } ] diff --git a/src/tools/screencast.ts b/src/tools/screencast.ts index 7a649ab03..111732901 100644 --- a/src/tools/screencast.ts +++ b/src/tools/screencast.ts @@ -87,6 +87,7 @@ export const startScreencast = definePageTool(args => ({ path: resolvedPath, format: format, ffmpegPath: args?.experimentalFfmpegPath, + fps: args?.experimentalScreencastFps, }); } catch (err) { // If we generated a temporary directory for this recording, remove it so diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 562e6153d..c967723e0 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -7,7 +7,10 @@ import assert from 'node:assert'; import {describe, it} from 'node:test'; -import {parseArguments} from '../src/bin/chrome-devtools-mcp-cli-options.js'; +import { + cliOptions, + parseArguments, +} from '../src/bin/chrome-devtools-mcp-cli-options.js'; describe('cli args parsing', () => { const defaultArgs = { @@ -278,6 +281,21 @@ describe('cli args parsing', () => { }); }); + it('rejects invalid screencast fps values', async () => { + const coerce = cliOptions.experimentalScreencastFps.coerce; + assert.ok(coerce); + + assert.strictEqual(coerce(undefined), undefined); + assert.strictEqual(coerce(10), 10); + + for (const value of [0, -1, 10.5, Number.NaN]) { + assert.throws( + () => coerce(value), + /Invalid experimentalScreencastFps .* Expected a positive integer\./, + ); + } + }); + it('parses usage statistics flag', async () => { // Test default (should be true). const defaultArgs = parseArguments('1.0.0', ['node', 'main.js'], {}); diff --git a/tests/tools/screencast.test.ts b/tests/tools/screencast.test.ts index adf0ff2f7..9b2cd44cc 100644 --- a/tests/tools/screencast.test.ts +++ b/tests/tools/screencast.test.ts @@ -12,7 +12,7 @@ import {describe, it, afterEach} from 'node:test'; import sinon from 'sinon'; -import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js'; +import {parseArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js'; import {startScreencast, stopScreencast} from '../../src/tools/screencast.js'; import {withMcpContext} from '../utils.js'; @@ -209,9 +209,13 @@ describe('screencast', () => { .resolves(mockRecorder as never); const experimentalFfmpegPath = '/custom/path/to/ffmpeg'; - await startScreencast({ - experimentalFfmpegPath, - } as ParsedArguments).handler( + const args = parseArguments('test', [ + 'node', + 'test', + '--experimental-screencast', + `--experimental-ffmpeg-path=${experimentalFfmpegPath}`, + ]); + await startScreencast(args).handler( {params: {}, page: context.getSelectedMcpPage()}, response, context, @@ -222,6 +226,32 @@ describe('screencast', () => { assert.strictEqual(callArgs?.ffmpegPath, experimentalFfmpegPath); }); }); + + it('passes screencast fps from args to puppeteer', async () => { + await withMcpContext(async (response, context) => { + const mockRecorder = createMockRecorder(); + const selectedPage = context.getSelectedMcpPage().pptrPage; + const screencastStub = sinon + .stub(selectedPage, 'screencast') + .resolves(mockRecorder as never); + + const args = parseArguments('test', [ + 'node', + 'test', + '--experimental-screencast', + '--experimental-screencast-fps=10', + ]); + await startScreencast(args).handler( + {params: {}, page: context.getSelectedMcpPage()}, + response, + context, + ); + + sinon.assert.calledOnce(screencastStub); + const callArgs = screencastStub.firstCall.args[0]; + assert.strictEqual(callArgs?.fps, 10); + }); + }); }); describe('screencast_stop', () => {