Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/browserServerBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,6 @@ export class BrowserServerBackend implements ServerBackend {
}

serverClosed() {
void this._context!.dispose().catch(logUnhandledError);
void this._context?.dispose().catch(logUnhandledError);
}
}
14 changes: 9 additions & 5 deletions src/mcp/proxyBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type NonEmptyArray<T> = [T, ...T[]];
export type ClientFactory = {
name: string;
description: string;
create(server: Server): Promise<Client>;
create(server: Server, options: any): Promise<Client>;
};

export type ClientFactoryList = NonEmptyArray<ClientFactory>;
Expand All @@ -51,7 +51,7 @@ export class ProxyBackend implements ServerBackend {

async initialize(server: Server): Promise<void> {
this._server = server;
await this._setCurrentClient(this._clientFactories[0]);
await this._setCurrentClient(this._clientFactories[0], undefined);
}

tools(): ToolSchema<any>[] {
Expand Down Expand Up @@ -83,7 +83,7 @@ export class ProxyBackend implements ServerBackend {
if (!factory)
throw new Error('Unknown connection method: ' + params.name);

await this._setCurrentClient(factory);
await this._setCurrentClient(factory, params.options);
return {
content: [{ type: 'text', text: '### Result\nSuccessfully changed connection method.\n' }],
};
Expand All @@ -104,10 +104,14 @@ export class ProxyBackend implements ServerBackend {
title: 'Connect to a browser context',
description: [
'Connect to a browser using one of the available methods:',
'',
...this._clientFactories.map(factory => `- "${factory.name}": ${factory.description}`),
'',
`By default, you're connected to the first method. Only call this tool to change it.`,
].join('\n'),
inputSchema: z.object({
name: z.enum(this._clientFactories.map(factory => factory.name) as [string, ...string[]]).default(this._clientFactories[0].name).describe('The method to use to connect to the browser'),
options: z.any().optional().describe('Options to pass to the connection method.'),
}),
type: 'readOnly',
},
Expand All @@ -118,9 +122,9 @@ export class ProxyBackend implements ServerBackend {
});
}

private async _setCurrentClient(factory: ClientFactory) {
private async _setCurrentClient(factory: ClientFactory, options: any) {
await this._currentClient?.close();
this._currentClient = await factory.create(this._server!);
this._currentClient = await factory.create(this._server!, options);
const tools = await this._currentClient.listTools();
this._tools = tools.tools.map(tool => ({
name: tool.name,
Expand Down
5 changes: 4 additions & 1 deletion src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { startTraceViewerServer } from 'playwright-core/lib/server';
import * as mcpTransport from './mcp/transport.js';
import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js';
import { packageJSON } from './package.js';
import { createVSCodeClientFactory } from './vscode/host.js';
import { createExtensionClientFactory, runWithExtension } from './extension/main.js';
import { Context } from './context.js';
import { contextFactory } from './browserContextFactory.js';
Expand Down Expand Up @@ -88,7 +89,9 @@ program
if (options.connectTool) {
const factories: ClientFactoryList = [
new InProcessClientFactory(browserContextFactory, config),
createExtensionClientFactory(config)
createExtensionClientFactory(config),
// TODO: enable vscode client factory without --connect-tool, just based on client name
createVSCodeClientFactory(config),
];
serverBackendFactory = () => new ProxyBackend(factories);
} else {
Expand Down
52 changes: 52 additions & 0 deletions src/vscode/host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { FullConfig } from '../config.js';
import { ClientFactory } from '../mcp/proxyBackend.js';
import { Server } from '../mcp/server.js';

class VSCodeClientFactory implements ClientFactory {
name = 'vscode';
description = 'Connect to a browser running in the Playwright VS Code extension';

constructor(private readonly _config: FullConfig) {}

async create(server: Server, options: any): Promise<Client> {
if (typeof options.connectionString !== 'string')
throw new Error('Missing options.connectionString');
if (typeof options.lib !== 'string')
throw new Error('Missing options.library');

const client = new Client(server.getClientVersion()!);
await client.connect(new StdioClientTransport({
command: process.execPath,
cwd: process.cwd(),
args: [
new URL('./main.js', import.meta.url).pathname,
JSON.stringify(this._config),
options.connectionString,
options.lib,
],
}));
await client.ping();
return client;
}
}

export function createVSCodeClientFactory(config: FullConfig): ClientFactory {
return new VSCodeClientFactory(config);
}
58 changes: 58 additions & 0 deletions src/vscode/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { BrowserContext } from 'playwright-core';
import { FullConfig } from '../config.js';
import * as mcpServer from '../mcp/server.js';
import { BrowserServerBackend } from '../browserServerBackend.js';
import { BrowserContextFactory, ClientInfo } from '../browserContextFactory.js';

const config: FullConfig = JSON.parse(process.argv[2]);
const connectionString = new URL(process.argv[3]);
const lib = process.argv[4];

const playwright = await import(lib).then(mod => mod.default ?? mod) as typeof import('playwright');

class VSCodeBrowserContextFactory implements BrowserContextFactory {
name = 'vscode';
description = 'Connect to a browser running in the Playwright VS Code extension';

async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal): Promise<{ browserContext: BrowserContext; close: () => Promise<void>; }> {
connectionString.searchParams.set('launch-options', JSON.stringify({
...config.browser.launchOptions,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: why mix them up rather than keep in separate fields?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because that's how launchPersistentContext works - I updated the code to make that clearer

...config.browser.contextOptions,
userDataDir: config.browser.userDataDir,
}));

const browser = await playwright.chromium.connect(connectionString.toString());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it supposed to work with WebKit and Firefox?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I made that clearer in the code


const context = browser.contexts()[0] ?? await browser.newContext(config.browser.contextOptions);

return {
browserContext: context,
close: async () => {
await browser.close();
}
};
}
}

await mcpServer.connect(
() => new BrowserServerBackend(config, new VSCodeBrowserContextFactory()),
new StdioServerTransport(),
false
);
Loading