Skip to content

Commit eadd2dd

Browse files
committed
feat(extension): support multi-tab - Playwright can open and control multiple tabs
- Add createTab command to protocol: relay calls it when Playwright opens a new tab via Target.createTarget; extension creates a real Chrome tab and attaches the debugger - Add tabId to forwardCDPCommand/forwardCDPEvent: routes CDP traffic to the correct tab - cdpRelay: replace single _connectedTabInfo with _tabSessions map (sessionId → tabId) and _tabIdToSessionId map for reverse lookup from CDP events - Handle Target.createTarget: call createTab on extension, emit Target.attachedToTarget - Handle Target.getTargets: return all tracked tabs - Bump protocol VERSION to 2
1 parent 837d8eb commit eadd2dd

File tree

2 files changed

+65
-28
lines changed

2 files changed

+65
-28
lines changed

packages/playwright-core/src/tools/mcp/cdpRelay.ts

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,10 @@ export class CDPRelayServer {
6868
private _wss: WebSocketServer;
6969
private _playwrightConnection: WebSocket | null = null;
7070
private _extensionConnection: ExtensionConnection | null = null;
71-
private _connectedTabInfo: {
72-
targetInfo: any;
73-
// Page sessionId that should be used by this connection.
74-
sessionId: string;
75-
} | undefined;
71+
// Map from relay sessionId (pw-tab-N) to tab info.
72+
private _tabSessions = new Map<string, { targetInfo: any; tabId?: number }>();
73+
// Map from extension tabId to relay sessionId, for routing CDP events.
74+
private _tabIdToSessionId = new Map<number, string>();
7675
private _nextSessionId: number = 1;
7776
private _extensionConnectionPromise!: ManualPromise<void>;
7877

@@ -214,7 +213,8 @@ export class CDPRelayServer {
214213
}
215214

216215
private _resetExtensionConnection() {
217-
this._connectedTabInfo = undefined;
216+
this._tabSessions.clear();
217+
this._tabIdToSessionId.clear();
218218
this._extensionConnection = null;
219219
this._extensionConnectionPromise = new ManualPromise();
220220
void this._extensionConnectionPromise.catch(logUnhandledError);
@@ -245,14 +245,22 @@ export class CDPRelayServer {
245245

246246
private _handleExtensionMessage<M extends keyof ExtensionEvents>(method: M, params: ExtensionEvents[M]['params']) {
247247
switch (method) {
248-
case 'forwardCDPEvent':
249-
const sessionId = params.sessionId || this._connectedTabInfo?.sessionId;
248+
case 'forwardCDPEvent': {
249+
let sessionId = params.sessionId;
250+
if (!sessionId) {
251+
// Route event to the correct relay session by tabId, or fall back to the first session.
252+
if (params.tabId !== undefined)
253+
sessionId = this._tabIdToSessionId.get(params.tabId);
254+
else
255+
sessionId = [...this._tabSessions.keys()][0];
256+
}
250257
this._sendToPlaywright({
251258
sessionId,
252259
method: params.method,
253-
params: params.params
260+
params: params.params,
254261
});
255262
break;
263+
}
256264
}
257265
}
258266

@@ -288,28 +296,47 @@ export class CDPRelayServer {
288296
// Forward child session handling.
289297
if (sessionId)
290298
break;
291-
// Simulate auto-attach behavior with real target info
292-
const { targetInfo } = await this._extensionConnection!.send('attachToTab', { });
293-
this._connectedTabInfo = {
294-
targetInfo,
295-
sessionId: `pw-tab-${this._nextSessionId++}`,
296-
};
299+
// Simulate auto-attach behavior with real target info.
300+
const { targetInfo, tabId } = await this._extensionConnection!.send('attachToTab', { });
301+
const tabSessionId = `pw-tab-${this._nextSessionId++}`;
302+
this._tabSessions.set(tabSessionId, { targetInfo, tabId });
303+
if (tabId !== undefined)
304+
this._tabIdToSessionId.set(tabId, tabSessionId);
297305
debugLogger('Simulating auto-attach');
298306
this._sendToPlaywright({
299307
method: 'Target.attachedToTarget',
300308
params: {
301-
sessionId: this._connectedTabInfo.sessionId,
302-
targetInfo: {
303-
...this._connectedTabInfo.targetInfo,
304-
attached: true,
305-
},
306-
waitingForDebugger: false
309+
sessionId: tabSessionId,
310+
targetInfo: { ...targetInfo, attached: true },
311+
waitingForDebugger: false,
307312
}
308313
});
309314
return { };
310315
}
316+
case 'Target.createTarget': {
317+
const { targetInfo, tabId } = await this._extensionConnection!.send('createTab', { url: params?.url });
318+
const tabSessionId = `pw-tab-${this._nextSessionId++}`;
319+
this._tabSessions.set(tabSessionId, { targetInfo, tabId });
320+
if (tabId !== undefined)
321+
this._tabIdToSessionId.set(tabId, tabSessionId);
322+
this._sendToPlaywright({
323+
method: 'Target.attachedToTarget',
324+
params: {
325+
sessionId: tabSessionId,
326+
targetInfo: { ...targetInfo, attached: true },
327+
waitingForDebugger: false,
328+
}
329+
});
330+
return { targetId: targetInfo.targetId };
331+
}
332+
case 'Target.getTargets': {
333+
return {
334+
targetInfos: [...this._tabSessions.values()].map(s => ({ ...s.targetInfo, attached: true })),
335+
};
336+
}
311337
case 'Target.getTargetInfo': {
312-
return this._connectedTabInfo?.targetInfo;
338+
const tabSession = sessionId ? this._tabSessions.get(sessionId) : undefined;
339+
return (tabSession ?? [...this._tabSessions.values()][0])?.targetInfo;
313340
}
314341
}
315342
return await this._forwardToExtension(method, params, sessionId);
@@ -318,10 +345,13 @@ export class CDPRelayServer {
318345
private async _forwardToExtension(method: string, params: any, sessionId: string | undefined): Promise<any> {
319346
if (!this._extensionConnection)
320347
throw new Error('Extension not connected');
321-
// Top level sessionId is only passed between the relay and the client.
322-
if (this._connectedTabInfo?.sessionId === sessionId)
348+
// Relay sessionIds (pw-tab-N) are internal — resolve them to tabId for routing.
349+
let tabId: number | undefined;
350+
if (sessionId && this._tabSessions.has(sessionId)) {
351+
tabId = this._tabSessions.get(sessionId)!.tabId;
323352
sessionId = undefined;
324-
return await this._extensionConnection.send('forwardCDPCommand', { sessionId, method, params });
353+
}
354+
return await this._extensionConnection.send('forwardCDPCommand', { sessionId, tabId, method, params });
325355
}
326356

327357
private _sendToPlaywright(message: CDPResponse): void {

packages/playwright-core/src/tools/mcp/protocol.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,22 @@
1616

1717
// Whenever the commands/events change, the version must be updated. The latest
1818
// extension version should be compatible with the old MCP clients.
19-
export const VERSION = 1;
19+
export const VERSION = 2;
2020

2121
export type ExtensionCommand = {
2222
'attachToTab': {
2323
params: {};
2424
};
25+
'createTab': {
26+
params: {
27+
url?: string;
28+
};
29+
};
2530
'forwardCDPCommand': {
2631
params: {
2732
method: string,
28-
sessionId?: string
33+
sessionId?: string,
34+
tabId?: number,
2935
params?: any,
3036
};
3137
};
@@ -35,7 +41,8 @@ export type ExtensionEvents = {
3541
'forwardCDPEvent': {
3642
params: {
3743
method: string,
38-
sessionId?: string
44+
sessionId?: string,
45+
tabId?: number,
3946
params?: any,
4047
};
4148
};

0 commit comments

Comments
 (0)