Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function contextFactory(config: FullConfig): BrowserContextFactory {
export type BrowserContextFactoryResult = {
browserContext: playwright.BrowserContext;
close: (afterClose: () => Promise<void>) => Promise<void>;
closeCurrentTabBeforeClose?: boolean;
};

export interface BrowserContextFactory {
Expand Down
6 changes: 5 additions & 1 deletion packages/playwright/src/mcp/browser/browserServerBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,21 @@ import type { Tool } from './tools/tool';
import type { BrowserContextFactory } from './browserContextFactory';
import type * as mcpServer from '../sdk/server';
import type { ServerBackend } from '../sdk/server';
import type * as playwright from 'playwright';

export class BrowserServerBackend implements ServerBackend {
private _tools: Tool[];
private _context: Context | undefined;
private _sessionLog: SessionLog | undefined;
private _config: FullConfig;
private _browserContextFactory: BrowserContextFactory;
private _closePageOverride?: (page: playwright.Page) => Promise<void>;

constructor(config: FullConfig, factory: BrowserContextFactory) {
constructor(config: FullConfig, factory: BrowserContextFactory, closePageOverride?: (page: playwright.Page) => Promise<void>) {
this._config = config;
this._browserContextFactory = factory;
this._tools = filteredTools(config);
this._closePageOverride = closePageOverride;
}

async initialize(server: mcpServer.Server, clientInfo: mcpServer.ClientInfo): Promise<void> {
Expand All @@ -47,6 +50,7 @@ export class BrowserServerBackend implements ServerBackend {
browserContextFactory: this._browserContextFactory,
sessionLog: this._sessionLog,
clientInfo,
closePageOverride: this._closePageOverride,
});
}

Expand Down
10 changes: 8 additions & 2 deletions packages/playwright/src/mcp/browser/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type ContextOptions = {
browserContextFactory: BrowserContextFactory;
sessionLog: SessionLog | undefined;
clientInfo: ClientInfo;
closePageOverride?: (page: playwright.Page) => Promise<void>;
};

export class Context {
Expand Down Expand Up @@ -113,7 +114,10 @@ export class Context {
if (!tab)
throw new Error(`Tab ${index} not found`);
const url = tab.page.url();
await tab.page.close();
if (this.options.closePageOverride)
await this.options.closePageOverride(tab.page);
else
await tab.page.close();
return url;
}

Expand Down Expand Up @@ -164,10 +168,12 @@ export class Context {
const promise = this._browserContextPromise;
this._browserContextPromise = undefined;

await promise.then(async ({ browserContext, close }) => {
await promise.then(async ({ browserContext, close, closeCurrentTabBeforeClose }) => {
if (this.config.saveTrace)
await browserContext.tracing.stop();
const videos = this.config.saveVideo ? browserContext.pages().map(page => page.video()).filter(video => !!video) : [];
if (closeCurrentTabBeforeClose)
await this._currentTab?.page.close();
await close(async () => {
for (const video of videos) {
const name = await this.outputFile(dateAsFileName('webm'), { origin: 'code', reason: 'Saving video' });
Expand Down
10 changes: 5 additions & 5 deletions packages/playwright/src/mcp/extension/cdpRelay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ export class CDPRelayServer {
return `${this._wsHost}${this._extensionPath}`;
}

async ensureExtensionConnectionForMCPContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined) {
async ensureExtensionConnectionForMCPContext(clientInfo: ClientInfo, abortSignal: AbortSignal, createNewTab: boolean) {
debugLogger('Ensuring extension connection for MCP context');
if (this._extensionConnection)
return;
this._connectBrowser(clientInfo, toolName);
this._connectBrowser(clientInfo, createNewTab);
debugLogger('Waiting for incoming extension connection');
await Promise.race([
this._extensionConnectionPromise,
Expand All @@ -114,7 +114,7 @@ export class CDPRelayServer {
debugLogger('Extension connection established');
}

private _connectBrowser(clientInfo: ClientInfo, toolName: string | undefined) {
private _connectBrowser(clientInfo: ClientInfo, createNewTab: boolean) {
const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`;
// Need to specify "key" in the manifest.json to make the id stable when loading from file.
const url = new URL('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html');
Expand All @@ -125,8 +125,8 @@ export class CDPRelayServer {
};
url.searchParams.set('client', JSON.stringify(client));
url.searchParams.set('protocolVersion', process.env.PWMCP_TEST_PROTOCOL_VERSION ?? protocol.VERSION.toString());
if (toolName)
url.searchParams.set('newTab', String(toolName === 'browser_navigate'));
if (createNewTab)
url.searchParams.set('newTab', String(createNewTab));
const token = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
if (token)
url.searchParams.set('token', token);
Expand Down
20 changes: 14 additions & 6 deletions packages/playwright/src/mcp/extension/extensionContextFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { debug } from 'playwright-core/lib/utilsBundle';
import { startHttpServer } from '../sdk/http';
import { CDPRelayServer } from './cdpRelay';

import type { BrowserContextFactory } from '../browser/browserContextFactory';
import type { BrowserContextFactory, BrowserContextFactoryResult } from '../browser/browserContextFactory';
import type { ClientInfo } from '../sdk/server';

const debugLogger = debug('pw:mcp:relay');
Expand All @@ -36,20 +36,22 @@ export class ExtensionContextFactory implements BrowserContextFactory {
this._executablePath = executablePath;
}

async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> {
const browser = await this._obtainBrowser(clientInfo, abortSignal, toolName);
async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<BrowserContextFactoryResult> {
const createNewTab = toolName === 'browser_navigate';
const browser = await this._obtainBrowser(clientInfo, abortSignal, createNewTab);
return {
browserContext: browser.contexts()[0],
close: async () => {
debugLogger('close() called for browser context');
await browser.close();
}
},
closeCurrentTabBeforeClose: createNewTab,
};
}

private async _obtainBrowser(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<playwright.Browser> {
private async _obtainBrowser(clientInfo: ClientInfo, abortSignal: AbortSignal, createNewTab: boolean): Promise<playwright.Browser> {
const relay = await this._startRelay(abortSignal);
await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal, toolName);
await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal, createNewTab);
return await playwright.chromium.connectOverCDP(relay.cdpEndpoint());
}

Expand All @@ -65,3 +67,9 @@ export class ExtensionContextFactory implements BrowserContextFactory {
return cdpRelayServer;
}
}

export function closePageUsingEvaluate(page: playwright.Page): Promise<void> {
return page.evaluate(() => {
window.close();
});
}
4 changes: 2 additions & 2 deletions packages/playwright/src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import * as mcpServer from './sdk/server';

import type { Config } from './config';
import type { BrowserContext } from 'playwright';
import type { BrowserContextFactory } from './browser/browserContextFactory';
import type { BrowserContextFactory, BrowserContextFactoryResult } from './browser/browserContextFactory';
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';

const packageJSON = require('../../package.json');
Expand All @@ -42,7 +42,7 @@ class SimpleBrowserContextFactory implements BrowserContextFactory {
this._contextGetter = contextGetter;
}

async createContext(): Promise<{ browserContext: BrowserContext, close: () => Promise<void> }> {
async createContext(): Promise<BrowserContextFactoryResult> {
const browserContext = await this._contextGetter();
return {
browserContext,
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright/src/mcp/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { setupExitWatchdog } from './browser/watchdog';
import { contextFactory } from './browser/browserContextFactory';
import { ProxyBackend } from './sdk/proxyBackend';
import { BrowserServerBackend } from './browser/browserServerBackend';
import { ExtensionContextFactory } from './extension/extensionContextFactory';
import { closePageUsingEvaluate, ExtensionContextFactory } from './extension/extensionContextFactory';

import type { Command } from 'playwright-core/lib/utilsBundle';
import type { MCPProvider } from './sdk/proxyBackend';
Expand Down Expand Up @@ -83,7 +83,7 @@ export function decorateCommand(command: Command, version: string) {
name: 'Playwright w/ extension',
nameInConfig: 'playwright-extension',
version,
create: () => new BrowserServerBackend(config, extensionContextFactory)
create: () => new BrowserServerBackend(config, extensionContextFactory, closePageUsingEvaluate)
};
await mcpServer.start(serverBackendFactory, config.server);
return;
Expand All @@ -99,7 +99,7 @@ export function decorateCommand(command: Command, version: string) {
{
name: 'extension',
description: 'Connect to a browser using the Playwright MCP extension',
connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, extensionContextFactory)),
connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, extensionContextFactory, closePageUsingEvaluate)),
},
];
const factory: mcpServer.ServerBackendFactory = {
Expand Down
3 changes: 1 addition & 2 deletions packages/playwright/src/mcp/test/browserBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import { Tab } from '../browser/tab';
import type * as playwright from '../../../index';
import type { Page } from '../../../../playwright-core/src/client/page';
import type { BrowserContextFactory } from '../browser/browserContextFactory';
import type { ClientInfo } from '../sdk/server';

export async function runBrowserBackendAtEnd(context: playwright.BrowserContext, errorMessage?: string) {
const testInfo = currentTestInfo();
Expand Down Expand Up @@ -80,7 +79,7 @@ export async function runBrowserBackendAtEnd(context: playwright.BrowserContext,

function identityFactory(browserContext: playwright.BrowserContext): BrowserContextFactory {
return {
createContext: async (clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined) => {
createContext: async () => {
return {
browserContext,
close: async () => {}
Expand Down