Merge branch 'main' into vscode-client-factory
This commit is contained in:
28
package-lock.json
generated
28
package-lock.json
generated
@@ -14,8 +14,8 @@
|
|||||||
"debug": "^4.4.1",
|
"debug": "^4.4.1",
|
||||||
"dotenv": "^17.2.0",
|
"dotenv": "^17.2.0",
|
||||||
"mime": "^4.0.7",
|
"mime": "^4.0.7",
|
||||||
"playwright": "1.55.0-alpha-2025-08-07",
|
"playwright": "1.55.0-alpha-2025-08-12",
|
||||||
"playwright-core": "1.55.0-alpha-2025-08-07",
|
"playwright-core": "1.55.0-alpha-2025-08-12",
|
||||||
"ws": "^8.18.1",
|
"ws": "^8.18.1",
|
||||||
"zod": "^3.24.1",
|
"zod": "^3.24.1",
|
||||||
"zod-to-json-schema": "^3.24.4"
|
"zod-to-json-schema": "^3.24.4"
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"@anthropic-ai/sdk": "^0.57.0",
|
"@anthropic-ai/sdk": "^0.57.0",
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@eslint/js": "^9.19.0",
|
"@eslint/js": "^9.19.0",
|
||||||
"@playwright/test": "1.55.0-alpha-2025-08-07",
|
"@playwright/test": "1.55.0-alpha-2025-08-12",
|
||||||
"@stylistic/eslint-plugin": "^3.0.1",
|
"@stylistic/eslint-plugin": "^3.0.1",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/node": "^22.13.10",
|
"@types/node": "^22.13.10",
|
||||||
@@ -703,13 +703,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.55.0-alpha-2025-08-07",
|
"version": "1.55.0-alpha-2025-08-12",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0-alpha-2025-08-07.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0-alpha-2025-08-12.tgz",
|
||||||
"integrity": "sha512-N83L8JSSJ+E690HCbgzmXIcbRfM/rlh0uWZhbHbMp9q4qDPABSgvhm0HGiG345PV1ozoqcCI/mXLZPircsmPIA==",
|
"integrity": "sha512-lyq9MDSd4UcOWx5292AYLBfbYYCstg8iLb+lk6LdM69ps6bwmPloZO3Ol3JO3FQQ63qAuW9VD0w+ZYKL0lRmQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.55.0-alpha-2025-08-07"
|
"playwright": "1.55.0-alpha-2025-08-12"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@@ -3745,12 +3745,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.55.0-alpha-2025-08-07",
|
"version": "1.55.0-alpha-2025-08-12",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0-alpha-2025-08-07.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0-alpha-2025-08-12.tgz",
|
||||||
"integrity": "sha512-rH8kdQOZzhjxC6FOL9zSEDwPl88ZqQq9QEvRDONWhzKwRQ/jOXlEZRxm8QRCBdrLqBMTGHx/YOaP7MIV//rtIA==",
|
"integrity": "sha512-daZPM5gX0VTG6ae3/qOpEKc9NxoavkM2lfL0UIzTG0k+yK8ZeSPYo63iewZhVANsWRm0BT+XQ1NniAUOwWQ+xA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.55.0-alpha-2025-08-07"
|
"playwright-core": "1.55.0-alpha-2025-08-12"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@@ -3763,9 +3763,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.55.0-alpha-2025-08-07",
|
"version": "1.55.0-alpha-2025-08-12",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0-alpha-2025-08-07.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0-alpha-2025-08-12.tgz",
|
||||||
"integrity": "sha512-NUuC6R0/dLk1QKiYoJL8NUsQAC6Je0C2BpuIg5h4wcvBwJ5TFldslmik17Txg3TXBSqwgG76DAl4Q6UdHGn54Q==",
|
"integrity": "sha512-4uxOd9xmeF6gqdsORzzlXd7p795vcACOiAGVHHEiTuFXsD83LYH+0C/SYLWB0Z+fAq4LdKGsy0qEfTm0JkY8Ig==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
|
|||||||
@@ -43,8 +43,8 @@
|
|||||||
"debug": "^4.4.1",
|
"debug": "^4.4.1",
|
||||||
"dotenv": "^17.2.0",
|
"dotenv": "^17.2.0",
|
||||||
"mime": "^4.0.7",
|
"mime": "^4.0.7",
|
||||||
"playwright": "1.55.0-alpha-2025-08-07",
|
"playwright": "1.55.0-alpha-2025-08-12",
|
||||||
"playwright-core": "1.55.0-alpha-2025-08-07",
|
"playwright-core": "1.55.0-alpha-2025-08-12",
|
||||||
"ws": "^8.18.1",
|
"ws": "^8.18.1",
|
||||||
"zod": "^3.24.1",
|
"zod": "^3.24.1",
|
||||||
"zod-to-json-schema": "^3.24.4"
|
"zod-to-json-schema": "^3.24.4"
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
"@anthropic-ai/sdk": "^0.57.0",
|
"@anthropic-ai/sdk": "^0.57.0",
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@eslint/js": "^9.19.0",
|
"@eslint/js": "^9.19.0",
|
||||||
"@playwright/test": "1.55.0-alpha-2025-08-07",
|
"@playwright/test": "1.55.0-alpha-2025-08-12",
|
||||||
"@stylistic/eslint-plugin": "^3.0.1",
|
"@stylistic/eslint-plugin": "^3.0.1",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/node": "^22.13.10",
|
"@types/node": "^22.13.10",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[*]
|
[*]
|
||||||
./tools/
|
./tools/
|
||||||
./mcp/
|
./mcp/
|
||||||
|
./utils/
|
||||||
|
|
||||||
[program.ts]
|
[program.ts]
|
||||||
***
|
***
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ import path from 'path';
|
|||||||
import * as playwright from 'playwright';
|
import * as playwright from 'playwright';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { registryDirectory } from 'playwright-core/lib/server/registry/index';
|
import { registryDirectory } from 'playwright-core/lib/server/registry/index';
|
||||||
import { logUnhandledError, testDebug } from './log.js';
|
// @ts-ignore
|
||||||
import { createHash } from './utils.js';
|
import { startTraceViewerServer } from 'playwright-core/lib/server';
|
||||||
|
import { logUnhandledError, testDebug } from './utils/log.js';
|
||||||
|
import { createHash } from './utils/guid.js';
|
||||||
import { outputFile } from './config.js';
|
import { outputFile } from './config.js';
|
||||||
|
|
||||||
import type { FullConfig } from './config.js';
|
import type { FullConfig } from './config.js';
|
||||||
@@ -50,7 +52,6 @@ class BaseContextFactory implements BrowserContextFactory {
|
|||||||
readonly description: string;
|
readonly description: string;
|
||||||
readonly config: FullConfig;
|
readonly config: FullConfig;
|
||||||
protected _browserPromise: Promise<playwright.Browser> | undefined;
|
protected _browserPromise: Promise<playwright.Browser> | undefined;
|
||||||
protected _tracesDir: string | undefined;
|
|
||||||
|
|
||||||
constructor(name: string, description: string, config: FullConfig) {
|
constructor(name: string, description: string, config: FullConfig) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -58,11 +59,11 @@ class BaseContextFactory implements BrowserContextFactory {
|
|||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _obtainBrowser(): Promise<playwright.Browser> {
|
protected async _obtainBrowser(clientInfo: ClientInfo): Promise<playwright.Browser> {
|
||||||
if (this._browserPromise)
|
if (this._browserPromise)
|
||||||
return this._browserPromise;
|
return this._browserPromise;
|
||||||
testDebug(`obtain browser (${this.name})`);
|
testDebug(`obtain browser (${this.name})`);
|
||||||
this._browserPromise = this._doObtainBrowser();
|
this._browserPromise = this._doObtainBrowser(clientInfo);
|
||||||
void this._browserPromise.then(browser => {
|
void this._browserPromise.then(browser => {
|
||||||
browser.on('disconnected', () => {
|
browser.on('disconnected', () => {
|
||||||
this._browserPromise = undefined;
|
this._browserPromise = undefined;
|
||||||
@@ -73,16 +74,13 @@ class BaseContextFactory implements BrowserContextFactory {
|
|||||||
return this._browserPromise;
|
return this._browserPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _doObtainBrowser(): Promise<playwright.Browser> {
|
protected async _doObtainBrowser(clientInfo: ClientInfo): Promise<playwright.Browser> {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
async createContext(clientInfo: ClientInfo): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> {
|
async createContext(clientInfo: ClientInfo): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> {
|
||||||
if (this.config.saveTrace)
|
|
||||||
this._tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces-${Date.now()}`);
|
|
||||||
|
|
||||||
testDebug(`create browser context (${this.name})`);
|
testDebug(`create browser context (${this.name})`);
|
||||||
const browser = await this._obtainBrowser();
|
const browser = await this._obtainBrowser(clientInfo);
|
||||||
const browserContext = await this._doCreateContext(browser);
|
const browserContext = await this._doCreateContext(browser);
|
||||||
return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) };
|
return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) };
|
||||||
}
|
}
|
||||||
@@ -108,11 +106,11 @@ class IsolatedContextFactory extends BaseContextFactory {
|
|||||||
super('isolated', 'Create a new isolated browser context', config);
|
super('isolated', 'Create a new isolated browser context', config);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async _doObtainBrowser(): Promise<playwright.Browser> {
|
protected override async _doObtainBrowser(clientInfo: ClientInfo): Promise<playwright.Browser> {
|
||||||
await injectCdpPort(this.config.browser);
|
await injectCdpPort(this.config.browser);
|
||||||
const browserType = playwright[this.config.browser.browserName];
|
const browserType = playwright[this.config.browser.browserName];
|
||||||
return browserType.launch({
|
return browserType.launch({
|
||||||
tracesDir: this._tracesDir,
|
tracesDir: await startTraceServer(this.config, clientInfo.rootPath),
|
||||||
...this.config.browser.launchOptions,
|
...this.config.browser.launchOptions,
|
||||||
handleSIGINT: false,
|
handleSIGINT: false,
|
||||||
handleSIGTERM: false,
|
handleSIGTERM: false,
|
||||||
@@ -175,9 +173,7 @@ class PersistentContextFactory implements BrowserContextFactory {
|
|||||||
await injectCdpPort(this.config.browser);
|
await injectCdpPort(this.config.browser);
|
||||||
testDebug('create browser context (persistent)');
|
testDebug('create browser context (persistent)');
|
||||||
const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo.rootPath);
|
const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo.rootPath);
|
||||||
let tracesDir: string | undefined;
|
const tracesDir = await startTraceServer(this.config, clientInfo.rootPath);
|
||||||
if (this.config.saveTrace)
|
|
||||||
tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces-${Date.now()}`);
|
|
||||||
|
|
||||||
this._userDataDirs.add(userDataDir);
|
this._userDataDirs.add(userDataDir);
|
||||||
testDebug('lock user data dir', userDataDir);
|
testDebug('lock user data dir', userDataDir);
|
||||||
@@ -242,3 +238,16 @@ async function findFreePort(): Promise<number> {
|
|||||||
server.on('error', reject);
|
server.on('error', reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function startTraceServer(config: FullConfig, rootPath: string | undefined): Promise<string | undefined> {
|
||||||
|
if (!config.saveTrace)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
const tracesDir = await outputFile(config, rootPath, `traces-${Date.now()}`);
|
||||||
|
const server = await startTraceViewerServer();
|
||||||
|
const urlPrefix = server.urlPrefix('human-readable');
|
||||||
|
const url = urlPrefix + '/trace/index.html?trace=' + tracesDir + '/trace.json';
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('\nTrace viewer listening on ' + url);
|
||||||
|
return tracesDir;
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,11 +17,12 @@
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { FullConfig } from './config.js';
|
import { FullConfig } from './config.js';
|
||||||
import { Context } from './context.js';
|
import { Context } from './context.js';
|
||||||
import { logUnhandledError } from './log.js';
|
import { logUnhandledError } from './utils/log.js';
|
||||||
import { Response } from './response.js';
|
import { Response } from './response.js';
|
||||||
import { SessionLog } from './sessionLog.js';
|
import { SessionLog } from './sessionLog.js';
|
||||||
import { filteredTools } from './tools.js';
|
import { filteredTools } from './tools.js';
|
||||||
import { packageJSON } from './package.js';
|
import { packageJSON } from './utils/package.js';
|
||||||
|
import { toToolDefinition } from './tools/tool.js';
|
||||||
|
|
||||||
import type { Tool } from './tools/tool.js';
|
import type { Tool } from './tools/tool.js';
|
||||||
import type { BrowserContextFactory } from './browserContextFactory.js';
|
import type { BrowserContextFactory } from './browserContextFactory.js';
|
||||||
@@ -45,11 +46,9 @@ export class BrowserServerBackend implements ServerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initialize(server: mcpServer.Server): Promise<void> {
|
async initialize(server: mcpServer.Server): Promise<void> {
|
||||||
const capabilities = server.getClientCapabilities() as mcpServer.ClientCapabilities;
|
const capabilities = server.getClientCapabilities();
|
||||||
let rootPath: string | undefined;
|
let rootPath: string | undefined;
|
||||||
if (capabilities.roots && (
|
if (capabilities?.roots) {
|
||||||
server.getClientVersion()?.name === 'Visual Studio Code' ||
|
|
||||||
server.getClientVersion()?.name === 'Visual Studio Code - Insiders')) {
|
|
||||||
const { roots } = await server.listRoots();
|
const { roots } = await server.listRoots();
|
||||||
const firstRootUri = roots[0]?.uri;
|
const firstRootUri = roots[0]?.uri;
|
||||||
const url = firstRootUri ? new URL(firstRootUri) : undefined;
|
const url = firstRootUri ? new URL(firstRootUri) : undefined;
|
||||||
@@ -65,15 +64,15 @@ export class BrowserServerBackend implements ServerBackend {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
tools(): mcpServer.ToolSchema<any>[] {
|
tools(): mcpServer.ToolDefinition[] {
|
||||||
return this._tools.map(tool => tool.schema);
|
return this._tools.map(tool => toToolDefinition(tool.schema));
|
||||||
}
|
}
|
||||||
|
|
||||||
async callTool(schema: mcpServer.ToolSchema<any>, rawArguments: any) {
|
async callTool(name: string, rawArguments: any) {
|
||||||
|
const tool = this._tools.find(tool => tool.schema.name === name)!;
|
||||||
|
const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
|
||||||
const context = this._context!;
|
const context = this._context!;
|
||||||
const parsedArguments = schema.inputSchema.parse(rawArguments || {});
|
const response = new Response(context, name, parsedArguments);
|
||||||
const response = new Response(context, schema.name, parsedArguments);
|
|
||||||
const tool = this._tools.find(tool => tool.schema.name === schema.name)!;
|
|
||||||
context.setRunningTool(true);
|
context.setRunningTool(true);
|
||||||
try {
|
try {
|
||||||
await tool.handle(context, parsedArguments, response);
|
await tool.handle(context, parsedArguments, response);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import fs from 'fs';
|
|||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { devices } from 'playwright';
|
import { devices } from 'playwright';
|
||||||
import { sanitizeForFilePath } from './utils.js';
|
import { sanitizeForFilePath } from './utils/fileUtils.js';
|
||||||
|
|
||||||
import type { Config, ToolCapability } from '../config.js';
|
import type { Config, ToolCapability } from '../config.js';
|
||||||
import type { BrowserContextOptions, LaunchOptions } from 'playwright';
|
import type { BrowserContextOptions, LaunchOptions } from 'playwright';
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import * as playwright from 'playwright';
|
import * as playwright from 'playwright';
|
||||||
|
|
||||||
import { logUnhandledError } from './log.js';
|
import { logUnhandledError } from './utils/log.js';
|
||||||
import { Tab } from './tab.js';
|
import { Tab } from './tab.js';
|
||||||
import { outputFile } from './config.js';
|
import { outputFile } from './config.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
[*]
|
[*]
|
||||||
../
|
|
||||||
../mcp/
|
../mcp/
|
||||||
|
../utils/
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ import { spawn } from 'child_process';
|
|||||||
import http from 'http';
|
import http from 'http';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import { WebSocket, WebSocketServer } from 'ws';
|
import { WebSocket, WebSocketServer } from 'ws';
|
||||||
import { httpAddressToString } from '../httpServer.js';
|
import { httpAddressToString } from '../utils/httpServer.js';
|
||||||
import { logUnhandledError } from '../log.js';
|
import { logUnhandledError } from '../utils/log.js';
|
||||||
import { ManualPromise } from '../manualPromise.js';
|
import { ManualPromise } from '../utils/manualPromise.js';
|
||||||
import type websocket from 'ws';
|
import type websocket from 'ws';
|
||||||
import type { ClientInfo } from '../browserContextFactory.js';
|
import type { ClientInfo } from '../browserContextFactory.js';
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import * as playwright from 'playwright';
|
import * as playwright from 'playwright';
|
||||||
import { startHttpServer } from '../httpServer.js';
|
import { startHttpServer } from '../utils/httpServer.js';
|
||||||
import { CDPRelayServer } from './cdpRelay.js';
|
import { CDPRelayServer } from './cdpRelay.js';
|
||||||
|
|
||||||
import type { BrowserContextFactory, ClientInfo } from '../browserContextFactory.js';
|
import type { BrowserContextFactory, ClientInfo } from '../browserContextFactory.js';
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 { ExtensionContextFactory } from './extensionContextFactory.js';
|
|
||||||
import { BrowserServerBackend } from '../browserServerBackend.js';
|
|
||||||
import { InProcessClientFactory } from '../inProcessClient.js';
|
|
||||||
import * as mcpTransport from '../mcp/transport.js';
|
|
||||||
|
|
||||||
import type { FullConfig } from '../config.js';
|
|
||||||
import type { ClientFactory } from '../mcp/proxyBackend.js';
|
|
||||||
|
|
||||||
export async function runWithExtension(config: FullConfig) {
|
|
||||||
const contextFactory = createExtensionContextFactory(config);
|
|
||||||
const serverBackendFactory = () => new BrowserServerBackend(config, contextFactory);
|
|
||||||
await mcpTransport.start(serverBackendFactory, config.server);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createExtensionClientFactory(config: FullConfig): ClientFactory {
|
|
||||||
return new InProcessClientFactory(createExtensionContextFactory(config), config);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function createExtensionContextFactory(config: FullConfig) {
|
|
||||||
return new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir);
|
|
||||||
}
|
|
||||||
@@ -15,18 +15,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
||||||
import { ListRootsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
||||||
import { BrowserContextFactory } from './browserContextFactory.js';
|
import { BrowserContextFactory } from './browserContextFactory.js';
|
||||||
import { BrowserServerBackend } from './browserServerBackend.js';
|
import { BrowserServerBackend } from './browserServerBackend.js';
|
||||||
import { InProcessTransport } from './mcp/inProcessTransport.js';
|
import { InProcessTransport } from './mcp/inProcessTransport.js';
|
||||||
import * as mcpServer from './mcp/server.js';
|
import * as mcpServer from './mcp/server.js';
|
||||||
|
|
||||||
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
||||||
import type { FullConfig } from './config.js';
|
import type { FullConfig } from './config.js';
|
||||||
import type { ClientFactory } from './mcp/proxyBackend.js';
|
import type { MCPFactory } from './mcp/proxyBackend.js';
|
||||||
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
|
|
||||||
export class InProcessClientFactory implements ClientFactory {
|
export class InProcessMCPFactory implements MCPFactory {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
@@ -40,21 +38,8 @@ export class InProcessClientFactory implements ClientFactory {
|
|||||||
this._config = config;
|
this._config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(server: Server): Promise<Client> {
|
async create(): Promise<Transport> {
|
||||||
const client = new Client(server.getClientVersion() ?? { name: 'unknown', version: 'unknown' });
|
|
||||||
const clientCapabilities = server.getClientCapabilities();
|
|
||||||
if (clientCapabilities)
|
|
||||||
client.registerCapabilities(clientCapabilities);
|
|
||||||
|
|
||||||
if (clientCapabilities?.roots) {
|
|
||||||
client.setRequestHandler(ListRootsRequestSchema, async () => {
|
|
||||||
return await server.listRoots();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const delegate = mcpServer.createServer(new BrowserServerBackend(this._config, this._contextFactory), false);
|
const delegate = mcpServer.createServer(new BrowserServerBackend(this._config, this._contextFactory), false);
|
||||||
await client.connect(new InProcessTransport(delegate));
|
return new InProcessTransport(delegate);
|
||||||
await client.ping();
|
|
||||||
return client;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,3 +2,5 @@
|
|||||||
../
|
../
|
||||||
../loop/
|
../loop/
|
||||||
../mcp/
|
../mcp/
|
||||||
|
../tools/
|
||||||
|
../utils/
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ import dotenv from 'dotenv';
|
|||||||
|
|
||||||
import * as mcpServer from '../mcp/server.js';
|
import * as mcpServer from '../mcp/server.js';
|
||||||
import * as mcpTransport from '../mcp/transport.js';
|
import * as mcpTransport from '../mcp/transport.js';
|
||||||
import { packageJSON } from '../package.js';
|
import { packageJSON } from '../utils/package.js';
|
||||||
import { Context } from './context.js';
|
import { Context } from './context.js';
|
||||||
import { perform } from './perform.js';
|
import { perform } from './perform.js';
|
||||||
import { snapshot } from './snapshot.js';
|
import { snapshot } from './snapshot.js';
|
||||||
|
import { toToolDefinition } from '../tools/tool.js';
|
||||||
|
|
||||||
import type { FullConfig } from '../config.js';
|
import type { FullConfig } from '../config.js';
|
||||||
import type { ServerBackend } from '../mcp/server.js';
|
import type { ServerBackend } from '../mcp/server.js';
|
||||||
@@ -48,13 +49,13 @@ class LoopToolsServerBackend implements ServerBackend {
|
|||||||
this._context = await Context.create(this._config);
|
this._context = await Context.create(this._config);
|
||||||
}
|
}
|
||||||
|
|
||||||
tools(): mcpServer.ToolSchema<any>[] {
|
tools(): mcpServer.ToolDefinition[] {
|
||||||
return this._tools.map(tool => tool.schema);
|
return this._tools.map(tool => toToolDefinition(tool.schema));
|
||||||
}
|
}
|
||||||
|
|
||||||
async callTool(schema: mcpServer.ToolSchema<any>, rawArguments: any): Promise<mcpServer.ToolResponse> {
|
async callTool(name: string, rawArguments: any): Promise<mcpServer.ToolResponse> {
|
||||||
const tool = this._tools.find(tool => tool.schema.name === schema.name)!;
|
const tool = this._tools.find(tool => tool.schema.name === name)!;
|
||||||
const parsedArguments = schema.inputSchema.parse(rawArguments || {});
|
const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
|
||||||
return await tool.handle(this._context!, parsedArguments);
|
return await tool.handle(this._context!, parsedArguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,11 @@
|
|||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
import type * as mcpServer from '../mcp/server.js';
|
import type * as mcpServer from '../mcp/server.js';
|
||||||
import type { Context } from './context.js';
|
import type { Context } from './context.js';
|
||||||
|
import type { ToolSchema } from '../tools/tool.js';
|
||||||
|
|
||||||
|
|
||||||
export type Tool<Input extends z.Schema = z.Schema> = {
|
export type Tool<Input extends z.Schema = z.Schema> = {
|
||||||
schema: mcpServer.ToolSchema<Input>;
|
schema: ToolSchema<Input>;
|
||||||
handle: (context: Context, params: z.output<Input>) => Promise<mcpServer.ToolResponse>;
|
handle: (context: Context, params: z.output<Input>) => Promise<mcpServer.ToolResponse>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
[*]
|
[*]
|
||||||
../log.js
|
../utils/
|
||||||
../manualPromise.js
|
|
||||||
../httpServer.js
|
|
||||||
|
|||||||
@@ -14,60 +14,63 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ServerBackend, ToolResponse, ToolSchema } from './server.js';
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||||
import { defineTool, Tool } from '../tools/tool.js';
|
|
||||||
import { packageJSON } from '../package.js';
|
|
||||||
import { logUnhandledError } from '../log.js';
|
|
||||||
|
|
||||||
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
|
import { ListRootsRequestSchema, PingRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { logUnhandledError } from '../utils/log.js';
|
||||||
|
import { packageJSON } from '../utils/package.js';
|
||||||
|
|
||||||
|
|
||||||
|
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import type { ToolDefinition, ServerBackend, ToolResponse } from './server.js';
|
||||||
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
|
|
||||||
type NonEmptyArray<T> = [T, ...T[]];
|
type NonEmptyArray<T> = [T, ...T[]];
|
||||||
|
|
||||||
export type ClientFactory = {
|
export type MCPFactory = {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
create(server: Server, options: any): Promise<Client>;
|
create(options: any): Promise<Transport>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ClientFactoryList = NonEmptyArray<ClientFactory>;
|
export type MCPFactoryList = NonEmptyArray<MCPFactory>;
|
||||||
|
|
||||||
export class ProxyBackend implements ServerBackend {
|
export class ProxyBackend implements ServerBackend {
|
||||||
name = 'Playwright MCP Client Switcher';
|
name = 'Playwright MCP Client Switcher';
|
||||||
version = packageJSON.version;
|
version = packageJSON.version;
|
||||||
|
|
||||||
private _clientFactories: ClientFactoryList;
|
private _mcpFactories: MCPFactoryList;
|
||||||
private _currentClient: Client | undefined;
|
private _currentClient: Client | undefined;
|
||||||
private _contextSwitchTool: Tool<any>;
|
private _contextSwitchTool: ToolDefinition;
|
||||||
private _tools: ToolSchema<any>[] = [];
|
private _tools: ToolDefinition[] = [];
|
||||||
private _server: Server | undefined;
|
private _server: Server | undefined;
|
||||||
|
|
||||||
constructor(clientFactories: ClientFactoryList) {
|
constructor(clientFactories: MCPFactoryList) {
|
||||||
this._clientFactories = clientFactories;
|
this._mcpFactories = clientFactories;
|
||||||
this._contextSwitchTool = this._defineContextSwitchTool();
|
this._contextSwitchTool = this._defineContextSwitchTool();
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(server: Server): Promise<void> {
|
async initialize(server: Server): Promise<void> {
|
||||||
this._server = server;
|
this._server = server;
|
||||||
await this._setCurrentClient(this._clientFactories[0], undefined);
|
await this._setCurrentClient(this._mcpFactories[0], undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
tools(): ToolSchema<any>[] {
|
tools(): ToolDefinition[] {
|
||||||
if (this._clientFactories.length === 1)
|
if (this._mcpFactories.length === 1)
|
||||||
return this._tools;
|
return this._tools;
|
||||||
return [
|
return [
|
||||||
...this._tools,
|
...this._tools,
|
||||||
this._contextSwitchTool.schema,
|
this._contextSwitchTool,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
async callTool(schema: ToolSchema<any>, rawArguments: any): Promise<ToolResponse> {
|
async callTool(name: string, rawArguments: any): Promise<ToolResponse> {
|
||||||
if (schema.name === this._contextSwitchTool.schema.name)
|
if (name === this._contextSwitchTool.name)
|
||||||
return this._callContextSwitchTool(rawArguments);
|
return this._callContextSwitchTool(rawArguments);
|
||||||
const result = await this._currentClient!.callTool({
|
const result = await this._currentClient!.callTool({
|
||||||
name: schema.name,
|
name,
|
||||||
arguments: rawArguments,
|
arguments: rawArguments,
|
||||||
});
|
});
|
||||||
return result as unknown as ToolResponse;
|
return result as unknown as ToolResponse;
|
||||||
@@ -79,7 +82,7 @@ export class ProxyBackend implements ServerBackend {
|
|||||||
|
|
||||||
private async _callContextSwitchTool(params: any): Promise<ToolResponse> {
|
private async _callContextSwitchTool(params: any): Promise<ToolResponse> {
|
||||||
try {
|
try {
|
||||||
const factory = this._clientFactories.find(factory => factory.name === params.name);
|
const factory = this._mcpFactories.find(factory => factory.name === params.name);
|
||||||
if (!factory)
|
if (!factory)
|
||||||
throw new Error('Unknown connection method: ' + params.name);
|
throw new Error('Unknown connection method: ' + params.name);
|
||||||
|
|
||||||
@@ -95,43 +98,52 @@ export class ProxyBackend implements ServerBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _defineContextSwitchTool(): Tool<any> {
|
private _defineContextSwitchTool(): ToolDefinition {
|
||||||
return defineTool({
|
return {
|
||||||
capability: 'core',
|
name: 'browser_connect',
|
||||||
|
description: [
|
||||||
schema: {
|
'Connect to a browser using one of the available methods:',
|
||||||
name: 'browser_connect',
|
...this._mcpFactories.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: zodToJsonSchema(z.object({
|
||||||
|
name: z.enum(this._mcpFactories.map(factory => factory.name) as [string, ...string[]]).default(this._mcpFactories[0].name).describe('The method to use to connect to the browser'),
|
||||||
|
}), { strictUnions: true }) as ToolDefinition['inputSchema'],
|
||||||
|
annotations: {
|
||||||
title: 'Connect to a browser context',
|
title: 'Connect to a browser context',
|
||||||
description: [
|
readOnlyHint: true,
|
||||||
'Connect to a browser using one of the available methods:',
|
openWorldHint: false,
|
||||||
'',
|
|
||||||
...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',
|
|
||||||
},
|
},
|
||||||
|
};
|
||||||
async handle() {
|
|
||||||
throw new Error('Unreachable');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _setCurrentClient(factory: ClientFactory, options: any) {
|
private async _setCurrentClient(factory: MCPFactory, options: any) {
|
||||||
await this._currentClient?.close();
|
await this._currentClient?.close();
|
||||||
this._currentClient = await factory.create(this._server!, options);
|
this._currentClient = undefined;
|
||||||
|
|
||||||
|
const client = new Client({ name: 'Playwright MCP Proxy', version: packageJSON.version });
|
||||||
|
client.registerCapabilities({
|
||||||
|
roots: {
|
||||||
|
listRoots: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
client.setRequestHandler(ListRootsRequestSchema, async () => {
|
||||||
|
const clientName = this._server!.getClientVersion()?.name;
|
||||||
|
if (this._server!.getClientCapabilities()?.roots && (
|
||||||
|
clientName === 'Visual Studio Code' ||
|
||||||
|
clientName === 'Visual Studio Code - Insiders')) {
|
||||||
|
const { roots } = await this._server!.listRoots();
|
||||||
|
return { roots };
|
||||||
|
}
|
||||||
|
return { roots: [] };
|
||||||
|
});
|
||||||
|
client.setRequestHandler(PingRequestSchema, () => ({}));
|
||||||
|
|
||||||
|
const transport = await factory.create(options);
|
||||||
|
await client.connect(transport);
|
||||||
|
|
||||||
|
this._currentClient = client;
|
||||||
const tools = await this._currentClient.listTools();
|
const tools = await this._currentClient.listTools();
|
||||||
this._tools = tools.tools.map(tool => ({
|
this._tools = tools.tools;
|
||||||
name: tool.name,
|
|
||||||
title: tool.title ?? '',
|
|
||||||
description: tool.description ?? '',
|
|
||||||
inputSchema: tool.inputSchema ?? z.object({}),
|
|
||||||
type: tool.annotations?.readOnlyHint ? 'readOnly' as const : 'destructive' as const,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,17 +14,18 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from 'zod';
|
import debug from 'debug';
|
||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
import { ManualPromise } from '../utils/manualPromise.js';
|
||||||
import { ManualPromise } from '../manualPromise.js';
|
import { logUnhandledError } from '../utils/log.js';
|
||||||
import { logUnhandledError } from '../log.js';
|
|
||||||
|
|
||||||
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js';
|
import type { ImageContent, TextContent, Tool } from '@modelcontextprotocol/sdk/types.js';
|
||||||
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
export type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
export type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
|
||||||
|
const serverDebug = debug('pw:mcp:server');
|
||||||
|
|
||||||
export type ClientCapabilities = {
|
export type ClientCapabilities = {
|
||||||
roots?: {
|
roots?: {
|
||||||
listRoots?: boolean
|
listRoots?: boolean
|
||||||
@@ -36,22 +37,14 @@ export type ToolResponse = {
|
|||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ToolSchema<Input extends z.Schema> = {
|
export type ToolDefinition = Tool;
|
||||||
name: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
inputSchema: Input;
|
|
||||||
type: 'readOnly' | 'destructive';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ToolHandler = (toolName: string, params: any) => Promise<ToolResponse>;
|
|
||||||
|
|
||||||
export interface ServerBackend {
|
export interface ServerBackend {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
initialize?(server: Server): Promise<void>;
|
initialize?(server: Server): Promise<void>;
|
||||||
tools(): ToolSchema<any>[];
|
tools(): ToolDefinition[];
|
||||||
callTool(schema: ToolSchema<any>, rawArguments: any): Promise<ToolResponse>;
|
callTool(name: string, rawArguments: any): Promise<ToolResponse>;
|
||||||
serverClosed?(): void;
|
serverClosed?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,22 +65,14 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
serverDebug('listTools');
|
||||||
const tools = backend.tools();
|
const tools = backend.tools();
|
||||||
return { tools: tools.map(tool => ({
|
return { tools };
|
||||||
name: tool.name,
|
|
||||||
description: tool.description,
|
|
||||||
inputSchema: tool.inputSchema instanceof z.ZodType ? zodToJsonSchema(tool.inputSchema) : tool.inputSchema,
|
|
||||||
annotations: {
|
|
||||||
title: tool.title,
|
|
||||||
readOnlyHint: tool.type === 'readOnly',
|
|
||||||
destructiveHint: tool.type === 'destructive',
|
|
||||||
openWorldHint: true,
|
|
||||||
},
|
|
||||||
})) };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let heartbeatRunning = false;
|
let heartbeatRunning = false;
|
||||||
server.setRequestHandler(CallToolRequestSchema, async request => {
|
server.setRequestHandler(CallToolRequestSchema, async request => {
|
||||||
|
serverDebug('callTool', request);
|
||||||
await initializedPromise;
|
await initializedPromise;
|
||||||
|
|
||||||
if (runHeartbeat && !heartbeatRunning) {
|
if (runHeartbeat && !heartbeatRunning) {
|
||||||
@@ -100,12 +85,12 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser
|
|||||||
isError: true,
|
isError: true,
|
||||||
});
|
});
|
||||||
const tools = backend.tools();
|
const tools = backend.tools();
|
||||||
const tool = tools.find(tool => tool.name === request.params.name) as ToolSchema<any>;
|
const tool = tools.find(tool => tool.name === request.params.name);
|
||||||
if (!tool)
|
if (!tool)
|
||||||
return errorResult(`Error: Tool "${request.params.name}" not found`);
|
return errorResult(`Error: Tool "${request.params.name}" not found`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await backend.callTool(tool, request.params.arguments || {});
|
return await backend.callTool(tool.name, request.params.arguments || {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResult(String(error));
|
return errorResult(String(error));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import debug from 'debug';
|
|||||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
import { httpAddressToString, startHttpServer } from '../httpServer.js';
|
import { httpAddressToString, startHttpServer } from '../utils/httpServer.js';
|
||||||
import * as mcpServer from './server.js';
|
import * as mcpServer from './server.js';
|
||||||
|
|
||||||
import type { ServerBackendFactory } from './server.js';
|
import type { ServerBackendFactory } from './server.js';
|
||||||
|
|||||||
@@ -15,23 +15,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { program, Option } from 'commander';
|
import { program, Option } from 'commander';
|
||||||
// @ts-ignore
|
|
||||||
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 './utils/package.js';
|
||||||
import { createVSCodeClientFactory } from './vscode/host.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';
|
||||||
import { runLoopTools } from './loopTools/main.js';
|
import { runLoopTools } from './loopTools/main.js';
|
||||||
import { ProxyBackend } from './mcp/proxyBackend.js';
|
import { ProxyBackend } from './mcp/proxyBackend.js';
|
||||||
import { InProcessClientFactory } from './inProcessClient.js';
|
import { InProcessMCPFactory } from './inProcessMcpFactrory.js';
|
||||||
import { BrowserServerBackend } from './browserServerBackend.js';
|
import { BrowserServerBackend } from './browserServerBackend.js';
|
||||||
|
import { ExtensionContextFactory } from './extension/extensionContextFactory.js';
|
||||||
|
|
||||||
import type { ClientFactoryList } from './mcp/proxyBackend.js';
|
import type { MCPFactoryList } from './mcp/proxyBackend.js';
|
||||||
import type { ServerBackendFactory } from './mcp/server.js';
|
import type { FullConfig } from './config.js';
|
||||||
|
import { VSCodeMCPFactory } from './vscode/host.js';
|
||||||
|
|
||||||
program
|
program
|
||||||
.version('Version ' + packageJSON.version)
|
.version('Version ' + packageJSON.version)
|
||||||
@@ -76,7 +74,9 @@ program
|
|||||||
const config = await resolveCLIConfig(options);
|
const config = await resolveCLIConfig(options);
|
||||||
|
|
||||||
if (options.extension) {
|
if (options.extension) {
|
||||||
await runWithExtension(config);
|
const contextFactory = createExtensionContextFactory(config);
|
||||||
|
const serverBackendFactory = () => new BrowserServerBackend(config, contextFactory);
|
||||||
|
await mcpTransport.start(serverBackendFactory, config.server);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (options.loopTools) {
|
if (options.loopTools) {
|
||||||
@@ -84,28 +84,17 @@ program
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let serverBackendFactory: ServerBackendFactory;
|
|
||||||
const browserContextFactory = contextFactory(config);
|
const browserContextFactory = contextFactory(config);
|
||||||
|
const factories: MCPFactoryList = [
|
||||||
|
new InProcessMCPFactory(browserContextFactory, config),
|
||||||
|
];
|
||||||
if (options.connectTool) {
|
if (options.connectTool) {
|
||||||
const factories: ClientFactoryList = [
|
factories.push(
|
||||||
new InProcessClientFactory(browserContextFactory, config),
|
new InProcessMCPFactory(createExtensionContextFactory(config), config),
|
||||||
createExtensionClientFactory(config),
|
new VSCodeMCPFactory(config),
|
||||||
// TODO: enable vscode client factory without --connect-tool, just based on client name
|
);
|
||||||
createVSCodeClientFactory(config),
|
|
||||||
];
|
|
||||||
serverBackendFactory = () => new ProxyBackend(factories);
|
|
||||||
} else {
|
|
||||||
serverBackendFactory = () => new BrowserServerBackend(config, browserContextFactory);
|
|
||||||
}
|
|
||||||
await mcpTransport.start(serverBackendFactory, config.server);
|
|
||||||
|
|
||||||
if (config.saveTrace) {
|
|
||||||
const server = await startTraceViewerServer();
|
|
||||||
const urlPrefix = server.urlPrefix('human-readable');
|
|
||||||
const url = urlPrefix + '/trace/index.html?trace=' + config.browser.launchOptions.tracesDir + '/trace.json';
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('\nTrace viewer listening on ' + url);
|
|
||||||
}
|
}
|
||||||
|
await mcpTransport.start(() => new ProxyBackend(factories), config.server);
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupExitWatchdog() {
|
function setupExitWatchdog() {
|
||||||
@@ -124,4 +113,8 @@ function setupExitWatchdog() {
|
|||||||
process.on('SIGTERM', handleExit);
|
process.on('SIGTERM', handleExit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createExtensionContextFactory(config: FullConfig) {
|
||||||
|
return new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir);
|
||||||
|
}
|
||||||
|
|
||||||
void program.parseAsync(process.argv);
|
void program.parseAsync(process.argv);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { Response } from './response.js';
|
import { Response } from './response.js';
|
||||||
import { logUnhandledError } from './log.js';
|
import { logUnhandledError } from './utils/log.js';
|
||||||
import { outputFile } from './config.js';
|
import { outputFile } from './config.js';
|
||||||
|
|
||||||
import type { FullConfig } from './config.js';
|
import type { FullConfig } from './config.js';
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import * as playwright from 'playwright';
|
import * as playwright from 'playwright';
|
||||||
import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
|
import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
|
||||||
import { logUnhandledError } from './log.js';
|
import { logUnhandledError } from './utils/log.js';
|
||||||
import { ManualPromise } from './manualPromise.js';
|
import { ManualPromise } from './utils/manualPromise.js';
|
||||||
import { ModalState } from './tools/tool.js';
|
import { ModalState } from './tools/tool.js';
|
||||||
|
|
||||||
import type { Context } from './context.js';
|
import type { Context } from './context.js';
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
[*]
|
[*]
|
||||||
../javascript.js
|
../utils/
|
||||||
../log.js
|
|
||||||
../manualPromise.js
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { defineTabTool } from './tool.js';
|
import { defineTabTool } from './tool.js';
|
||||||
import * as javascript from '../javascript.js';
|
import * as javascript from '../utils/codegen.js';
|
||||||
import { generateLocator } from './utils.js';
|
import { generateLocator } from './utils.js';
|
||||||
|
|
||||||
import type * as playwright from 'playwright';
|
import type * as playwright from 'playwright';
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { z } from 'zod';
|
|||||||
import { defineTabTool } from './tool.js';
|
import { defineTabTool } from './tool.js';
|
||||||
import { elementSchema } from './snapshot.js';
|
import { elementSchema } from './snapshot.js';
|
||||||
import { generateLocator } from './utils.js';
|
import { generateLocator } from './utils.js';
|
||||||
import * as javascript from '../javascript.js';
|
import * as javascript from '../utils/codegen.js';
|
||||||
|
|
||||||
const pressKey = defineTabTool({
|
const pressKey = defineTabTool({
|
||||||
capability: 'core',
|
capability: 'core',
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineTabTool } from './tool.js';
|
import { defineTabTool } from './tool.js';
|
||||||
|
|
||||||
import * as javascript from '../javascript.js';
|
import * as javascript from '../utils/codegen.js';
|
||||||
|
|
||||||
const pdfSchema = z.object({
|
const pdfSchema = z.object({
|
||||||
filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
|
filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { defineTabTool } from './tool.js';
|
import { defineTabTool } from './tool.js';
|
||||||
import * as javascript from '../javascript.js';
|
import * as javascript from '../utils/codegen.js';
|
||||||
import { generateLocator } from './utils.js';
|
import { generateLocator } from './utils.js';
|
||||||
|
|
||||||
import type * as playwright from 'playwright';
|
import type * as playwright from 'playwright';
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { defineTabTool, defineTool } from './tool.js';
|
import { defineTabTool, defineTool } from './tool.js';
|
||||||
import * as javascript from '../javascript.js';
|
import * as javascript from '../utils/codegen.js';
|
||||||
import { generateLocator } from './utils.js';
|
import { generateLocator } from './utils.js';
|
||||||
|
|
||||||
const snapshot = defineTool({
|
const snapshot = defineTool({
|
||||||
|
|||||||
@@ -14,13 +14,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||||
|
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
import type { Context } from '../context.js';
|
import type { Context } from '../context.js';
|
||||||
import type * as playwright from 'playwright';
|
import type * as playwright from 'playwright';
|
||||||
import type { ToolCapability } from '../../config.js';
|
import type { ToolCapability } from '../../config.js';
|
||||||
import type { Tab } from '../tab.js';
|
import type { Tab } from '../tab.js';
|
||||||
import type { Response } from '../response.js';
|
import type { Response } from '../response.js';
|
||||||
import type { ToolSchema } from '../mcp/server.js';
|
import type { ToolDefinition } from '../mcp/server.js';
|
||||||
|
|
||||||
export type FileUploadModalState = {
|
export type FileUploadModalState = {
|
||||||
type: 'fileChooser';
|
type: 'fileChooser';
|
||||||
@@ -36,6 +38,28 @@ export type DialogModalState = {
|
|||||||
|
|
||||||
export type ModalState = FileUploadModalState | DialogModalState;
|
export type ModalState = FileUploadModalState | DialogModalState;
|
||||||
|
|
||||||
|
export type ToolSchema<Input extends z.Schema> = {
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: Input;
|
||||||
|
type: 'readOnly' | 'destructive';
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toToolDefinition(tool: ToolSchema<any>): ToolDefinition {
|
||||||
|
return {
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as ToolDefinition['inputSchema'],
|
||||||
|
annotations: {
|
||||||
|
title: tool.title,
|
||||||
|
readOnlyHint: tool.type === 'readOnly',
|
||||||
|
destructiveHint: tool.type === 'destructive',
|
||||||
|
openWorldHint: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Tool<Input extends z.Schema = z.Schema> = {
|
export type Tool<Input extends z.Schema = z.Schema> = {
|
||||||
capability: ToolCapability;
|
capability: ToolCapability;
|
||||||
schema: ToolSchema<Input>;
|
schema: ToolSchema<Input>;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function escapeWithQuotes(text: string, char: string = '\'') {
|
|||||||
if (char === '"')
|
if (char === '"')
|
||||||
return char + escapedText.replace(/["]/g, '\\"') + char;
|
return char + escapedText.replace(/["]/g, '\\"') + char;
|
||||||
if (char === '`')
|
if (char === '`')
|
||||||
return char + escapedText.replace(/[`]/g, '`') + char;
|
return char + escapedText.replace(/[`]/g, '\\`') + char;
|
||||||
throw new Error('Invalid escape char');
|
throw new Error('Invalid escape char');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import type { FullConfig } from './config.js';
|
import type { FullConfig } from '../config.js';
|
||||||
|
|
||||||
export function cacheDir() {
|
export function cacheDir() {
|
||||||
let cacheDirectory: string;
|
let cacheDirectory: string;
|
||||||
@@ -35,3 +35,11 @@ export function cacheDir() {
|
|||||||
export async function userDataDir(browserConfig: FullConfig['browser']) {
|
export async function userDataDir(browserConfig: FullConfig['browser']) {
|
||||||
return path.join(cacheDir(), 'ms-playwright', `mcp-${browserConfig.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
|
return path.join(cacheDir(), 'ms-playwright', `mcp-${browserConfig.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sanitizeForFilePath(s: string) {
|
||||||
|
const sanitize = (s: string) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
|
||||||
|
const separator = s.lastIndexOf('.');
|
||||||
|
if (separator === -1)
|
||||||
|
return sanitize(s);
|
||||||
|
return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1));
|
||||||
|
}
|
||||||
@@ -19,11 +19,3 @@ import crypto from 'crypto';
|
|||||||
export function createHash(data: string): string {
|
export function createHash(data: string): string {
|
||||||
return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7);
|
return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeForFilePath(s: string) {
|
|
||||||
const sanitize = (s: string) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
|
|
||||||
const separator = s.lastIndexOf('.');
|
|
||||||
if (separator === -1)
|
|
||||||
return sanitize(s);
|
|
||||||
return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1));
|
|
||||||
}
|
|
||||||
@@ -19,4 +19,4 @@ import path from 'path';
|
|||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
|
||||||
const __filename = url.fileURLToPath(import.meta.url);
|
const __filename = url.fileURLToPath(import.meta.url);
|
||||||
export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', 'package.json'), 'utf8'));
|
export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', '..', 'package.json'), 'utf8'));
|
||||||
@@ -13,26 +13,24 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
||||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||||
|
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
import { FullConfig } from '../config.js';
|
import { FullConfig } from '../config.js';
|
||||||
import { ClientFactory } from '../mcp/proxyBackend.js';
|
import { MCPFactory } from '../mcp/proxyBackend.js';
|
||||||
import { Server } from '../mcp/server.js';
|
|
||||||
|
|
||||||
class VSCodeClientFactory implements ClientFactory {
|
export class VSCodeMCPFactory implements MCPFactory {
|
||||||
name = 'vscode';
|
name = 'vscode';
|
||||||
description = 'Connect to a browser running in the Playwright VS Code extension';
|
description = 'Connect to a browser running in the Playwright VS Code extension';
|
||||||
|
|
||||||
constructor(private readonly _config: FullConfig) {}
|
constructor(private readonly _config: FullConfig) {}
|
||||||
|
|
||||||
async create(server: Server, options: any): Promise<Client> {
|
async create(options: any): Promise<Transport> {
|
||||||
if (typeof options.connectionString !== 'string')
|
if (typeof options.connectionString !== 'string')
|
||||||
throw new Error('Missing options.connectionString');
|
throw new Error('Missing options.connectionString');
|
||||||
if (typeof options.lib !== 'string')
|
if (typeof options.lib !== 'string')
|
||||||
throw new Error('Missing options.library');
|
throw new Error('Missing options.library');
|
||||||
|
|
||||||
const client = new Client(server.getClientVersion()!);
|
return new StdioClientTransport({
|
||||||
await client.connect(new StdioClientTransport({
|
|
||||||
command: process.execPath,
|
command: process.execPath,
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
args: [
|
args: [
|
||||||
@@ -41,12 +39,6 @@ class VSCodeClientFactory implements ClientFactory {
|
|||||||
options.connectionString,
|
options.connectionString,
|
||||||
options.lib,
|
options.lib,
|
||||||
],
|
],
|
||||||
}));
|
});
|
||||||
await client.ping();
|
|
||||||
return client;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createVSCodeClientFactory(config: FullConfig): ClientFactory {
|
|
||||||
return new VSCodeClientFactory(config);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import path from 'path';
|
|||||||
import { pathToFileURL } from 'url';
|
import { pathToFileURL } from 'url';
|
||||||
|
|
||||||
import { test, expect } from './fixtures.js';
|
import { test, expect } from './fixtures.js';
|
||||||
import { createHash } from '../src/utils.js';
|
import { createHash } from '../src/utils/guid.js';
|
||||||
|
|
||||||
const p = process.platform === 'win32' ? 'c:\\non\\existent\\folder' : '/non/existent/folder';
|
const p = process.platform === 'win32' ? 'c:\\non\\existent\\folder' : '/non/existent/folder';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user