Skip to main content
Glama

MCP Accessibility Scanner

browserContextFactory.ts9.93 kB
/** * 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 fs from 'fs'; import net from 'net'; import path from 'path'; import * as playwright from 'playwright'; // @ts-ignore import { registryDirectory } from 'playwright-core/lib/server/registry/index'; // @ts-ignore 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 type { FullConfig } from './config.js'; export function contextFactory(config: FullConfig): BrowserContextFactory { if (config.browser.remoteEndpoint) return new RemoteContextFactory(config); if (config.browser.cdpEndpoint) return new CdpContextFactory(config); if (config.browser.isolated) return new IsolatedContextFactory(config); return new PersistentContextFactory(config); } export type ClientInfo = { name?: string, version?: string, rootPath?: string }; export interface BrowserContextFactory { createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }>; } class BaseContextFactory implements BrowserContextFactory { readonly config: FullConfig; private _logName: string; protected _browserPromise: Promise<playwright.Browser> | undefined; constructor(name: string, config: FullConfig) { this._logName = name; this.config = config; } protected async _obtainBrowser(clientInfo: ClientInfo): Promise<playwright.Browser> { if (this._browserPromise) return this._browserPromise; testDebug(`obtain browser (${this._logName})`); this._browserPromise = this._doObtainBrowser(clientInfo); void this._browserPromise.then(browser => { browser.on('disconnected', () => { this._browserPromise = undefined; }); }).catch(() => { this._browserPromise = undefined; }); return this._browserPromise; } protected async _doObtainBrowser(clientInfo: ClientInfo): Promise<playwright.Browser> { throw new Error('Not implemented'); } async createContext(clientInfo: ClientInfo): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> { testDebug(`create browser context (${this._logName})`); const browser = await this._obtainBrowser(clientInfo); const browserContext = await this._doCreateContext(browser); return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) }; } protected async _doCreateContext(browser: playwright.Browser): Promise<playwright.BrowserContext> { throw new Error('Not implemented'); } private async _closeBrowserContext(browserContext: playwright.BrowserContext, browser: playwright.Browser) { testDebug(`close browser context (${this._logName})`); if (browser.contexts().length === 1) this._browserPromise = undefined; await browserContext.close().catch(logUnhandledError); if (browser.contexts().length === 0) { testDebug(`close browser (${this._logName})`); await browser.close().catch(logUnhandledError); } } } class IsolatedContextFactory extends BaseContextFactory { constructor(config: FullConfig) { super('isolated', config); } protected override async _doObtainBrowser(clientInfo: ClientInfo): Promise<playwright.Browser> { await injectCdpPort(this.config.browser); const browserType = playwright[this.config.browser.browserName]; return browserType.launch({ tracesDir: await startTraceServer(this.config, clientInfo.rootPath), ...this.config.browser.launchOptions, handleSIGINT: false, handleSIGTERM: false, }).catch(error => { if (error.message.includes('Executable doesn\'t exist')) throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`); throw error; }); } protected override async _doCreateContext(browser: playwright.Browser): Promise<playwright.BrowserContext> { return browser.newContext(this.config.browser.contextOptions); } } class CdpContextFactory extends BaseContextFactory { constructor(config: FullConfig) { super('cdp', config); } protected override async _doObtainBrowser(): Promise<playwright.Browser> { return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint!); } protected override async _doCreateContext(browser: playwright.Browser): Promise<playwright.BrowserContext> { return this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0]; } } class RemoteContextFactory extends BaseContextFactory { constructor(config: FullConfig) { super('remote', config); } protected override async _doObtainBrowser(): Promise<playwright.Browser> { const url = new URL(this.config.browser.remoteEndpoint!); url.searchParams.set('browser', this.config.browser.browserName); if (this.config.browser.launchOptions) url.searchParams.set('launch-options', JSON.stringify(this.config.browser.launchOptions)); return playwright[this.config.browser.browserName].connect(String(url)); } protected override async _doCreateContext(browser: playwright.Browser): Promise<playwright.BrowserContext> { return browser.newContext(); } } class PersistentContextFactory implements BrowserContextFactory { readonly config: FullConfig; readonly name = 'persistent'; readonly description = 'Create a new persistent browser context'; private _userDataDirs = new Set<string>(); constructor(config: FullConfig) { this.config = config; } async createContext(clientInfo: ClientInfo): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> { await injectCdpPort(this.config.browser); testDebug('create browser context (persistent)'); const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo.rootPath); const tracesDir = await startTraceServer(this.config, clientInfo.rootPath); this._userDataDirs.add(userDataDir); testDebug('lock user data dir', userDataDir); const browserType = playwright[this.config.browser.browserName]; for (let i = 0; i < 5; i++) { try { const browserContext = await browserType.launchPersistentContext(userDataDir, { tracesDir, ...this.config.browser.launchOptions, ...this.config.browser.contextOptions, handleSIGINT: false, handleSIGTERM: false, }); const close = () => this._closeBrowserContext(browserContext, userDataDir); return { browserContext, close }; } catch (error: any) { if (error.message.includes('Executable doesn\'t exist')) throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`); if (error.message.includes('ProcessSingleton') || error.message.includes('Invalid URL')) { // User data directory is already in use, try again. await new Promise(resolve => setTimeout(resolve, 1000)); continue; } throw error; } } throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`); } private async _closeBrowserContext(browserContext: playwright.BrowserContext, userDataDir: string) { testDebug('close browser context (persistent)'); testDebug('release user data dir', userDataDir); await browserContext.close().catch(() => {}); this._userDataDirs.delete(userDataDir); testDebug('close browser context complete (persistent)'); } private async _createUserDataDir(rootPath: string | undefined) { const dir = process.env.PWMCP_PROFILES_DIR_FOR_TEST ?? registryDirectory; const browserToken = this.config.browser.launchOptions?.channel ?? this.config.browser?.browserName; // Hesitant putting hundreds of files into the user's workspace, so using it for hashing instead. const rootPathToken = rootPath ? `-${createHash(rootPath)}` : ''; const result = path.join(dir, `mcp-${browserToken}${rootPathToken}`); await fs.promises.mkdir(result, { recursive: true }); return result; } } async function injectCdpPort(browserConfig: FullConfig['browser']) { if (browserConfig.browserName === 'chromium') (browserConfig.launchOptions as any).cdpPort = await findFreePort(); } async function findFreePort(): Promise<number> { return new Promise((resolve, reject) => { const server = net.createServer(); server.listen(0, () => { const { port } = server.address() as net.AddressInfo; server.close(() => resolve(port)); }); 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; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JustasMonkev/mcp-accessibility-scanner'

If you have feedback or need assistance with the MCP directory API, please join our Discord server