Skip to content

Commit 9b23cc9

Browse files
authored
chore(mcp): remove context factory (1) (microsoft#39449)
1 parent cd528a2 commit 9b23cc9

30 files changed

Lines changed: 436 additions & 613 deletions

packages/playwright-core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"./lib/cli/program": "./lib/cli/program.js",
2727
"./lib/mcpBundle": "./lib/mcpBundle.js",
2828
"./lib/mcp/exports": "./lib/mcp/exports.js",
29+
"./lib/mcp/index": "./lib/mcp/index.js",
2930
"./lib/remote/playwrightServer": "./lib/remote/playwrightServer.js",
3031
"./lib/server": "./lib/server/index.js",
3132
"./lib/server/utils/image_tools/stats": "./lib/server/utils/image_tools/stats.js",

packages/playwright-core/src/cli/daemon/daemon.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ import { decorateServer } from '../../server/utils/network';
2525
import { gracefullyProcessExitDoNotHang } from '../../server/utils/processLauncher';
2626

2727
import { BrowserServerBackend } from '../../mcp/browser/browserServerBackend';
28+
import { browserTools } from '../../mcp/browser/tools';
2829
import { SocketConnection } from '../client/socketConnection';
2930
import { commands } from './commands';
3031
import { parseCommand } from './command';
3132

3233
import type * as mcp from '../../mcp/exports';
33-
import type { BrowserContextFactory } from '../../mcp/browser/browserContextFactory';
3434
import type { FullConfig } from '../../mcp/browser/config';
35+
import type { BrowserContextFactory } from '../../mcp/browser/browserContextFactory';
3536
import type { SessionConfig } from '../client/registry';
3637

3738
const daemonDebug = debug('pw:daemon');
@@ -75,18 +76,15 @@ export async function startMcpDaemonServer(
7576
timestamp: Date.now(),
7677
};
7778

78-
const { browserContext, close } = await contextFactory.createContext(clientInfo, new AbortController().signal, {});
79+
const browserContext = mcpConfig.browser.isolated ? await contextFactory.createContext(clientInfo) : (await contextFactory.contexts(clientInfo))[0];
7980
if (!noShutdown) {
8081
browserContext.on('close', () => {
8182
daemonDebug('browser closed, shutting down daemon');
8283
shutdown(0);
8384
});
8485
}
8586

86-
const existingContextFactory = {
87-
createContext: () => Promise.resolve({ browserContext, close }),
88-
};
89-
const backend = new BrowserServerBackend(mcpConfig, existingContextFactory, { allTools: true });
87+
const backend = new BrowserServerBackend(mcpConfig, browserContext, browserTools);
9088
await backend.initialize(clientInfo);
9189

9290
await fs.mkdir(path.dirname(socketPath), { recursive: true });
@@ -140,7 +138,7 @@ export async function startMcpDaemonServer(
140138
server.listen(socketPath, () => {
141139
daemonDebug(`daemon server listening on ${socketPath}`);
142140
resolve(async () => {
143-
backend.serverClosed();
141+
await backend.dispose();
144142
await new Promise(cb => server.close(cb));
145143
});
146144
});

packages/playwright-core/src/client/eventEmitter.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,3 +396,21 @@ function unwrapListeners(arr: Listener[]): Listener[] {
396396
function wrappedListener(l: Listener): Listener {
397397
return (l as any).listener;
398398
}
399+
400+
export type Disposable = {
401+
dispose: () => void;
402+
};
403+
404+
class EventsHelper {
405+
static addEventListener(
406+
emitter: EventEmitterType,
407+
eventName: (string | symbol),
408+
handler: (...args: any[]) => void): Disposable {
409+
emitter.on(eventName, handler);
410+
return {
411+
dispose: () => emitter.removeListener(eventName, handler)
412+
};
413+
}
414+
}
415+
416+
export const eventsHelper = EventsHelper;

packages/playwright-core/src/mcp/DEPS.list

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
../../
33
./sdk/
44
./browser/
5+
./browser/tools/
56
./extension/
67
../cli/
78
../utilsBundle.ts

packages/playwright-core/src/mcp/browser/DEPS.list

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
./tools/
44
../sdk/
55
../log.ts
6+
../../utils/
67
../../utils/isomorphic/
78
../../utilsBundle.ts
89
../../mcpBundle.ts

packages/playwright-core/src/mcp/browser/browserContextFactory.ts

Lines changed: 25 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import path from 'path';
2222
import * as playwright from '../../..';
2323
import { registryDirectory } from '../../server/registry/index';
2424
import { startTraceViewerServer } from '../../server';
25-
import { logUnhandledError, testDebug } from '../log';
25+
import { testDebug } from '../log';
2626
import { outputDir, outputFile } from './config';
2727
import { firstRootPath } from '../sdk/server';
2828

@@ -31,8 +31,6 @@ import type { LaunchOptions, BrowserContextOptions } from '../../client/types';
3131
import type { ClientInfo } from '../sdk/server';
3232

3333
export function contextFactory(config: FullConfig): BrowserContextFactory {
34-
if (config.sharedBrowserContext)
35-
return SharedContextFactory.create(config);
3634
if (config.browser.remoteEndpoint)
3735
return new RemoteContextFactory(config);
3836
if (config.browser.cdpEndpoint)
@@ -42,26 +40,19 @@ export function contextFactory(config: FullConfig): BrowserContextFactory {
4240
return new PersistentContextFactory(config);
4341
}
4442

45-
export type BrowserContextFactoryResult = {
46-
browserContext: playwright.BrowserContext;
47-
close: () => Promise<void>;
48-
};
49-
50-
type CreateContextOptions = {
51-
toolName?: string;
52-
};
53-
5443
export interface BrowserContextFactory {
55-
createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, options: CreateContextOptions): Promise<BrowserContextFactoryResult>;
44+
contexts(clientInfo: ClientInfo): Promise<playwright.BrowserContext[]>;
45+
createContext(clientInfo: ClientInfo): Promise<playwright.BrowserContext>;
5646
}
5747

5848
export function identityBrowserContextFactory(browserContext: playwright.BrowserContext): BrowserContextFactory {
5949
return {
60-
createContext: async (clientInfo: ClientInfo, abortSignal: AbortSignal, options: CreateContextOptions) => {
61-
return {
62-
browserContext,
63-
close: async () => {}
64-
};
50+
contexts: async (clientInfo: ClientInfo) => {
51+
return [browserContext];
52+
},
53+
54+
createContext: async (clientInfo: ClientInfo) => {
55+
return browserContext;
6556
}
6657
};
6758
}
@@ -76,11 +67,11 @@ class BaseContextFactory implements BrowserContextFactory {
7667
this.config = config;
7768
}
7869

79-
protected async _obtainBrowser(clientInfo: ClientInfo, options: CreateContextOptions): Promise<playwright.Browser> {
70+
protected async _obtainBrowser(clientInfo: ClientInfo): Promise<playwright.Browser> {
8071
if (this._browserPromise)
8172
return this._browserPromise;
8273
testDebug(`obtain browser (${this._logName})`);
83-
this._browserPromise = this._doObtainBrowser(clientInfo, options);
74+
this._browserPromise = this._doObtainBrowser(clientInfo);
8475
void this._browserPromise.then(browser => {
8576
browser.on('disconnected', () => {
8677
this._browserPromise = undefined;
@@ -91,43 +82,32 @@ class BaseContextFactory implements BrowserContextFactory {
9182
return this._browserPromise;
9283
}
9384

94-
protected async _doObtainBrowser(clientInfo: ClientInfo, options: CreateContextOptions): Promise<playwright.Browser> {
85+
protected async _doObtainBrowser(clientInfo: ClientInfo): Promise<playwright.Browser> {
9586
throw new Error('Not implemented');
9687
}
9788

98-
async createContext(clientInfo: ClientInfo, _: AbortSignal, options: CreateContextOptions): Promise<BrowserContextFactoryResult> {
89+
async contexts(clientInfo: ClientInfo): Promise<playwright.BrowserContext[]> {
90+
const browser = await this._obtainBrowser(clientInfo);
91+
return browser.contexts();
92+
}
93+
94+
async createContext(clientInfo: ClientInfo): Promise<playwright.BrowserContext> {
9995
testDebug(`create browser context (${this._logName})`);
100-
const browser = await this._obtainBrowser(clientInfo, options);
101-
const browserContext = await this._doCreateContext(browser, clientInfo);
102-
await addInitScript(browserContext, this.config.browser.initScript);
103-
return {
104-
browserContext,
105-
close: () => this._closeBrowserContext(browserContext, browser)
106-
};
96+
const browser = await this._obtainBrowser(clientInfo);
97+
return await this._doCreateContext(browser, clientInfo);
10798
}
10899

109100
protected async _doCreateContext(browser: playwright.Browser, clientInfo: ClientInfo): Promise<playwright.BrowserContext> {
110101
throw new Error('Not implemented');
111102
}
112-
113-
private async _closeBrowserContext(browserContext: playwright.BrowserContext, browser: playwright.Browser) {
114-
testDebug(`close browser context (${this._logName})`);
115-
if (browser.contexts().length === 1)
116-
this._browserPromise = undefined;
117-
await browserContext.close().catch(logUnhandledError);
118-
if (browser.contexts().length === 0) {
119-
testDebug(`close browser (${this._logName})`);
120-
await browser.close().catch(logUnhandledError);
121-
}
122-
}
123103
}
124104

125105
class IsolatedContextFactory extends BaseContextFactory {
126106
constructor(config: FullConfig) {
127107
super('isolated', config);
128108
}
129109

130-
protected override async _doObtainBrowser(clientInfo: ClientInfo, options: CreateContextOptions): Promise<playwright.Browser> {
110+
protected override async _doObtainBrowser(clientInfo: ClientInfo): Promise<playwright.Browser> {
131111
await injectCdpPort(this.config.browser);
132112
const browserType = playwright[this.config.browser.browserName];
133113
const tracesDir = await computeTracesDir(this.config, clientInfo);
@@ -185,16 +165,15 @@ class RemoteContextFactory extends BaseContextFactory {
185165
}
186166
}
187167

188-
class PersistentContextFactory implements BrowserContextFactory {
189-
readonly config: FullConfig;
168+
class PersistentContextFactory extends BaseContextFactory {
190169
readonly name = 'persistent';
191170
readonly description = 'Create a new persistent browser context';
192171

193172
constructor(config: FullConfig) {
194-
this.config = config;
173+
super('persistent', config);
195174
}
196175

197-
async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, options: CreateContextOptions): Promise<BrowserContextFactoryResult> {
176+
protected override async _doObtainBrowser(clientInfo: ClientInfo): Promise<playwright.Browser> {
198177
await injectCdpPort(this.config.browser);
199178
testDebug('create browser context (persistent)');
200179
const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo);
@@ -205,8 +184,6 @@ class PersistentContextFactory implements BrowserContextFactory {
205184
if (await isProfileLocked5Times(userDataDir))
206185
throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
207186

208-
testDebug('lock user data dir', userDataDir);
209-
210187
const browserType = playwright[this.config.browser.browserName];
211188
const launchOptions: LaunchOptions & BrowserContextOptions = {
212189
tracesDir,
@@ -221,9 +198,7 @@ class PersistentContextFactory implements BrowserContextFactory {
221198
};
222199
try {
223200
const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions);
224-
await addInitScript(browserContext, this.config.browser.initScript);
225-
const close = () => this._closeBrowserContext(browserContext, userDataDir);
226-
return { browserContext, close };
201+
return browserContext.browser()!;
227202
} catch (error: any) {
228203
if (error.message.includes('Executable doesn\'t exist'))
229204
throwBrowserIsNotInstalledError(this.config);
@@ -237,15 +212,6 @@ class PersistentContextFactory implements BrowserContextFactory {
237212
}
238213
}
239214

240-
private async _closeBrowserContext(browserContext: playwright.BrowserContext, userDataDir: string) {
241-
testDebug('close browser context (persistent)');
242-
testDebug('release user data dir', userDataDir);
243-
await browserContext.close().catch(() => {});
244-
if (process.env.PWMCP_PROFILES_DIR_FOR_TEST && userDataDir.startsWith(process.env.PWMCP_PROFILES_DIR_FOR_TEST))
245-
await fs.promises.rm(userDataDir, { recursive: true }).catch(logUnhandledError);
246-
testDebug('close browser context complete (persistent)');
247-
}
248-
249215
private async _createUserDataDir(clientInfo: ClientInfo) {
250216
const dir = process.env.PWMCP_PROFILES_DIR_FOR_TEST ?? registryDirectory;
251217
const browserToken = this.config.browser.launchOptions?.channel ?? this.config.browser?.browserName;
@@ -289,59 +255,6 @@ function createHash(data: string): string {
289255
return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7);
290256
}
291257

292-
async function addInitScript(browserContext: playwright.BrowserContext, initScript: string[] | undefined) {
293-
for (const scriptPath of initScript ?? [])
294-
await browserContext.addInitScript({ path: path.resolve(scriptPath) });
295-
}
296-
297-
export class SharedContextFactory implements BrowserContextFactory {
298-
private _contextPromise: Promise<BrowserContextFactoryResult> | undefined;
299-
private _baseFactory: BrowserContextFactory;
300-
private static _instance: SharedContextFactory | undefined;
301-
302-
static create(config: FullConfig) {
303-
if (SharedContextFactory._instance)
304-
throw new Error('SharedContextFactory already exists');
305-
const baseConfig = { ...config, sharedBrowserContext: false };
306-
const baseFactory = contextFactory(baseConfig);
307-
SharedContextFactory._instance = new SharedContextFactory(baseFactory);
308-
return SharedContextFactory._instance;
309-
}
310-
311-
private constructor(baseFactory: BrowserContextFactory) {
312-
this._baseFactory = baseFactory;
313-
}
314-
315-
async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, options: { toolName?: string }): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> {
316-
if (!this._contextPromise) {
317-
testDebug('create shared browser context');
318-
this._contextPromise = this._baseFactory.createContext(clientInfo, abortSignal, options);
319-
}
320-
321-
const { browserContext } = await this._contextPromise;
322-
testDebug(`shared context client connected`);
323-
return {
324-
browserContext,
325-
close: async () => {
326-
testDebug(`shared context client disconnected`);
327-
},
328-
};
329-
}
330-
331-
static async dispose() {
332-
await SharedContextFactory._instance?._dispose();
333-
}
334-
335-
private async _dispose() {
336-
const contextPromise = this._contextPromise;
337-
this._contextPromise = undefined;
338-
if (!contextPromise)
339-
return;
340-
const { close } = await contextPromise;
341-
await close();
342-
}
343-
}
344-
345258
async function computeTracesDir(config: FullConfig, clientInfo: ClientInfo): Promise<string | undefined> {
346259
return path.resolve(outputDir(config, clientInfo), 'traces');
347260
}

packages/playwright-core/src/mcp/browser/browserServerBackend.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,13 @@
1616

1717
import { FullConfig } from './config';
1818
import { Context } from './context';
19-
import { logUnhandledError } from '../log';
2019
import { Response } from './response';
2120
import { SessionLog } from './sessionLog';
22-
import { browserTools, filteredTools } from './tools';
2321
import { toMcpTool } from '../sdk/tool';
22+
import { logUnhandledError } from '../log';
2423

24+
import type * as playwright from '../../..';
2525
import type { Tool } from './tools/tool';
26-
import type { BrowserContextFactory } from './browserContextFactory';
2726
import type * as mcpServer from '../sdk/server';
2827
import type { ServerBackend } from '../sdk/server';
2928

@@ -32,24 +31,27 @@ export class BrowserServerBackend implements ServerBackend {
3231
private _context: Context | undefined;
3332
private _sessionLog: SessionLog | undefined;
3433
private _config: FullConfig;
35-
private _browserContextFactory: BrowserContextFactory;
34+
readonly browserContext: playwright.BrowserContext;
3635

37-
constructor(config: FullConfig, factory: BrowserContextFactory, options: { allTools?: boolean, structuredOutput?: boolean } = {}) {
36+
constructor(config: FullConfig, browserContext: playwright.BrowserContext, tools: Tool[]) {
3837
this._config = config;
39-
this._browserContextFactory = factory;
40-
this._tools = options.allTools ? browserTools : filteredTools(config);
38+
this._tools = tools;
39+
this.browserContext = browserContext;
4140
}
4241

4342
async initialize(clientInfo: mcpServer.ClientInfo): Promise<void> {
4443
this._sessionLog = this._config.saveSession ? await SessionLog.create(this._config, clientInfo) : undefined;
45-
this._context = new Context({
44+
this._context = new Context(this.browserContext, {
4645
config: this._config,
47-
browserContextFactory: this._browserContextFactory,
4846
sessionLog: this._sessionLog,
4947
clientInfo,
5048
});
5149
}
5250

51+
async dispose() {
52+
await this._context?.dispose().catch(logUnhandledError);
53+
}
54+
5355
async listTools(): Promise<mcpServer.Tool[]> {
5456
return this._tools.map(tool => toMcpTool(tool.schema));
5557
}
@@ -82,8 +84,4 @@ export class BrowserServerBackend implements ServerBackend {
8284
}
8385
return responseObject;
8486
}
85-
86-
serverClosed() {
87-
void this._context?.dispose().catch(logUnhandledError);
88-
}
8987
}

0 commit comments

Comments
 (0)