chore: run test server per context (#874)
Fixes https://github.com/microsoft/playwright-mcp/issues/869
This commit is contained in:
@@ -21,6 +21,8 @@ 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';
|
||||||
|
// @ts-ignore
|
||||||
|
import { startTraceViewerServer } from 'playwright-core/lib/server';
|
||||||
import { logUnhandledError, testDebug } from './log.js';
|
import { logUnhandledError, testDebug } from './log.js';
|
||||||
import { createHash } from './utils.js';
|
import { createHash } from './utils.js';
|
||||||
import { outputFile } from './config.js';
|
import { outputFile } 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
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 { ManualPromise } from '../manualPromise.js';
|
import { ManualPromise } from '../manualPromise.js';
|
||||||
@@ -23,6 +24,8 @@ import type { ImageContent, TextContent, Tool } from '@modelcontextprotocol/sdk/
|
|||||||
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
|
||||||
@@ -62,12 +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 };
|
return { tools };
|
||||||
});
|
});
|
||||||
|
|
||||||
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) {
|
||||||
|
|||||||
@@ -15,8 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
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';
|
||||||
@@ -95,14 +93,6 @@ program
|
|||||||
serverBackendFactory = () => new BrowserServerBackend(config, browserContextFactory);
|
serverBackendFactory = () => new BrowserServerBackend(config, browserContextFactory);
|
||||||
}
|
}
|
||||||
await mcpTransport.start(serverBackendFactory, config.server);
|
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupExitWatchdog() {
|
function setupExitWatchdog() {
|
||||||
|
|||||||
Reference in New Issue
Block a user