mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-10-12 00:25:14 +03:00
chore(vscode): expose debug controller (#979)
See https://github.com/microsoft/playwright-vscode/pull/684 for the other side.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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' }));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user