Skip to content

Commit c0cc980

Browse files
authored
feat(mcp): pass MCP client name to extension connection (#40049)
1 parent 2611e04 commit c0cc980

11 files changed

Lines changed: 100 additions & 57 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ nohup.out
2020
.trace
2121
.tmp
2222
.playwright-cli
23+
.playwright-mcp
2324
allure*
2425
blob-report
2526
playwright-report

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,12 @@ import { parseCommand } from './command';
2828
import { commands } from './commands';
2929

3030
import { SocketConnection } from '../utils/socketConnection';
31-
import { createClientInfo } from '../cli-client/registry';
32-
3331
import type * as playwright from '../../..';
3432
import type { SessionConfig, ClientInfo } from '../cli-client/registry';
3533
import type { CallToolRequest, CallToolResult } from '../backend/tool';
3634
import type { ContextConfig } from '../backend/context';
3735
import type { BrowserInfo } from '../../serverRegistry';
36+
import type { ClientInfo as McpClientInfo } from '../utils/mcp/server';
3837

3938
async function socketExists(socketPath: string): Promise<boolean> {
4039
try {
@@ -50,9 +49,10 @@ export async function startCliDaemonServer(
5049
sessionName: string,
5150
browserContext: playwright.BrowserContext,
5251
browserInfo: BrowserInfo,
53-
contextConfig: ContextConfig = {},
54-
clientInfo = createClientInfo(),
55-
options?: {
52+
contextConfig: ContextConfig,
53+
clientInfo: ClientInfo,
54+
mcpClientInfo: McpClientInfo,
55+
options: {
5656
persistent?: boolean,
5757
exitOnClose?: boolean,
5858
}
@@ -70,7 +70,7 @@ export async function startCliDaemonServer(
7070
}
7171

7272
const backend = new BrowserBackend(contextConfig, browserContext, browserTools);
73-
await backend.initialize({ cwd: process.cwd() });
73+
await backend.initialize(mcpClientInfo);
7474

7575
if (browserContext.isClosed())
7676
throw new Error('Browser context was closed before the daemon could start');

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,20 @@ program.argument('[session-name]', 'name of the session to create or connect to'
5050
setupExitWatchdog();
5151
const clientInfo = createClientInfo();
5252
const mcpConfig = await configUtils.resolveCLIConfigForCLI(clientInfo.daemonProfilesDir, sessionName, options);
53-
const clientInfoEx = {
53+
const mcpClientInfo = {
5454
cwd: process.cwd(),
55-
sessionName,
56-
workspaceDir: clientInfo.workspaceDir,
55+
clientName: guessClientName(),
5756
};
5857

5958
try {
60-
const { browser, browserInfo } = await createBrowserWithInfo(mcpConfig, clientInfoEx);
59+
const { browser, browserInfo, canBind } = await createBrowserWithInfo(mcpConfig, mcpClientInfo);
60+
if (canBind)
61+
await browser.bind(sessionName, { workspaceDir: clientInfo.workspaceDir });
6162
const browserContext = mcpConfig.browser.isolated ? await browser.newContext(mcpConfig.browser.contextOptions) : browser.contexts()[0];
6263
if (!browserContext)
6364
throw new Error('Error: unable to connect to a browser that does not have any contexts');
6465
const persistent = options.persistent || options.profile || mcpConfig.browser.userDataDir ? true : undefined;
65-
const socketPath = await startCliDaemonServer(sessionName, browserContext, browserInfo, mcpConfig, clientInfo, { persistent, exitOnClose: true });
66+
const socketPath = await startCliDaemonServer(sessionName, browserContext, browserInfo, mcpConfig, clientInfo, mcpClientInfo, { persistent, exitOnClose: true });
6667
console.log(`### Success\nDaemon listening on ${socketPath}`);
6768
console.log('<EOF>');
6869
} catch (error) {
@@ -74,6 +75,14 @@ program.argument('[session-name]', 'name of the session to create or connect to'
7475

7576
void program.parseAsync();
7677

78+
function guessClientName(): string {
79+
if (process.env.CLAUDECODE)
80+
return 'Claude Code';
81+
if (process.env.COPILOT_CLI)
82+
return 'GitHub Copilot';
83+
return 'playwright-cli';
84+
}
85+
7786
function defaultConfigFile(): string {
7887
return path.resolve('.playwright', 'cli.config.json');
7988
}

packages/playwright-core/src/tools/exports.ts

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

17-
export { createClientInfo } from './cli-client/registry';
18-
export { startCliDaemonServer } from './cli-daemon/daemon';
19-
export { logUnhandledError } from './mcp/log';
2017
export { setupExitWatchdog } from './mcp/watchdog';
21-
export { toMcpTool } from './utils/mcp/tool';
2218

2319
export { BrowserBackend } from './backend/browserBackend';
2420
export { parseResponse } from './backend/response';

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

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -35,36 +35,37 @@ import type { ClientInfo } from '../utils/mcp/server';
3535
import type { Playwright } from '../../client/playwright';
3636
import type { BrowserInfo } from '../../serverRegistry';
3737

38-
type ClientInfoEx = ClientInfo & {
39-
sessionName?: string;
40-
workspaceDir?: string;
41-
};
42-
4338
type BrowserWithInfo = {
4439
browser: playwright.Browser,
45-
browserInfo: BrowserInfo
40+
browserInfo: BrowserInfo,
41+
canBind: boolean,
4642
};
4743

48-
export async function createBrowser(config: FullConfig, clientInfo: ClientInfoEx): Promise<playwright.Browser> {
44+
export async function createBrowser(config: FullConfig, clientInfo: ClientInfo): Promise<playwright.Browser> {
4945
const { browser } = await createBrowserWithInfo(config, clientInfo);
5046
return browser;
5147
}
5248

53-
export async function createBrowserWithInfo(config: FullConfig, clientInfo: ClientInfoEx): Promise<BrowserWithInfo> {
49+
export async function createBrowserWithInfo(config: FullConfig, clientInfo: ClientInfo): Promise<BrowserWithInfo> {
5450
if (config.browser.remoteEndpoint)
5551
return await createRemoteBrowser(config);
5652

5753
let browser: playwright.Browser;
58-
if (config.browser.cdpEndpoint)
59-
browser = await createCDPBrowser(config, clientInfo);
60-
else if (config.browser.isolated)
54+
let canBind = false;
55+
if (config.browser.cdpEndpoint) {
56+
browser = await createCDPBrowser(config);
57+
canBind = true;
58+
} else if (config.browser.isolated) {
6159
browser = await createIsolatedBrowser(config, clientInfo);
62-
else if (config.extension)
63-
browser = await createExtensionBrowser(config, clientInfo);
64-
else
60+
canBind = true;
61+
} else if (config.extension) {
62+
browser = await createExtensionBrowser(config, clientInfo.clientName);
63+
} else {
6564
browser = await createPersistentBrowser(config, clientInfo);
65+
canBind = true;
66+
}
6667

67-
return { browser, browserInfo: browserInfo(browser, config) };
68+
return { browser, browserInfo: browserInfo(browser, config), canBind };
6869
}
6970

7071
export interface BrowserContextFactory {
@@ -82,7 +83,7 @@ function browserInfo(browser: playwright.Browser, config: FullConfig): BrowserIn
8283
};
8384
}
8485

85-
async function createIsolatedBrowser(config: FullConfig, clientInfo: ClientInfoEx): Promise<playwright.Browser> {
86+
async function createIsolatedBrowser(config: FullConfig, clientInfo: ClientInfo): Promise<playwright.Browser> {
8687
testDebug('create browser (isolated)');
8788
await injectCdpPort(config.browser);
8889
const browserType = playwright[config.browser.browserName];
@@ -97,17 +98,15 @@ async function createIsolatedBrowser(config: FullConfig, clientInfo: ClientInfoE
9798
throwBrowserIsNotInstalledError(config);
9899
throw error;
99100
});
100-
await startServer(browser, clientInfo);
101101
return browser;
102102
}
103103

104-
async function createCDPBrowser(config: FullConfig, clientInfo: ClientInfoEx): Promise<playwright.Browser> {
104+
async function createCDPBrowser(config: FullConfig): Promise<playwright.Browser> {
105105
testDebug('create browser (cdp)');
106106
const browser = await playwright.chromium.connectOverCDP(config.browser.cdpEndpoint!, {
107107
headers: config.browser.cdpHeaders,
108108
timeout: config.browser.cdpTimeout
109109
});
110-
await startServer(browser, clientInfo);
111110
return browser;
112111
}
113112

@@ -123,7 +122,8 @@ async function createRemoteBrowser(config: FullConfig): Promise<BrowserWithInfo>
123122
browserName: descriptor.browser.browserName,
124123
launchOptions: descriptor.browser.launchOptions,
125124
userDataDir: descriptor.browser.userDataDir
126-
}
125+
},
126+
canBind: false,
127127
};
128128
}
129129

@@ -132,10 +132,10 @@ async function createRemoteBrowser(config: FullConfig): Promise<BrowserWithInfo>
132132
// Use connectToBrowser instead of playwright[browserName].connect because we don't have browserName.
133133
const browser = await connectToBrowser(playwrightObject, { endpoint });
134134
browser._connectToBrowserType(playwrightObject[browser._browserName], {}, undefined);
135-
return { browser, browserInfo: browserInfo(browser, config) };
135+
return { browser, browserInfo: browserInfo(browser, config), canBind: false };
136136
}
137137

138-
async function createPersistentBrowser(config: FullConfig, clientInfo: ClientInfoEx): Promise<playwright.Browser> {
138+
async function createPersistentBrowser(config: FullConfig, clientInfo: ClientInfo): Promise<playwright.Browser> {
139139
testDebug('create browser (persistent)');
140140
await injectCdpPort(config.browser);
141141
const userDataDir = config.browser.userDataDir ?? await createUserDataDir(config, clientInfo);
@@ -162,7 +162,6 @@ async function createPersistentBrowser(config: FullConfig, clientInfo: ClientInf
162162
try {
163163
const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions);
164164
const browser = browserContext.browser()!;
165-
await startServer(browser, clientInfo);
166165
return browser;
167166
} catch (error: any) {
168167
if (error.message.includes('Executable doesn\'t exist'))
@@ -254,8 +253,3 @@ function throwBrowserIsNotInstalledError(config: FullConfig): never {
254253
else
255254
throw new Error(`Browser "${channel}" is not installed. Run \`npx @playwright/mcp install-browser ${channel}\` to install`);
256255
}
257-
258-
async function startServer(browser: playwright.Browser, clientInfo: ClientInfoEx) {
259-
if (clientInfo.sessionName)
260-
await browser.bind(clientInfo.sessionName, { workspaceDir: clientInfo.workspaceDir });
261-
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import { logUnhandledError } from './log';
3535
import * as protocol from './protocol';
3636

3737
import type websocket from 'ws';
38-
import type { ClientInfo } from '../utils/mcp/server';
3938
import type { ExtensionCommand, ExtensionEvents } from './protocol';
4039
import type { WebSocket, WebSocketServer } from '../../utilsBundle';
4140

@@ -99,11 +98,11 @@ export class CDPRelayServer {
9998
return `${this._wsHost}${this._extensionPath}`;
10099
}
101100

102-
async ensureExtensionConnectionForMCPContext(clientInfo: ClientInfo) {
101+
async ensureExtensionConnectionForMCPContext(clientName: string) {
103102
debugLogger('Ensuring extension connection for MCP context');
104103
if (this._extensionConnection)
105104
return;
106-
this._connectBrowser(clientInfo);
105+
this._connectBrowser(clientName);
107106
debugLogger('Waiting for incoming extension connection');
108107
await Promise.race([
109108
this._extensionConnectionPromise,
@@ -114,14 +113,15 @@ export class CDPRelayServer {
114113
debugLogger('Extension connection established');
115114
}
116115

117-
private _connectBrowser(clientInfo: ClientInfo) {
116+
private _connectBrowser(clientName: string) {
118117
const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`;
119118
// Need to specify "key" in the manifest.json to make the id stable when loading from file.
120119
const url = new URL('chrome-extension://mmlmfjhmonkocbjadbfplnigmagldckm/connect.html');
121120
url.searchParams.set('mcpRelayUrl', mcpRelayEndpoint);
122121
const client = {
123-
name: 'Playwright Agent',
124-
version: require('../../../package.json').version,
122+
name: clientName,
123+
// Not used anymore.
124+
version: undefined,
125125
};
126126
url.searchParams.set('client', JSON.stringify(client));
127127
url.searchParams.set('protocolVersion', process.env.PWMCP_TEST_PROTOCOL_VERSION ?? protocol.VERSION.toString());

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@ import { debug } from '../../utilsBundle';
1919
import { createHttpServer, startHttpServer } from '../../server/utils/network';
2020
import { CDPRelayServer } from './cdpRelay';
2121

22-
import type { ClientInfo } from '../utils/mcp/server';
2322
import type { FullConfig } from './config';
2423

2524
const debugLogger = debug('pw:mcp:relay');
2625

27-
export async function createExtensionBrowser(config: FullConfig, clientInfo: ClientInfo): Promise<playwright.Browser> {
26+
export async function createExtensionBrowser(config: FullConfig, clientName: string): Promise<playwright.Browser> {
2827
const httpServer = createHttpServer();
2928
await startHttpServer(httpServer, {});
3029
const relay = new CDPRelayServer(
@@ -34,6 +33,6 @@ export async function createExtensionBrowser(config: FullConfig, clientInfo: Cli
3433
config.browser.launchOptions.executablePath);
3534
debugLogger(`CDP relay server started, extension endpoint: ${relay.extensionEndpoint()}.`);
3635

37-
await relay.ensureExtensionConnectionForMCPContext(clientInfo);
36+
await relay.ensureExtensionConnectionForMCPContext(clientName);
3837
return await playwright.chromium.connectOverCDP(relay.cdpEndpoint(), { isLocal: true });
3938
}

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { ProgramOption } from '../../utilsBundle';
1919
import * as mcpServer from '../utils/mcp/server';
2020
import { commaSeparatedList, dotenvFileLoader, enumParser, headerParser, numberParser, resolutionParser, resolveCLIConfigForMCP, semicolonSeparatedList } from './config';
2121
import { setupExitWatchdog } from './watchdog';
22-
import { createBrowser } from './browserFactory';
22+
import { createBrowser, createBrowserWithInfo } from './browserFactory';
2323
import { BrowserBackend } from '../backend/browserBackend';
2424
import { filteredTools } from '../backend/tools';
2525
import { testDebug } from './log';
@@ -114,17 +114,28 @@ export function decorateMCPCommand(command: Command) {
114114
const useSharedBrowser = config.sharedBrowserContext || config.browser.isolated;
115115
let sharedBrowser: playwright.Browser | undefined;
116116
let clientCount = 0;
117+
const clientNameCounters = new Map<string, number>();
117118

118119
const factory: mcpServer.ServerBackendFactory = {
119120
name: 'Playwright',
120121
nameInConfig: 'playwright',
121122
version,
122123
toolSchemas: tools.map(tool => tool.schema),
123124
create: async (clientInfo: ClientInfo) => {
124-
if (useSharedBrowser && clientCount === 0)
125-
sharedBrowser = await createBrowser(config, clientInfo);
125+
if (useSharedBrowser && clientCount === 0) {
126+
const { browser, canBind } = await createBrowserWithInfo(config, clientInfo);
127+
sharedBrowser = browser;
128+
if (canBind)
129+
await browser.bind(clientInfo.clientName, { workspaceDir: clientInfo.cwd });
130+
}
126131
clientCount++;
127-
const browser = sharedBrowser || await createBrowser(config, clientInfo);
132+
const { browser, canBind } = sharedBrowser ? { browser: sharedBrowser, canBind: false } : await createBrowserWithInfo(config, clientInfo);
133+
if (canBind) {
134+
const count = (clientNameCounters.get(clientInfo.clientName) ?? 0) + 1;
135+
clientNameCounters.set(clientInfo.clientName, count);
136+
const sessionName = count > 1 ? `${clientInfo.clientName} (${count})` : clientInfo.clientName;
137+
await browser.bind(sessionName, { workspaceDir: clientInfo.cwd });
138+
}
128139
const browserContext = config.browser.isolated ? await browser.newContext(config.browser.contextOptions) : browser.contexts()[0];
129140
return new BrowserBackend(config, browserContext, tools);
130141
},

packages/playwright-core/src/tools/trace/traceSnapshot.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ async function runCommandOnSnapshot(server: { url: string, stop: () => Promise<v
121121
outputMode: 'file',
122122
skillMode: true,
123123
}, context, browserTools);
124-
await backend.initialize({ cwd: process.cwd() });
124+
await backend.initialize({ cwd: process.cwd(), clientName: 'playwright-cli' });
125125

126126
try {
127127
if (!browserArgs.length)

packages/playwright-core/src/tools/utils/mcp/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const serverDebugResponse = debug('pw:mcp:server:response');
3434

3535
export type ClientInfo = {
3636
cwd: string;
37+
clientName: string;
3738
};
3839

3940
export type ProgressParams = { message?: string, progress?: number, total?: number };
@@ -158,6 +159,7 @@ const initializeServer = async (server: Server, factory: ServerBackendFactory, r
158159

159160
const clientInfo: ClientInfo = {
160161
cwd: firstRootPath(clientRoots),
162+
clientName: server.getClientVersion()?.name ?? 'Playwright MCP',
161163
};
162164

163165
const backend = await backendManager.createBackend(factory, clientInfo);

0 commit comments

Comments
 (0)