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

Commit e897112

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 in mp4 format. ffmpeg must be installed in order to record a screencast.
1 parent 5e5b746 commit e897112

File tree

7 files changed

+346
-0
lines changed

7 files changed

+346
-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: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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} 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(): Promise<string> {
19+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'chrome-devtools-mcp-'));
20+
return path.join(dir, `screencast.mp4`);
21+
}
22+
23+
export const startScreencast = defineTool({
24+
name: 'screencast_start',
25+
description:
26+
'Starts recording a screencast (video) of the selected page in mp4 format. 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+
'Output path. Uses mkdtemp to generate a unique path if not provided.',
38+
),
39+
},
40+
handler: async (request, response, context) => {
41+
if (context.getScreenRecorder() !== null) {
42+
response.appendResponseLine(
43+
'Error: a screencast recording is already in progress. Use screencast_stop to stop it before starting a new one.',
44+
);
45+
return;
46+
}
47+
48+
const filePath =
49+
request.params.filePath ?? (await generateTempFilePath());
50+
const resolvedPath = path.resolve(filePath);
51+
52+
const page = context.getSelectedPage();
53+
54+
let recorder: ScreenRecorder;
55+
try {
56+
recorder = await page.screencast({
57+
path: resolvedPath as `${string}.mp4`,
58+
format: 'mp4' as const,
59+
});
60+
} catch (err) {
61+
const message = err instanceof Error ? err.message : String(err);
62+
if (message.includes('ENOENT') && message.includes('ffmpeg')) {
63+
throw new Error(
64+
'ffmpeg is required for screencast recording but was not found. ' +
65+
'Install ffmpeg (https://ffmpeg.org/) and ensure it is available in your PATH.',
66+
);
67+
}
68+
throw err;
69+
}
70+
71+
context.setScreenRecorder({recorder, filePath: resolvedPath});
72+
73+
response.appendResponseLine(
74+
`Screencast recording started. The recording will be saved to ${resolvedPath}. Use screencast_stop to stop recording.`,
75+
);
76+
},
77+
});
78+
79+
export const stopScreencast = defineTool({
80+
name: 'screencast_stop',
81+
description: 'Stops the active screencast recording on the selected page.',
82+
annotations: {
83+
category: ToolCategory.DEBUGGING,
84+
readOnlyHint: false,
85+
conditions: ['screencast'],
86+
},
87+
schema: {},
88+
handler: async (_request, response, context) => {
89+
await stopScreencastAndAppendOutput(response, context);
90+
},
91+
});
92+
93+
async function stopScreencastAndAppendOutput(
94+
response: Response,
95+
context: Context,
96+
): Promise<void> {
97+
const data = context.getScreenRecorder();
98+
if (!data) {
99+
return;
100+
}
101+
try {
102+
await data.recorder.stop();
103+
response.appendResponseLine(
104+
`The screencast recording has been stopped and saved to ${data.filePath}.`,
105+
);
106+
} finally {
107+
context.setScreenRecorder(null);
108+
}
109+
}

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)