Skip to content

Commit 29fb934

Browse files
authored
fix(mcp): use single output dir (#37436)
1 parent 778439d commit 29fb934

File tree

17 files changed

+131
-74
lines changed

17 files changed

+131
-74
lines changed

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ import { registryDirectory } from 'playwright-core/lib/server/registry/index';
2424
import { startTraceViewerServer } from 'playwright-core/lib/server';
2525
import { findBrowserProcess, getBrowserExecPath } from './processUtils';
2626
import { logUnhandledError, testDebug } from '../log';
27-
import { outputFile } from './config';
27+
import { outputFile } from './config';
28+
import { firstRootPath } from '../sdk/server';
2829

2930
import type { FullConfig } from './config';
3031
import type { LaunchOptions } from '../../../../playwright-core/src/client/types';
32+
import type { ClientInfo } from '../sdk/server';
3133

3234
export function contextFactory(config: FullConfig): BrowserContextFactory {
3335
if (config.browser.remoteEndpoint)
@@ -39,8 +41,6 @@ export function contextFactory(config: FullConfig): BrowserContextFactory {
3941
return new PersistentContextFactory(config);
4042
}
4143

42-
export type ClientInfo = { name?: string, version?: string, rootPath?: string };
43-
4444
export interface BrowserContextFactory {
4545
createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }>;
4646
}
@@ -105,7 +105,7 @@ class IsolatedContextFactory extends BaseContextFactory {
105105
protected override async _doObtainBrowser(clientInfo: ClientInfo): Promise<playwright.Browser> {
106106
await injectCdpPort(this.config.browser);
107107
const browserType = playwright[this.config.browser.browserName];
108-
const tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces`);
108+
const tracesDir = await outputFile(this.config, clientInfo, `traces`);
109109
if (this.config.saveTrace)
110110
await startTraceServer(this.config, tracesDir);
111111
return browserType.launch({
@@ -171,8 +171,8 @@ class PersistentContextFactory implements BrowserContextFactory {
171171
async createContext(clientInfo: ClientInfo): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> {
172172
await injectCdpPort(this.config.browser);
173173
testDebug('create browser context (persistent)');
174-
const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo.rootPath);
175-
const tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces`);
174+
const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo);
175+
const tracesDir = await outputFile(this.config, clientInfo, `traces`);
176176
if (this.config.saveTrace)
177177
await startTraceServer(this.config, tracesDir);
178178

@@ -218,10 +218,11 @@ class PersistentContextFactory implements BrowserContextFactory {
218218
testDebug('close browser context complete (persistent)');
219219
}
220220

221-
private async _createUserDataDir(rootPath: string | undefined) {
221+
private async _createUserDataDir(clientInfo: ClientInfo) {
222222
const dir = process.env.PWMCP_PROFILES_DIR_FOR_TEST ?? registryDirectory;
223223
const browserToken = this.config.browser.launchOptions?.channel ?? this.config.browser?.browserName;
224224
// Hesitant putting hundreds of files into the user's workspace, so using it for hashing instead.
225+
const rootPath = firstRootPath(clientInfo);
225226
const rootPathToken = rootPath ? `-${createHash(rootPath)}` : '';
226227
const result = path.join(dir, `mcp-${browserToken}${rootPathToken}`);
227228
await fs.promises.mkdir(result, { recursive: true });

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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { fileURLToPath } from 'url';
1817
import { FullConfig } from './config';
1918
import { Context } from './context';
2019
import { logUnhandledError } from '../log';
@@ -41,19 +40,13 @@ export class BrowserServerBackend implements ServerBackend {
4140
this._tools = filteredTools(config);
4241
}
4342

44-
async initialize(server: mcpServer.Server, clientVersion: mcpServer.ClientVersion, roots: mcpServer.Root[]): Promise<void> {
45-
let rootPath: string | undefined;
46-
if (roots.length > 0) {
47-
const firstRootUri = roots[0]?.uri;
48-
const url = firstRootUri ? new URL(firstRootUri) : undefined;
49-
rootPath = url ? fileURLToPath(url) : undefined;
50-
}
51-
this._sessionLog = this._config.saveSession ? await SessionLog.create(this._config, rootPath) : undefined;
43+
async initialize(server: mcpServer.Server, clientInfo: mcpServer.ClientInfo): Promise<void> {
44+
this._sessionLog = this._config.saveSession ? await SessionLog.create(this._config, clientInfo) : undefined;
5245
this._context = new Context({
5346
config: this._config,
5447
browserContextFactory: this._browserContextFactory,
5548
sessionLog: this._sessionLog,
56-
clientInfo: { ...clientVersion, rootPath },
49+
clientInfo,
5750
});
5851
}
5952

packages/playwright/src/mcp/browser/config.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@
1717
import fs from 'fs';
1818
import os from 'os';
1919
import path from 'path';
20+
2021
import { devices } from 'playwright-core';
2122
import { dotenv } from 'playwright-core/lib/utilsBundle';
2223

24+
import { firstRootPath } from '../sdk/server';
25+
2326
import type * as playwright from '../../../types/test';
2427
import type { Config, ToolCapability } from '../config';
28+
import type { ClientInfo } from '../sdk/server';
2529

2630
export type CLIOptions = {
2731
allowedOrigins?: string[];
@@ -265,10 +269,11 @@ async function loadConfig(configFile: string | undefined): Promise<Config> {
265269
}
266270
}
267271

268-
export async function outputFile(config: FullConfig, rootPath: string | undefined, name: string): Promise<string> {
272+
export async function outputFile(config: FullConfig, clientInfo: ClientInfo, name: string): Promise<string> {
273+
const rootPath = firstRootPath(clientInfo);
269274
const outputDir = config.outputDir
270275
?? (rootPath ? path.join(rootPath, '.playwright-mcp') : undefined)
271-
?? path.join(os.tmpdir(), 'playwright-mcp-output', sanitizeForFilePath(new Date().toISOString()));
276+
?? path.join(process.env.PW_TMPDIR_FOR_TEST ?? os.tmpdir(), 'playwright-mcp-output', String(clientInfo.timestamp));
272277

273278
await fs.promises.mkdir(outputDir, { recursive: true });
274279
const fileName = sanitizeForFilePath(name);

packages/playwright/src/mcp/browser/context.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ import * as codegen from './codegen';
2323

2424
import type * as playwright from '../../../types/test';
2525
import type { FullConfig } from './config';
26-
import type { BrowserContextFactory, ClientInfo } from './browserContextFactory';
26+
import type { BrowserContextFactory } from './browserContextFactory';
2727
import type * as actions from './actions';
2828
import type { SessionLog } from './sessionLog';
2929
import type { Tracing } from '../../../../playwright-core/src/client/tracing';
30+
import type { ClientInfo } from '../sdk/server';
3031

3132
const testDebug = debug('pw:mcp:test');
3233

@@ -113,7 +114,7 @@ export class Context {
113114
}
114115

115116
async outputFile(name: string): Promise<string> {
116-
return outputFile(this.config, this._clientInfo.rootPath, name);
117+
return outputFile(this.config, this._clientInfo, name);
117118
}
118119

119120
private _onPageCreated(page: playwright.Page) {

packages/playwright/src/mcp/browser/sessionLog.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { outputFile } from './config';
2424
import type { FullConfig } from './config';
2525
import type * as actions from './actions';
2626
import type { Tab, TabSnapshot } from './tab';
27+
import type * as mcpServer from '../sdk/server';
2728

2829
type LogEntry = {
2930
timestamp: number;
@@ -51,8 +52,8 @@ export class SessionLog {
5152
this._file = path.join(this._folder, 'session.md');
5253
}
5354

54-
static async create(config: FullConfig, rootPath: string | undefined): Promise<SessionLog> {
55-
const sessionFolder = await outputFile(config, rootPath, `session-${Date.now()}`);
55+
static async create(config: FullConfig, clientInfo: mcpServer.ClientInfo): Promise<SessionLog> {
56+
const sessionFolder = await outputFile(config, clientInfo, `session-${Date.now()}`);
5657
await fs.promises.mkdir(sessionFolder, { recursive: true });
5758
// eslint-disable-next-line no-console
5859
console.error(`Session: ${sessionFolder}`);

packages/playwright/src/mcp/extension/cdpRelay.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { logUnhandledError } from '../log';
3434
import * as protocol from './protocol';
3535

3636
import type websocket from 'ws';
37-
import type { ClientInfo } from '../browser/browserContextFactory';
37+
import type { ClientInfo } from '../sdk/server';
3838
import type { ExtensionCommand, ExtensionEvents } from './protocol';
3939
import type { WebSocket, WebSocketServer } from 'playwright-core/lib/utilsBundle';
4040

packages/playwright/src/mcp/extension/extensionContextFactory.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import { debug } from 'playwright-core/lib/utilsBundle';
2020
import { startHttpServer } from '../sdk/http';
2121
import { CDPRelayServer } from './cdpRelay';
2222

23-
import type { BrowserContextFactory, ClientInfo } from '../browser/browserContextFactory';
23+
import type { BrowserContextFactory } from '../browser/browserContextFactory';
24+
import type { ClientInfo } from '../sdk/server';
2425

2526
const debugLogger = debug('pw:mcp:relay');
2627

packages/playwright/src/mcp/sdk/mdb.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ export class MDBBackend implements mcpServer.ServerBackend {
3434
private _stack: { client: Client, toolNames: string[], resultPromise: ManualPromise<mcpServer.CallToolResult> | undefined }[] = [];
3535
private _interruptPromise: ManualPromise<mcpServer.CallToolResult> | undefined;
3636
private _topLevelBackend: mcpServer.ServerBackend;
37-
private _roots: mcpServer.Root[] | undefined;
37+
private _clientInfo: mcpServer.ClientInfo | undefined;
3838

3939
constructor(topLevelBackend: mcpServer.ServerBackend) {
4040
this._topLevelBackend = topLevelBackend;
4141
}
4242

43-
async initialize(server: mcpServer.Server, clientVersion: mcpServer.ClientVersion, roots: mcpServer.Root[]): Promise<void> {
44-
if (!this._roots)
45-
this._roots = roots;
43+
async initialize(server: mcpServer.Server, clientInfo: mcpServer.ClientInfo): Promise<void> {
44+
if (!this._clientInfo)
45+
this._clientInfo = clientInfo;
4646
}
4747

4848
async listTools(): Promise<mcpServer.Tool[]> {
@@ -107,8 +107,8 @@ export class MDBBackend implements mcpServer.ServerBackend {
107107

108108
private async _pushClient(transport: Transport, introMessage?: string): Promise<mcpServer.CallToolResult> {
109109
mdbDebug('pushing client to the stack');
110-
const client = new mcpBundle.Client({ name: 'Internal client', version: '0.0.0' }, { capabilities: { roots: {} } });
111-
client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._roots || [] }));
110+
const client = new mcpBundle.Client({ name: 'Pushing client', version: '0.0.0' }, { capabilities: { roots: {} } });
111+
client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._clientInfo?.roots || [] }));
112112
client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
113113
await client.connect(transport);
114114
mdbDebug('connected to the new client');
@@ -169,7 +169,7 @@ export async function runOnPauseBackendLoop(backend: mcpServer.ServerBackend, in
169169
await mcpHttp.installHttpTransport(httpServer, factory);
170170
const url = mcpHttp.httpAddressToString(httpServer.address());
171171

172-
const client = new mcpBundle.Client({ name: 'Internal client', version: '0.0.0' });
172+
const client = new mcpBundle.Client({ name: 'On-pause client', version: '0.0.0' });
173173
client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
174174
const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(process.env.PLAYWRIGHT_DEBUGGER_MCP!));
175175
await client.connect(transport);
@@ -205,8 +205,8 @@ class ServerBackendWithCloseListener implements mcpServer.ServerBackend {
205205
this._backend = backend;
206206
}
207207

208-
async initialize(server: mcpServer.Server, clientVersion: mcpServer.ClientVersion, roots: mcpServer.Root[]): Promise<void> {
209-
await this._backend.initialize?.(server, clientVersion, roots);
208+
async initialize(server: mcpServer.Server, clientInfo: mcpServer.ClientInfo): Promise<void> {
209+
await this._backend.initialize?.(server, clientInfo);
210210
}
211211

212212
async listTools(): Promise<mcpServer.Tool[]> {

packages/playwright/src/mcp/sdk/proxyBackend.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { debug } from 'playwright-core/lib/utilsBundle';
1818

1919
import * as mcpBundle from './bundle';
2020

21-
import type { ServerBackend, ClientVersion, Root, Server } from './server';
21+
import type { ServerBackend, ClientInfo, Server } from './server';
2222
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
2323
import type { Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
2424
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
@@ -36,15 +36,15 @@ export class ProxyBackend implements ServerBackend {
3636
private _mcpProviders: MCPProvider[];
3737
private _currentClient: Client | undefined;
3838
private _contextSwitchTool: Tool;
39-
private _roots: Root[] = [];
39+
private _clientInfo: ClientInfo | undefined;
4040

4141
constructor(mcpProviders: MCPProvider[]) {
4242
this._mcpProviders = mcpProviders;
4343
this._contextSwitchTool = this._defineContextSwitchTool();
4444
}
4545

46-
async initialize(server: Server, clientVersion: ClientVersion, roots: Root[]): Promise<void> {
47-
this._roots = roots;
46+
async initialize(server: Server, clientInfo: ClientInfo): Promise<void> {
47+
this._clientInfo = clientInfo;
4848
}
4949

5050
async listTools(): Promise<Tool[]> {
@@ -124,7 +124,7 @@ export class ProxyBackend implements ServerBackend {
124124
listRoots: true,
125125
},
126126
});
127-
client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._roots }));
127+
client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._clientInfo?.roots || [] }));
128128
client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
129129

130130
const transport = await factory.connect();

packages/playwright/src/mcp/sdk/server.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { fileURLToPath } from 'url';
18+
1719
import { debug } from 'playwright-core/lib/utilsBundle';
1820

1921
import * as mcpBundle from './bundle';
@@ -28,10 +30,15 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
2830

2931
const serverDebug = debug('pw:mcp:server');
3032

31-
export type ClientVersion = { name: string, version: string };
33+
export type ClientInfo = {
34+
name: string;
35+
version: string;
36+
roots: Root[];
37+
timestamp: number;
38+
};
3239

3340
export interface ServerBackend {
34-
initialize?(server: Server, clientVersion: ClientVersion, roots: Root[]): Promise<void>;
41+
initialize?(server: Server, clientInfo: ClientInfo): Promise<void>;
3542
listTools(): Promise<Tool[]>;
3643
callTool(name: string, args: CallToolRequest['params']['arguments']): Promise<CallToolResult>;
3744
serverClosed?(server: Server): void;
@@ -93,8 +100,15 @@ const initializeServer = async (server: Server, backend: ServerBackend, runHeart
93100
const { roots } = await server.listRoots();
94101
clientRoots = roots;
95102
}
96-
const clientVersion = server.getClientVersion() ?? { name: 'unknown', version: 'unknown' };
97-
await backend.initialize?.(server, clientVersion, clientRoots);
103+
104+
const clientInfo: ClientInfo = {
105+
name: server.getClientVersion()?.name ?? 'unknown',
106+
version: server.getClientVersion()?.version ?? 'unknown',
107+
roots: clientRoots,
108+
timestamp: Date.now(),
109+
};
110+
111+
await backend.initialize?.(server, clientInfo);
98112
if (runHeartbeat)
99113
startHeartbeat(server);
100114
};
@@ -145,3 +159,11 @@ export async function start(serverBackendFactory: ServerBackendFactory, options:
145159
// eslint-disable-next-line no-console
146160
console.error(message);
147161
}
162+
163+
export function firstRootPath(clientInfo: ClientInfo): string | undefined {
164+
if (clientInfo.roots.length === 0)
165+
return undefined;
166+
const firstRootUri = clientInfo.roots[0]?.uri;
167+
const url = firstRootUri ? new URL(firstRootUri) : undefined;
168+
return url ? fileURLToPath(url) : undefined;
169+
}

0 commit comments

Comments
 (0)