From f9898c858396ffa4ee1f62e96c8cfec45e9a66fa Mon Sep 17 00:00:00 2001
From: Yury Semikhatsky <yurys@chromium.org>
Date: Mon, 8 Jun 2026 15:18:22 -0700
Subject: [PATCH 1/2] fix(cli): support attaching to a Playwright browser
 server endpoint

`playwright-cli attach --endpoint=ws://...` failed two ways: the client
reported "no target specified" because the endpoint was missing from the
target chain, and the daemon threw "unable to connect to a browser that
does not have any contexts" since a launchServer browser exposes no
contexts until one is created.

Fixes https://github.com/microsoft/playwright/issues/41154
---
 .../src/tools/cli-client/program.ts           |  2 +-
 .../src/tools/cli-daemon/program.ts           |  4 +--
 tests/mcp/cli-endpoint.spec.ts                | 34 +++++++++++++++++++
 3 files changed, 36 insertions(+), 4 deletions(-)
 create mode 100644 tests/mcp/cli-endpoint.spec.ts

diff --git a/packages/playwright-core/src/tools/cli-client/program.ts b/packages/playwright-core/src/tools/cli-client/program.ts
index 1d9dc5d657c2d..0b87a30bae33a 100644
--- a/packages/playwright-core/src/tools/cli-client/program.ts
+++ b/packages/playwright-core/src/tools/cli-client/program.ts
@@ -168,7 +168,7 @@ export async function program(options?: { embedderVersion?: string}) {
       }
 
       const cdpChannel = typeof args.cdp === 'string' && isKnownChannel(args.cdp) ? args.cdp : undefined;
-      const targetName = attachTarget ?? cdpChannel ?? extensionChannel ?? args.cdp as string;
+      const targetName = attachTarget ?? cdpChannel ?? extensionChannel ?? args.endpoint as string ?? args.cdp as string;
       if (!targetName)
         output.errorAttachNoTarget();
       const attachSessionName = explicitSessionName(args.session as string) ?? attachTarget ?? cdpChannel ?? extensionChannel ?? sessionName;
diff --git a/packages/playwright-core/src/tools/cli-daemon/program.ts b/packages/playwright-core/src/tools/cli-daemon/program.ts
index 1d8877330b8ad..a2183c5695f8b 100644
--- a/packages/playwright-core/src/tools/cli-daemon/program.ts
+++ b/packages/playwright-core/src/tools/cli-daemon/program.ts
@@ -62,9 +62,7 @@ export function decorateProgram(program: Command) {
           const { browser, browserInfo, canBind, ownership } = await createBrowserWithInfo(mcpConfig, mcpClientInfo, options);
           if (canBind)
             await browser.bind(sessionName, { workspaceDir: clientInfo.workspaceDir });
-          const browserContext = mcpConfig.browser.isolated ? await browser.newContext(mcpConfig.browser.contextOptions) : browser.contexts()[0];
-          if (!browserContext)
-            throw new Error('Error: unable to connect to a browser that does not have any contexts');
+          const browserContext = browser.contexts()[0] ?? await browser.newContext(mcpConfig.browser.contextOptions);
           const persistent = options.persistent || options.profile || mcpConfig.browser.userDataDir ? true : undefined;
           const socketPath = await startCliDaemonServer(sessionName, browserContext, browserInfo, mcpConfig, clientInfo, mcpClientInfo, { persistent, exitOnClose: true, ownership });
           console.log(`Daemon listening on ${socketPath}\n`);
diff --git a/tests/mcp/cli-endpoint.spec.ts b/tests/mcp/cli-endpoint.spec.ts
new file mode 100644
index 0000000000000..b6e18ac1951ec
--- /dev/null
+++ b/tests/mcp/cli-endpoint.spec.ts
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { test, expect } from './cli-fixtures';
+
+test('attach --endpoint keeps the default session', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41154' } }, async ({ wsEndpoint, cli }) => {
+  const { exitCode, error } = await cli('attach', `--endpoint=${wsEndpoint}`);
+  expect(error).not.toContain('no target specified');
+  expect(exitCode).toBe(0);
+
+  const { output: listOutput } = await cli('list');
+  expect(listOutput).toContain('- default:');
+  expect(listOutput).toContain('(attached)');
+});
+
+test('attach via --endpoint can snapshot the connected browser', async ({ wsEndpoint, cli, server }) => {
+  await cli('attach', `--endpoint=${wsEndpoint}`);
+  await cli('goto', server.HELLO_WORLD);
+  const { inlineSnapshot } = await cli('snapshot');
+  expect(inlineSnapshot).toContain('Hello, world!');
+});

From e70b447f1d9472560c4b88b1d8f0960312ad18a3 Mon Sep 17 00:00:00 2001
From: Yury Semikhatsky <yurys@chromium.org>
Date: Tue, 9 Jun 2026 10:29:14 -0700
Subject: [PATCH 2/2] fix(cli): create context only when attaching to a raw
 server endpoint

Scope context creation to the raw `--endpoint` branch in createRemoteBrowser
so that attaching to a bound browser server without contexts still fails, as
before. Merge the two endpoint tests per review.
---
 packages/playwright-core/src/tools/cli-daemon/program.ts | 4 +++-
 packages/playwright-core/src/tools/mcp/browserFactory.ts | 4 ++++
 tests/mcp/cli-endpoint.spec.ts                           | 5 +----
 3 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/packages/playwright-core/src/tools/cli-daemon/program.ts b/packages/playwright-core/src/tools/cli-daemon/program.ts
index a2183c5695f8b..1d8877330b8ad 100644
--- a/packages/playwright-core/src/tools/cli-daemon/program.ts
+++ b/packages/playwright-core/src/tools/cli-daemon/program.ts
@@ -62,7 +62,9 @@ export function decorateProgram(program: Command) {
           const { browser, browserInfo, canBind, ownership } = await createBrowserWithInfo(mcpConfig, mcpClientInfo, options);
           if (canBind)
             await browser.bind(sessionName, { workspaceDir: clientInfo.workspaceDir });
-          const browserContext = browser.contexts()[0] ?? await browser.newContext(mcpConfig.browser.contextOptions);
+          const browserContext = mcpConfig.browser.isolated ? await browser.newContext(mcpConfig.browser.contextOptions) : browser.contexts()[0];
+          if (!browserContext)
+            throw new Error('Error: unable to connect to a browser that does not have any contexts');
           const persistent = options.persistent || options.profile || mcpConfig.browser.userDataDir ? true : undefined;
           const socketPath = await startCliDaemonServer(sessionName, browserContext, browserInfo, mcpConfig, clientInfo, mcpClientInfo, { persistent, exitOnClose: true, ownership });
           console.log(`Daemon listening on ${socketPath}\n`);
diff --git a/packages/playwright-core/src/tools/mcp/browserFactory.ts b/packages/playwright-core/src/tools/mcp/browserFactory.ts
index e6b808459c5ac..70cf0878e55d7 100644
--- a/packages/playwright-core/src/tools/mcp/browserFactory.ts
+++ b/packages/playwright-core/src/tools/mcp/browserFactory.ts
@@ -147,6 +147,10 @@ async function createRemoteBrowser(config: FullConfig): Promise<BrowserWithInfo>
   // Use connectToBrowser instead of playwright[browserName].connect because we don't have browserName.
   const browser = await connectToBrowser(playwrightObject, remoteOptions);
   browser._connectToBrowserType(playwrightObject[browser._browserName], {}, undefined);
+  // A browser started via `launchServer` exposes no contexts until one is
+  // created, so create one when attaching to such a server.
+  if (!browser.contexts().length)
+    await browser.newContext(config.browser.contextOptions);
   return { browser, browserInfo: browserInfo(browser, config), canBind: false, ownership: 'attached' };
 }
 
diff --git a/tests/mcp/cli-endpoint.spec.ts b/tests/mcp/cli-endpoint.spec.ts
index b6e18ac1951ec..0fa4519e32076 100644
--- a/tests/mcp/cli-endpoint.spec.ts
+++ b/tests/mcp/cli-endpoint.spec.ts
@@ -16,7 +16,7 @@
 
 import { test, expect } from './cli-fixtures';
 
-test('attach --endpoint keeps the default session', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41154' } }, async ({ wsEndpoint, cli }) => {
+test('attach --endpoint keeps the default session', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41154' } }, async ({ wsEndpoint, cli, server }) => {
   const { exitCode, error } = await cli('attach', `--endpoint=${wsEndpoint}`);
   expect(error).not.toContain('no target specified');
   expect(exitCode).toBe(0);
@@ -24,10 +24,7 @@ test('attach --endpoint keeps the default session', { annotation: { type: 'issue
   const { output: listOutput } = await cli('list');
   expect(listOutput).toContain('- default:');
   expect(listOutput).toContain('(attached)');
-});
 
-test('attach via --endpoint can snapshot the connected browser', async ({ wsEndpoint, cli, server }) => {
-  await cli('attach', `--endpoint=${wsEndpoint}`);
   await cli('goto', server.HELLO_WORLD);
   const { inlineSnapshot } = await cli('snapshot');
   expect(inlineSnapshot).toContain('Hello, world!');
