Add --video and --autostart (#271849)

This enables:
* Playwright video recording
* Starting the Code OSS on server start (for clients like Copilot CLI that don't react to tool list changes)
This commit is contained in:
Tyler James Leonhardt
2025-10-16 16:18:23 -07:00
committed by GitHub
parent e5243d2a17
commit af1cbea727
9 changed files with 89 additions and 84 deletions
+1
View File
@@ -24,6 +24,7 @@ export interface LaunchOptions {
readonly logger: Logger;
logsPath: string;
crashesPath: string;
readonly videosPath?: string;
verbose?: boolean;
useInMemorySecretStorage?: boolean;
readonly extraArgs?: string[];
+7 -1
View File
@@ -105,7 +105,13 @@ async function launchBrowser(options: LaunchOptions, endpoint: string) {
browser.on('disconnected', () => logger.log(`Playwright: browser disconnected`));
const context = await measureAndLog(() => browser.newContext(), 'browser.newContext', logger);
const context = await measureAndLog(
() => browser.newContext({
recordVideo: options.videosPath ? { dir: options.videosPath } : undefined
}),
'browser.newContext',
logger
);
if (tracing) {
try {
@@ -33,6 +33,7 @@ async function launchElectron(configuration: IElectronConfiguration, options: La
const electron = await measureAndLog(() => playwrightImpl._electron.launch({
executablePath: configuration.electronPath,
args: configuration.args,
recordVideo: options.videosPath ? { dir: options.videosPath } : undefined,
env: configuration.env as { [key: string]: string },
timeout: 0
}), 'playwright-electron#launch', logger);
+7
View File
@@ -0,0 +1,7 @@
SCRIPT_DIR="$(dirname -- "$( readlink -f -- "$0"; )")"
# Go to mcp server project root
cd "$SCRIPT_DIR/.."
# Start mcp
npm run start-stdio -- --video --autostart
+8 -66
View File
@@ -10,71 +10,12 @@ import * as fs from 'fs';
import * as os from 'os';
import * as vscodetest from '@vscode/test-electron';
import { createApp, retry } from './utils';
import * as minimist from 'minimist';
import { opts } from './options';
const rootPath = path.join(__dirname, '..', '..', '..');
const [, , ...args] = process.argv;
const opts = minimist(args, {
string: [
'browser',
'build',
'stable-build',
'wait-time',
'test-repo',
'electronArgs'
],
boolean: [
'verbose',
'remote',
'web',
'headless',
'tracing'
],
default: {
verbose: false
}
}) as {
verbose?: boolean;
remote?: boolean;
headless?: boolean;
web?: boolean;
tracing?: boolean;
build?: string;
'stable-build'?: string;
browser?: 'chromium' | 'webkit' | 'firefox' | 'chromium-msedge' | 'chromium-chrome' | undefined;
electronArgs?: string;
};
const logsRootPath = (() => {
const logsParentPath = path.join(rootPath, '.build', 'logs');
let logsName: string;
if (opts.web) {
logsName = 'smoke-tests-browser';
} else if (opts.remote) {
logsName = 'smoke-tests-remote';
} else {
logsName = 'smoke-tests-electron';
}
return path.join(logsParentPath, logsName);
})();
const crashesRootPath = (() => {
const crashesParentPath = path.join(rootPath, '.build', 'crashes');
let crashesName: string;
if (opts.web) {
crashesName = 'smoke-tests-browser';
} else if (opts.remote) {
crashesName = 'smoke-tests-remote';
} else {
crashesName = 'smoke-tests-electron';
}
return path.join(crashesParentPath, crashesName);
})();
const logsRootPath = path.join(rootPath, '.build', 'vscode-playwright-mcp', 'logs');
const crashesRootPath = path.join(rootPath, '.build', 'vscode-playwright-mcp', 'crashes');
const videoRootPath = path.join(rootPath, '.build', 'vscode-playwright-mcp', 'videos');
const logger = createLogger();
@@ -291,7 +232,7 @@ async function setup(): Promise<void> {
logger.log('Smoketest setup done!\n');
}
export async function getApplication() {
export async function getApplication({ recordVideo }: { recordVideo?: boolean } = {}) {
const testCodePath = getDevElectronPath();
const electronPath = testCodePath;
if (!fs.existsSync(electronPath || '')) {
@@ -315,6 +256,7 @@ export async function getApplication() {
logger,
logsPath: path.join(logsRootPath, 'suite_unknown'),
crashesPath: path.join(crashesRootPath, 'suite_unknown'),
videosPath: (recordVideo || opts.video) ? videoRootPath : undefined,
verbose: opts.verbose,
remote: opts.remote,
web: opts.web,
@@ -350,12 +292,12 @@ export class ApplicationService {
return this._application;
}
async getOrCreateApplication(): Promise<Application> {
async getOrCreateApplication({ recordVideo }: { recordVideo?: boolean } = {}): Promise<Application> {
if (this._closing) {
await this._closing;
}
if (!this._application) {
this._application = await getApplication();
this._application = await getApplication({ recordVideo });
this._application.code.driver.currentPage.on('close', () => {
this._closing = (async () => {
if (this._application) {
+6 -3
View File
@@ -7,6 +7,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ApplicationService } from './application';
import { applyAllTools } from './automationTools/index.js';
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { z } from 'zod';
export async function getServer(appService: ApplicationService): Promise<Server> {
const server = new McpServer({
@@ -18,9 +19,11 @@ export async function getServer(appService: ApplicationService): Promise<Server>
server.tool(
'vscode_automation_start',
'Start VS Code Build',
{},
async () => {
const app = await appService.getOrCreateApplication();
{
recordVideo: z.boolean().optional()
},
async ({ recordVideo }) => {
const app = await appService.getOrCreateApplication({ recordVideo });
return {
content: [{
type: 'text' as const,
+14 -14
View File
@@ -32,20 +32,20 @@ export function applyCoreTools(server: McpServer, appService: ApplicationService
// }
// );
// I don't think Playwright needs this
// server.tool(
// 'vscode_automation_stop',
// 'Stop the VS Code application',
// async () => {
// await app.stop();
// return {
// content: [{
// type: 'text' as const,
// text: 'VS Code stopped successfully'
// }]
// };
// }
// );
tools.push(server.tool(
'vscode_automation_stop',
'Stop the VS Code application',
async () => {
const app = await appService.getOrCreateApplication();
await app.stop();
return {
content: [{
type: 'text' as const,
text: 'VS Code stopped successfully'
}]
};
}
));
// This doesn't seem particularly useful
// server.tool(
+4
View File
@@ -11,6 +11,7 @@ import { ApplicationService } from './application';
import { createInMemoryTransportPair } from './inMemoryTransport';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { Application } from '../../automation';
import { opts } from './options';
interface SubServerConfig {
subServer: Client;
@@ -81,6 +82,9 @@ export async function getServer(): Promise<Server> {
}
});
if (opts.autostart) {
await appService.getOrCreateApplication();
}
return multiplexServer.server;
}
+41
View File
@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as minimist from 'minimist';
const [, , ...args] = process.argv;
export const opts = minimist(args, {
string: [
'browser',
'build',
'stable-build',
'wait-time',
'test-repo',
'electronArgs'
],
boolean: [
'verbose',
'remote',
'web',
'headless',
'tracing',
'video',
'autostart'
],
default: {
verbose: false
}
}) as {
verbose?: boolean;
remote?: boolean;
headless?: boolean;
web?: boolean;
tracing?: boolean;
build?: string;
'stable-build'?: string;
browser?: 'chromium' | 'webkit' | 'firefox' | 'chromium-msedge' | 'chromium-chrome' | undefined;
electronArgs?: string;
video?: boolean;
autostart?: boolean;
};