get started
This commit is contained in:
@@ -29,7 +29,7 @@ type NonEmptyArray<T> = [T, ...T[]];
|
|||||||
export type ClientFactory = {
|
export type ClientFactory = {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
create(): Promise<Client>;
|
create(options: any): Promise<Client>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ClientFactoryList = NonEmptyArray<ClientFactory>;
|
export type ClientFactoryList = NonEmptyArray<ClientFactory>;
|
||||||
@@ -49,7 +49,7 @@ export class ProxyBackend implements ServerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initialize(server: Server): Promise<void> {
|
async initialize(server: Server): Promise<void> {
|
||||||
await this._setCurrentClient(this._clientFactories[0]);
|
await this._setCurrentClient(this._clientFactories[0], undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
tools(): ToolSchema<any>[] {
|
tools(): ToolSchema<any>[] {
|
||||||
@@ -81,7 +81,7 @@ export class ProxyBackend implements ServerBackend {
|
|||||||
if (!factory)
|
if (!factory)
|
||||||
throw new Error('Unknown connection method: ' + params.name);
|
throw new Error('Unknown connection method: ' + params.name);
|
||||||
|
|
||||||
await this._setCurrentClient(factory);
|
await this._setCurrentClient(factory, params.options);
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: '### Result\nSuccessfully changed connection method.\n' }],
|
content: [{ type: 'text', text: '### Result\nSuccessfully changed connection method.\n' }],
|
||||||
};
|
};
|
||||||
@@ -103,9 +103,11 @@ export class ProxyBackend implements ServerBackend {
|
|||||||
description: [
|
description: [
|
||||||
'Connect to a browser using one of the available methods:',
|
'Connect to a browser using one of the available methods:',
|
||||||
...this._clientFactories.map(factory => `- "${factory.name}": ${factory.description}`),
|
...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'),
|
].join('\n'),
|
||||||
inputSchema: z.object({
|
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'),
|
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',
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
@@ -116,9 +118,9 @@ export class ProxyBackend implements ServerBackend {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _setCurrentClient(factory: ClientFactory) {
|
private async _setCurrentClient(factory: ClientFactory, options: any) {
|
||||||
await this._currentClient?.close();
|
await this._currentClient?.close();
|
||||||
this._currentClient = await factory.create();
|
this._currentClient = await factory.create(options);
|
||||||
const tools = await this._currentClient.listTools();
|
const tools = await this._currentClient.listTools();
|
||||||
this._tools = tools.tools.map(tool => ({
|
this._tools = tools.tools.map(tool => ({
|
||||||
name: tool.name,
|
name: tool.name,
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser
|
|||||||
return { tools: tools.map(tool => ({
|
return { tools: tools.map(tool => ({
|
||||||
name: tool.name,
|
name: tool.name,
|
||||||
description: tool.description,
|
description: tool.description,
|
||||||
inputSchema: zodToJsonSchema(tool.inputSchema),
|
// TODO: we expect inputSchema to be a zod schema, but in the out-of-process case it's already a json schema.
|
||||||
|
// we should probably move the "zodToJsonSchema" call into defineTool.
|
||||||
|
inputSchema: tool.inputSchema.$schema ? tool.inputSchema : zodToJsonSchema(tool.inputSchema),
|
||||||
annotations: {
|
annotations: {
|
||||||
title: tool.title,
|
title: tool.title,
|
||||||
readOnlyHint: tool.type === 'readOnly',
|
readOnlyHint: tool.type === 'readOnly',
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { startTraceViewerServer } from 'playwright-core/lib/server';
|
|||||||
import * as mcpTransport from './mcp/transport.js';
|
import * as mcpTransport from './mcp/transport.js';
|
||||||
import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js';
|
import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js';
|
||||||
import { packageJSON } from './package.js';
|
import { packageJSON } from './package.js';
|
||||||
|
import { createVSCodeClientFactory } from './vscode/host.js';
|
||||||
import { createExtensionClientFactory, runWithExtension } from './extension/main.js';
|
import { createExtensionClientFactory, runWithExtension } from './extension/main.js';
|
||||||
import { Context } from './context.js';
|
import { Context } from './context.js';
|
||||||
import { contextFactory } from './browserContextFactory.js';
|
import { contextFactory } from './browserContextFactory.js';
|
||||||
@@ -88,7 +89,8 @@ program
|
|||||||
if (options.connectTool) {
|
if (options.connectTool) {
|
||||||
const factories: ClientFactoryList = [
|
const factories: ClientFactoryList = [
|
||||||
new InProcessClientFactory(browserContextFactory, config),
|
new InProcessClientFactory(browserContextFactory, config),
|
||||||
createExtensionClientFactory(config)
|
createExtensionClientFactory(config),
|
||||||
|
createVSCodeClientFactory(config),
|
||||||
];
|
];
|
||||||
serverBackendFactory = () => new ProxyBackend(factories);
|
serverBackendFactory = () => new ProxyBackend(factories);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
55
src/vscode/host.ts
Normal file
55
src/vscode/host.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* 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 { packageJSON } from '../package.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(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({
|
||||||
|
name: this.name,
|
||||||
|
version: packageJSON.version
|
||||||
|
});
|
||||||
|
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
src/vscode/main.ts
Normal file
58
src/vscode/main.ts
Normal file
@@ -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 = 'unused';
|
||||||
|
description = 'unused';
|
||||||
|
|
||||||
|
async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal): Promise<{ browserContext: BrowserContext; close: () => Promise<void>; }> {
|
||||||
|
connectionString.searchParams.set('launch-options', JSON.stringify({
|
||||||
|
...config.browser.launchOptions,
|
||||||
|
...config.browser.contextOptions,
|
||||||
|
userDataDir: config.browser.userDataDir,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const browser = await playwright.chromium.connect(connectionString.toString());
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user