豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit ab2a9b2

Browse files
committed
Add screencast recording tools (experimental)
Screencast tools (screencast_start, screencast_stop) are registered when the --experimental-screencast CLI flag is provided. These allow the agent to start/stop a screencast recording. ffmpeg must be installed in order to record a screencast.
1 parent 5e5b746 commit ab2a9b2

File tree

7 files changed

+379
-0
lines changed

7 files changed

+379
-0
lines changed

src/McpContext.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type {
2626
ElementHandle,
2727
HTTPRequest,
2828
Page,
29+
ScreenRecorder,
2930
SerializedAXNode,
3031
PredefinedNetworkConditions,
3132
Viewport,
@@ -121,6 +122,8 @@ export class McpContext implements Context {
121122
#extensionRegistry = new ExtensionRegistry();
122123

123124
#isRunningTrace = false;
125+
#screenRecorderData: {recorder: ScreenRecorder; filePath: string} | null =
126+
null;
124127
#networkConditionsMap = new WeakMap<Page, string>();
125128
#cpuThrottlingRateMap = new WeakMap<Page, number>();
126129
#geolocationMap = new WeakMap<Page, GeolocationOptions>();
@@ -372,6 +375,16 @@ export class McpContext implements Context {
372375
return this.#isRunningTrace;
373376
}
374377

378+
getScreenRecorder(): {recorder: ScreenRecorder; filePath: string} | null {
379+
return this.#screenRecorderData;
380+
}
381+
382+
setScreenRecorder(
383+
data: {recorder: ScreenRecorder; filePath: string} | null,
384+
): void {
385+
this.#screenRecorderData = data;
386+
}
387+
375388
isCruxEnabled(): boolean {
376389
return this.#options.performanceCrux;
377390
}

src/cli.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ export const cliOptions = {
173173
describe: 'Whether to enable interoperability tools',
174174
hidden: true,
175175
},
176+
experimentalScreencast: {
177+
type: 'boolean',
178+
describe: 'Whether to enable screencast tools',
179+
hidden: true,
180+
},
176181
chromeArg: {
177182
type: 'array',
178183
describe:

src/main.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ function registerTool(tool: ToolDefinition): void {
182182
) {
183183
return;
184184
}
185+
if (
186+
tool.annotations.conditions?.includes('screencast') &&
187+
!args.experimentalScreencast
188+
) {
189+
return;
190+
}
185191
server.registerTool(
186192
tool.name,
187193
{

src/tools/ToolDefinition.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
Dialog,
1111
ElementHandle,
1212
Page,
13+
ScreenRecorder,
1314
Viewport,
1415
} from '../third_party/index.js';
1516
import type {InsightName, TraceResult} from '../trace-processing/parse.js';
@@ -146,6 +147,10 @@ export type Context = Readonly<{
146147
* Returns a reqid for a cdpRequestId.
147148
*/
148149
resolveCdpElementId(cdpBackendNodeId: number): string | undefined;
150+
getScreenRecorder(): {recorder: ScreenRecorder; filePath: string} | null;
151+
setScreenRecorder(
152+
data: {recorder: ScreenRecorder; filePath: string} | null,
153+
): void;
149154
installExtension(path: string): Promise<string>;
150155
uninstallExtension(id: string): Promise<void>;
151156
listExtensions(): InstalledExtension[];

src/tools/screencast.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import fs from 'node:fs/promises';
8+
import os from 'node:os';
9+
import path from 'node:path';
10+
11+
import {zod} from '../third_party/index.js';
12+
import type {ScreenRecorder, VideoFormat} from '../third_party/index.js';
13+
14+
import {ToolCategory} from './categories.js';
15+
import type {Context, Response} from './ToolDefinition.js';
16+
import {defineTool} from './ToolDefinition.js';
17+
18+
async function generateTempFilePath(format: VideoFormat): Promise<string> {
19+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'chrome-devtools-mcp-'));
20+
return path.join(dir, `screencast.${format}`);
21+
}
22+
23+
export const startScreencast = defineTool({
24+
name: 'screencast_start',
25+
description:
26+
'Starts recording a screencast (video) of the selected page. Requires ffmpeg to be installed on the system.',
27+
annotations: {
28+
category: ToolCategory.DEBUGGING,
29+
readOnlyHint: false,
30+
conditions: ['screencast'],
31+
},
32+
schema: {
33+
filePath: zod
34+
.string()
35+
.optional()
36+
.describe(
37+
'The absolute file path, or a file path relative to the current working directory, to save the screencast to. For example, recording.webm. If not specified, a temporary file will be created.',
38+
),
39+
format: zod
40+
.enum(['webm', 'mp4', 'gif'])
41+
.default('webm')
42+
.describe('Specifies the output file format. Default is "webm".'),
43+
quality: zod
44+
.number()
45+
.min(0)
46+
.max(63)
47+
.optional()
48+
.describe(
49+
'Recording quality (CRF) between 0-63. Lower values mean better quality but larger files. Default is 30.',
50+
),
51+
fps: zod
52+
.number()
53+
.optional()
54+
.describe('Frame rate in frames per second. Default is 30 (20 for GIF).'),
55+
scale: zod
56+
.number()
57+
.optional()
58+
.describe(
59+
'Scales the output video. For example, 0.5 will halve the dimensions. Default is 1.',
60+
),
61+
speed: zod
62+
.number()
63+
.optional()
64+
.describe(
65+
'Playback speed multiplier. For example, 2 will double the speed. Default is 1.',
66+
),
67+
},
68+
handler: async (request, response, context) => {
69+
if (context.getScreenRecorder() !== null) {
70+
response.appendResponseLine(
71+
'Error: a screencast recording is already in progress. Use screencast_stop to stop it before starting a new one.',
72+
);
73+
return;
74+
}
75+
76+
const format = request.params.format as VideoFormat;
77+
const filePath =
78+
request.params.filePath ?? (await generateTempFilePath(format));
79+
const resolvedPath = path.resolve(filePath);
80+
81+
const page = context.getSelectedPage();
82+
83+
let recorder: ScreenRecorder;
84+
try {
85+
recorder = await page.screencast({
86+
path: resolvedPath as `${string}.${VideoFormat}`,
87+
format,
88+
quality: request.params.quality,
89+
fps: request.params.fps,
90+
scale: request.params.scale,
91+
speed: request.params.speed,
92+
});
93+
} catch (err) {
94+
const message = err instanceof Error ? err.message : String(err);
95+
if (message.includes('ENOENT') && message.includes('ffmpeg')) {
96+
throw new Error(
97+
'ffmpeg is required for screencast recording but was not found. ' +
98+
'Install ffmpeg (https://ffmpeg.org/) and ensure it is available in your PATH.',
99+
);
100+
}
101+
throw err;
102+
}
103+
104+
context.setScreenRecorder({recorder, filePath: resolvedPath});
105+
106+
response.appendResponseLine(
107+
`Screencast recording started. The recording will be saved to ${resolvedPath}. Use screencast_stop to stop recording.`,
108+
);
109+
},
110+
});
111+
112+
export const stopScreencast = defineTool({
113+
name: 'screencast_stop',
114+
description: 'Stops the active screencast recording on the selected page.',
115+
annotations: {
116+
category: ToolCategory.DEBUGGING,
117+
readOnlyHint: false,
118+
conditions: ['screencast'],
119+
},
120+
schema: {},
121+
handler: async (_request, response, context) => {
122+
await stopScreencastAndAppendOutput(response, context);
123+
},
124+
});
125+
126+
async function stopScreencastAndAppendOutput(
127+
response: Response,
128+
context: Context,
129+
): Promise<void> {
130+
const data = context.getScreenRecorder();
131+
if (!data) {
132+
return;
133+
}
134+
try {
135+
await data.recorder.stop();
136+
response.appendResponseLine(
137+
`The screencast recording has been stopped and saved to ${data.filePath}.`,
138+
);
139+
} finally {
140+
context.setScreenRecorder(null);
141+
}
142+
}

src/tools/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as inputTools from './input.js';
1111
import * as networkTools from './network.js';
1212
import * as pagesTools from './pages.js';
1313
import * as performanceTools from './performance.js';
14+
import * as screencastTools from './screencast.js';
1415
import * as screenshotTools from './screenshot.js';
1516
import * as scriptTools from './script.js';
1617
import * as snapshotTools from './snapshot.js';
@@ -24,6 +25,7 @@ const tools = [
2425
...Object.values(networkTools),
2526
...Object.values(pagesTools),
2627
...Object.values(performanceTools),
28+
...Object.values(screencastTools),
2729
...Object.values(screenshotTools),
2830
...Object.values(scriptTools),
2931
...Object.values(snapshotTools),

0 commit comments

Comments
 (0)