feat: add screencast fps option (#2312)

Addresses #2291.

## Summary
- add an `--experimental-screencast-fps` option for screencast recording
- validate the value as a positive integer and imply
`--experimental-screencast`
- pass the configured FPS through to Puppeteer's `screencast()` call
- update generated option docs and flag telemetry metadata

## Testing
- `git diff --check origin/main...HEAD`
- `npm run check-format`
- `npm run typecheck`
- `npm test -- --test-only tests/tools/screencast.test.ts
tests/cli.test.ts`
This commit is contained in:
mehmet turac
2026-09-01 15:58:34 +00:00
committed by GitHub
parent 552c870d3c
commit 2e641d8a8a
6 changed files with 77 additions and 5 deletions
+5
View File
@@ -704,6 +704,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:** `false`
- **`--categoryExperimentalWebmcp`/ `--category-experimental-webmcp`**
Set to true to enable debugging WebMCP tools. Requires Chrome 150+ with the following flag: `--enable-features=WebMCP`
- **Type:** boolean
+17
View File
@@ -206,6 +206,23 @@ export const mcpOptions = {
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:
+4
View File
@@ -397,5 +397,9 @@
{
"name": "javascript_evaluation",
"flagType": "boolean"
},
{
"name": "experimental_screencast_fps_present",
"flagType": "boolean"
}
]
+1
View File
@@ -89,6 +89,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
+16 -1
View File
@@ -7,7 +7,7 @@
import assert from 'node:assert';
import {describe, it} from 'node:test';
import {parser} from '../src/config/mcp-options.js';
import {mcpOptions, parser} from '../src/config/mcp-options.js';
function parseArguments(argv: string[], env: NodeJS.ProcessEnv = {}) {
return parser('0.0.0', ['node', 'main.js', ...argv], env)
@@ -232,6 +232,21 @@ describe('cli args parsing', () => {
});
});
it('rejects invalid screencast fps values', async () => {
const coerce = mcpOptions.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(['main.js'], {});
+34 -4
View File
@@ -12,7 +12,7 @@ import {describe, it, afterEach} from 'node:test';
import sinon from 'sinon';
import type {ParsedArguments} from '../../src/config/mcp-options.js';
import {parseArguments} from '../../src/config/mcp-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', () => {