/** * 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 debug from 'debug'; import * as playwright from 'playwright'; import { Tab } from './tab.js'; import type { Tool } from './tools/tool.js'; import type { FullConfig } from './config.js'; import type { BrowserContextFactory } from './browserContextFactory.js'; const testDebug = debug('pw:mcp:test'); export class Context { readonly tools: Tool[]; readonly config: FullConfig; private _browserContextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> | undefined; private _browserContextFactory: BrowserContextFactory; private _tabs: Tab[] = []; private _currentTab: Tab | undefined; clientVersion: { name: string; version: string; } | undefined; constructor(tools: Tool[], config: FullConfig, browserContextFactory: BrowserContextFactory) { this.tools = tools; this.config = config; this._browserContextFactory = browserContextFactory; testDebug('create context'); } tabs(): Tab[] { return this._tabs; } currentTabOrDie(): Tab { if (!this._currentTab) throw new Error('No open pages available. Use the "browser_navigate" tool to navigate to a page first.'); return this._currentTab; } async newTab(): Promise { const { browserContext } = await this._ensureBrowserContext(); const page = await browserContext.newPage(); this._currentTab = this._tabs.find(t => t.page === page)!; return this._currentTab; } async selectTab(index: number) { this._currentTab = this._tabs[index]; await this._currentTab.page.bringToFront(); } async ensureTab(): Promise { const { browserContext } = await this._ensureBrowserContext(); if (!this._currentTab) await browserContext.newPage(); return this._currentTab!; } async listTabsMarkdown(): Promise { if (!this._tabs.length) return ['### No tabs open']; const lines: string[] = ['### Open tabs']; for (let i = 0; i < this._tabs.length; i++) { const tab = this._tabs[i]; const title = await tab.title(); const url = tab.page.url(); const current = tab === this._currentTab ? ' (current)' : ''; lines.push(`- ${i}:${current} [${title}] (${url})`); } return lines; } async closeTab(index: number | undefined) { const tab = index === undefined ? this._currentTab : this._tabs[index]; await tab?.page.close(); return await this.listTabsMarkdown(); } async run(tool: Tool, params: Record | undefined) { // Tab management is done outside of the action() call. const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {})); const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult; if (resultOverride) return resultOverride; const tab = this.currentTabOrDie(); const { actionResult, snapshot } = await tab.run(action || (() => Promise.resolve()), { waitForNetwork, captureSnapshot }); const result: string[] = []; result.push(`### Ran Playwright code \`\`\`js ${code.join('\n')} \`\`\``); if (tab.modalStates().length) { result.push('', ...tab.modalStatesMarkdown()); return { content: [{ type: 'text', text: result.join('\n'), }], }; } result.push(...tab.takeRecentConsoleMarkdown()); result.push(...tab.listDownloadsMarkdown()); if (snapshot) { if (this.tabs().length > 1) result.push('', ...(await this.listTabsMarkdown())); result.push('', snapshot); } const content = actionResult?.content ?? []; return { content: [ ...content, { type: 'text', text: result.join('\n'), } ], }; } private _onPageCreated(page: playwright.Page) { const tab = new Tab(this, page, tab => this._onPageClosed(tab)); this._tabs.push(tab); if (!this._currentTab) this._currentTab = tab; } private _onPageClosed(tab: Tab) { const index = this._tabs.indexOf(tab); if (index === -1) return; this._tabs.splice(index, 1); if (this._currentTab === tab) this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)]; if (!this._tabs.length) void this.close(); } async close() { if (!this._browserContextPromise) return; testDebug('close context'); const promise = this._browserContextPromise; this._browserContextPromise = undefined; await promise.then(async ({ browserContext, close }) => { if (this.config.saveTrace) await browserContext.tracing.stop(); await close(); }); } private async _setupRequestInterception(context: playwright.BrowserContext) { if (this.config.network?.allowedOrigins?.length) { await context.route('**', route => route.abort('blockedbyclient')); for (const origin of this.config.network.allowedOrigins) await context.route(`*://${origin}/**`, route => route.continue()); } if (this.config.network?.blockedOrigins?.length) { for (const origin of this.config.network.blockedOrigins) await context.route(`*://${origin}/**`, route => route.abort('blockedbyclient')); } } private _ensureBrowserContext() { if (!this._browserContextPromise) { this._browserContextPromise = this._setupBrowserContext(); this._browserContextPromise.catch(() => { this._browserContextPromise = undefined; }); } return this._browserContextPromise; } private async _setupBrowserContext(): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { // TODO: move to the browser context factory to make it based on isolation mode. const result = await this._browserContextFactory.createContext(this.clientVersion!); const { browserContext } = result; await this._setupRequestInterception(browserContext); for (const page of browserContext.pages()) this._onPageCreated(page); browserContext.on('page', page => this._onPageCreated(page)); if (this.config.saveTrace) { await browserContext.tracing.start({ name: 'trace', screenshots: false, snapshots: true, sources: false, }); } return result; } }