chore(vscode): expose debug controller (#979)

See https://github.com/microsoft/playwright-vscode/pull/684 for the
other side.
This commit is contained in:
Simon Knott
2025-09-02 08:59:36 +02:00
committed by GitHub
parent e8e2af40b7
commit b85dc6954a
2 changed files with 125 additions and 4 deletions

View File

@@ -33,10 +33,12 @@ import { contextFactory } from '../browserContextFactory.js';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import type { ClientVersion, ServerBackend } from '../mcp/server.js';
import type { Root, Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import type { Browser, BrowserContext, BrowserServer } from 'playwright';
const contextSwitchOptions = z.object({
connectionString: z.string().optional().describe('The connection string to use to connect to the browser'),
lib: z.string().optional().describe('The library to use for the connection'),
debugController: z.boolean().optional().describe('Enable the debug controller')
});
class VSCodeProxyBackend implements ServerBackend {
@@ -47,15 +49,18 @@ class VSCodeProxyBackend implements ServerBackend {
private _contextSwitchTool: Tool;
private _roots: Root[] = [];
private _clientVersion?: ClientVersion;
private _context?: BrowserContext;
private _browser?: Browser;
private _browserServer?: BrowserServer;
constructor(private readonly _config: FullConfig, private readonly _defaultTransportFactory: () => Promise<Transport>) {
constructor(private readonly _config: FullConfig, private readonly _defaultTransportFactory: (delegate: VSCodeProxyBackend) => Promise<Transport>) {
this._contextSwitchTool = this._defineContextSwitchTool();
}
async initialize(server: mcpServer.Server, clientVersion: ClientVersion, roots: Root[]): Promise<void> {
this._clientVersion = clientVersion;
this._roots = roots;
const transport = await this._defaultTransportFactory();
const transport = await this._defaultTransportFactory(this);
await this._setCurrentClient(transport);
}
@@ -80,9 +85,47 @@ class VSCodeProxyBackend implements ServerBackend {
void this._currentClient?.close().catch(logUnhandledError);
}
onContext(context: BrowserContext) {
this._context = context;
context.on('close', () => {
this._context = undefined;
});
}
private async _getDebugControllerURL() {
if (!this._context)
return;
const browser = this._context.browser() as any;
if (!browser || !browser._launchServer)
return;
if (this._browser !== browser)
this._browserServer = undefined;
if (!this._browserServer)
this._browserServer = await browser._launchServer({ _debugController: true }) as BrowserServer;
const url = new URL(this._browserServer.wsEndpoint());
url.searchParams.set('debug-controller', '1');
return url.toString();
}
private async _callContextSwitchTool(params: z.infer<typeof contextSwitchOptions>): Promise<CallToolResult> {
if (params.debugController) {
const url = await this._getDebugControllerURL();
const lines = [`### Result`];
if (url) {
lines.push(`URL: ${url}`);
lines.push(`Version: ${packageJSON.dependencies.playwright}`);
} else {
lines.push(`No open browsers.`);
}
return { content: [{ type: 'text', text: lines.join('\n') }] };
}
if (!params.connectionString || !params.lib) {
const transport = await this._defaultTransportFactory();
const transport = await this._defaultTransportFactory(this);
await this._setCurrentClient(transport);
return {
content: [{ type: 'text', text: '### Result\nSuccessfully disconnected.\n' }],
@@ -142,7 +185,20 @@ export async function runVSCodeTools(config: FullConfig) {
name: 'Playwright w/ vscode',
nameInConfig: 'playwright-vscode',
version: packageJSON.version,
create: () => new VSCodeProxyBackend(config, () => mcpServer.wrapInProcess(new BrowserServerBackend(config, contextFactory(config))))
create: () => new VSCodeProxyBackend(
config,
delegate => mcpServer.wrapInProcess(
new BrowserServerBackend(config,
{
async createContext(clientInfo, abortSignal, toolName) {
const context = await contextFactory(config).createContext(clientInfo, abortSignal, toolName);
delegate.onContext(context.browserContext);
return context;
},
}
)
)
)
};
await mcpServer.start(serverBackendFactory, config.server);
return;

View File

@@ -52,3 +52,68 @@ test('browser_connect(vscode) works', async ({ startClient, playwright, browserN
result: expect.stringContaining('ECONNREFUSED')
});
});
test('browser_connect(debugController) works', async ({ startClient }) => {
test.skip(!globalThis.WebSocket, 'WebSocket is not supported in this environment');
const { client } = await startClient({
args: ['--vscode'],
});
expect(await client.callTool({
name: 'browser_connect',
arguments: {
debugController: true,
}
})).toHaveResponse({
result: 'No open browsers.'
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
url: 'data:text/html,foo'
}
})).toHaveResponse({
pageState: expect.stringContaining('foo'),
});
const response = await client.callTool({
name: 'browser_connect',
arguments: {
debugController: true,
}
});
expect(response.content?.[0].text).toMatch(/Version: \d+\.\d+\.\d+/);
const url = new URL(response.content?.[0].text.match(/URL: (.*)/)?.[1]);
const messages: unknown[] = [];
const socket = new WebSocket(url);
socket.onmessage = event => {
messages.push(JSON.parse(event.data));
};
await new Promise((resolve, reject) => {
socket.onopen = resolve;
socket.onerror = reject;
});
socket.send(JSON.stringify({
id: '1',
guid: 'DebugController',
method: 'setReportStateChanged',
params: {
enabled: true,
},
metadata: {},
}));
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
url: 'data:text/html,bar'
}
})).toHaveResponse({
pageState: expect.stringContaining('bar'),
});
await expect.poll(() => messages).toContainEqual(expect.objectContaining({ method: 'stateChanged' }));
});