From e1a52ffab367f6aecb60acf8af7c56d5597a90e6 Mon Sep 17 00:00:00 2001
From: Pavel Feldman <pavel.feldman@gmail.com>
Date: Thu, 18 Dec 2025 11:15:38 -0800
Subject: [PATCH] feat(api): expose connectOverCDP({ isLocal })

---
 docs/src/api/class-browsertype.md             |  17 ++-
 packages/playwright-client/types/types.d.ts   |   6 +
 .../playwright-core/src/client/browserType.ts |   1 +
 .../playwright-core/src/protocol/validator.ts |   1 +
 .../playwright-core/src/server/browserType.ts |   2 +-
 .../src/server/chromium/chromium.ts           |   7 +-
 packages/playwright-core/types/types.d.ts     |   6 +
 .../mcp/extension/extensionContextFactory.ts  |   2 +-
 packages/playwright/src/mcp/program.ts        |  26 ----
 packages/playwright/src/mcp/sdk/exports.ts    |   1 -
 .../playwright/src/mcp/sdk/proxyBackend.ts    | 134 ------------------
 packages/protocol/src/channels.d.ts           |   2 +
 packages/protocol/src/protocol.yml            |   1 +
 .../library/chromium/connect-over-cdp.spec.ts |  16 +++
 tests/mcp/capabilities.spec.ts                |  32 -----
 15 files changed, 51 insertions(+), 203 deletions(-)
 delete mode 100644 packages/playwright/src/mcp/sdk/proxyBackend.ts

diff --git a/docs/src/api/class-browsertype.md b/docs/src/api/class-browsertype.md
index 1d658a5b5f915..ccb2af7cb72ef 100644
--- a/docs/src/api/class-browsertype.md
+++ b/docs/src/api/class-browsertype.md
@@ -211,12 +211,12 @@ A CDP websocket endpoint or http url to connect to. For example `http://localhos
 
 Additional HTTP headers to be sent with connect request. Optional.
 
-### option: BrowserType.connectOverCDP.slowMo
-* since: v1.11
-- `slowMo` <[float]>
+### option: BrowserType.connectOverCDP.isLocal
+* since: v1.58
+- `isLocal` <[boolean]>
 
-Slows down Playwright operations by the specified amount of milliseconds. Useful so that you
-can see what is going on. Defaults to 0.
+Tells Playwright that it runs on the same host as the CDP server. It will enable certain optimizations that rely upon
+the file system being the same between Playwright and the Browser.
 
 ### option: BrowserType.connectOverCDP.logger
 * since: v1.14
@@ -226,6 +226,13 @@ can see what is going on. Defaults to 0.
 
 Logger sink for Playwright logging. Optional.
 
+### option: BrowserType.connectOverCDP.slowMo
+* since: v1.11
+- `slowMo` <[float]>
+
+Slows down Playwright operations by the specified amount of milliseconds. Useful so that you
+can see what is going on. Defaults to 0.
+
 ### option: BrowserType.connectOverCDP.timeout
 * since: v1.11
 - `timeout` <[float]>
diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts
index 6788988908458..471274b115811 100644
--- a/packages/playwright-client/types/types.d.ts
+++ b/packages/playwright-client/types/types.d.ts
@@ -22046,6 +22046,12 @@ export interface ConnectOverCDPOptions {
    */
   headers?: { [key: string]: string; };
 
+  /**
+   * Tells Playwright that it runs on the same host as the CDP server. It will enable certain optimizations that rely
+   * upon the file system being the same between Playwright and the Browser.
+   */
+  isLocal?: boolean;
+
   /**
    * Logger sink for Playwright logging. Optional.
    * @deprecated The logs received by the logger are incomplete. Please use tracing instead.
diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts
index b13037d356197..45be8e2f20166 100644
--- a/packages/playwright-core/src/client/browserType.ts
+++ b/packages/playwright-core/src/client/browserType.ts
@@ -201,6 +201,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
       headers,
       slowMo: params.slowMo,
       timeout: new TimeoutSettings(this._platform).timeout(params),
+      isLocal: params.isLocal,
     });
     const browser = Browser.from(result.browser);
     browser._connectToBrowserType(this, {}, params.logger);
diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts
index d75cb62722b0c..f54c9a256384e 100644
--- a/packages/playwright-core/src/protocol/validator.ts
+++ b/packages/playwright-core/src/protocol/validator.ts
@@ -623,6 +623,7 @@ scheme.BrowserTypeConnectOverCDPParams = tObject({
   headers: tOptional(tArray(tType('NameValue'))),
   slowMo: tOptional(tFloat),
   timeout: tFloat,
+  isLocal: tOptional(tBoolean),
 });
 scheme.BrowserTypeConnectOverCDPResult = tObject({
   browser: tChannel(['Browser']),
diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts
index b4f843f22ae93..8bb1fb4bb23a2 100644
--- a/packages/playwright-core/src/server/browserType.ts
+++ b/packages/playwright-core/src/server/browserType.ts
@@ -284,7 +284,7 @@ export abstract class BrowserType extends SdkObject {
       await fs.promises.mkdir(options.tracesDir, { recursive: true });
   }
 
-  async connectOverCDP(progress: Progress, endpointURL: string, options: { slowMo?: number, timeout?: number, headers?: types.HeadersArray }): Promise<Browser> {
+  async connectOverCDP(progress: Progress, endpointURL: string, options: { slowMo?: number, timeout?: number, headers?: types.HeadersArray, isLocal?: boolean }): Promise<Browser> {
     throw new Error('CDP connections are only supported by Chromium');
   }
 
diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts
index d782868fd5a64..ceca33eaab797 100644
--- a/packages/playwright-core/src/server/chromium/chromium.ts
+++ b/packages/playwright-core/src/server/chromium/chromium.ts
@@ -76,11 +76,11 @@ export class Chromium extends BrowserType {
     return super.launchPersistentContext(progress, userDataDir, options);
   }
 
-  override async connectOverCDP(progress: Progress, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray }) {
+  override async connectOverCDP(progress: Progress, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray, isLocal?: boolean }) {
     return await this._connectOverCDPInternal(progress, endpointURL, options);
   }
 
-  async _connectOverCDPInternal(progress: Progress, endpointURL: string, options: types.LaunchOptions & { headers?: types.HeadersArray }, onClose?: () => Promise<void>) {
+  async _connectOverCDPInternal(progress: Progress, endpointURL: string, options: types.LaunchOptions & { headers?: types.HeadersArray, isLocal?: boolean }, onClose?: () => Promise<void>) {
     let headersMap: { [key: string]: string; } | undefined;
     if (options.headers)
       headersMap = headersArrayToObject(options.headers, false);
@@ -125,7 +125,8 @@ export class Chromium extends BrowserType {
       };
       validateBrowserContextOptions(persistent, browserOptions);
       const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions));
-      browser._isCollocatedWithServer = false;
+      if (!options.isLocal)
+        browser._isCollocatedWithServer = false;
       browser.on(Browser.Events.Disconnected, doCleanup);
       return browser;
     } catch (error) {
diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts
index 6788988908458..471274b115811 100644
--- a/packages/playwright-core/types/types.d.ts
+++ b/packages/playwright-core/types/types.d.ts
@@ -22046,6 +22046,12 @@ export interface ConnectOverCDPOptions {
    */
   headers?: { [key: string]: string; };
 
+  /**
+   * Tells Playwright that it runs on the same host as the CDP server. It will enable certain optimizations that rely
+   * upon the file system being the same between Playwright and the Browser.
+   */
+  isLocal?: boolean;
+
   /**
    * Logger sink for Playwright logging. Optional.
    * @deprecated The logs received by the logger are incomplete. Please use tracing instead.
diff --git a/packages/playwright/src/mcp/extension/extensionContextFactory.ts b/packages/playwright/src/mcp/extension/extensionContextFactory.ts
index c59cff05c9376..a3b9d54e94c22 100644
--- a/packages/playwright/src/mcp/extension/extensionContextFactory.ts
+++ b/packages/playwright/src/mcp/extension/extensionContextFactory.ts
@@ -50,7 +50,7 @@ export class ExtensionContextFactory implements BrowserContextFactory {
   private async _obtainBrowser(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<playwright.Browser> {
     const relay = await this._startRelay(abortSignal);
     await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal, toolName);
-    return await playwright.chromium.connectOverCDP(relay.cdpEndpoint());
+    return await playwright.chromium.connectOverCDP(relay.cdpEndpoint(), { isLocal: true });
   }
 
   private async _startRelay(abortSignal: AbortSignal) {
diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts
index 1c0389dcfb7eb..d26c6fa8032fe 100644
--- a/packages/playwright/src/mcp/program.ts
+++ b/packages/playwright/src/mcp/program.ts
@@ -25,12 +25,10 @@ import * as mcpServer from './sdk/server';
 import { commaSeparatedList, dotenvFileLoader, enumParser, headerParser, numberParser, resolutionParser, resolveCLIConfig, semicolonSeparatedList } from './browser/config';
 import { setupExitWatchdog } from './browser/watchdog';
 import { contextFactory } from './browser/browserContextFactory';
-import { ProxyBackend } from './sdk/proxyBackend';
 import { BrowserServerBackend } from './browser/browserServerBackend';
 import { ExtensionContextFactory } from './extension/extensionContextFactory';
 
 import type { Command } from 'playwright-core/lib/utilsBundle';
-import type { MCPProvider } from './sdk/proxyBackend';
 
 export function decorateCommand(command: Command, version: string) {
   command
@@ -73,7 +71,6 @@ export function decorateCommand(command: Command, version: string) {
       .option('--user-agent <ua string>', 'specify user agent string')
       .option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
       .option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280x720"', resolutionParser.bind(null, '--viewport-size'))
-      .addOption(new ProgramOption('--connect-tool', 'Allow to switch between different browser connection methods.').hideHelp())
       .addOption(new ProgramOption('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
       .action(async options => {
         setupExitWatchdog();
@@ -108,29 +105,6 @@ export function decorateCommand(command: Command, version: string) {
           return;
         }
 
-        if (options.connectTool) {
-          const providers: MCPProvider[] = [
-            {
-              name: 'default',
-              description: 'Starts standalone browser',
-              connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, browserContextFactory)),
-            },
-            {
-              name: 'extension',
-              description: 'Connect to a browser using the Playwright MCP extension',
-              connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, extensionContextFactory)),
-            },
-          ];
-          const factory: mcpServer.ServerBackendFactory = {
-            name: 'Playwright w/ switch',
-            nameInConfig: 'playwright-switch',
-            version,
-            create: () => new ProxyBackend(providers),
-          };
-          await mcpServer.start(factory, config.server);
-          return;
-        }
-
         const factory: mcpServer.ServerBackendFactory = {
           name: 'Playwright',
           nameInConfig: 'playwright',
diff --git a/packages/playwright/src/mcp/sdk/exports.ts b/packages/playwright/src/mcp/sdk/exports.ts
index b30e94b9be501..939b3c230817e 100644
--- a/packages/playwright/src/mcp/sdk/exports.ts
+++ b/packages/playwright/src/mcp/sdk/exports.ts
@@ -15,7 +15,6 @@
  */
 
 export * from './inProcessTransport';
-export * from './proxyBackend';
 export * from './server';
 export * from './tool';
 export * from './http';
diff --git a/packages/playwright/src/mcp/sdk/proxyBackend.ts b/packages/playwright/src/mcp/sdk/proxyBackend.ts
deleted file mode 100644
index 46e1d061f30e8..0000000000000
--- a/packages/playwright/src/mcp/sdk/proxyBackend.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-/**
- * 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 { debug } from 'playwright-core/lib/utilsBundle';
-import * as mcpBundle from 'playwright-core/lib/mcpBundle';
-
-import type { ServerBackend, ClientInfo } from './server';
-import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
-import type { Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
-import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
-
-export type MCPProvider = {
-  name: string;
-  description: string;
-  connect(): Transport;
-};
-
-const errorsDebug = debug('pw:mcp:errors');
-const { z, zodToJsonSchema } = mcpBundle;
-
-export class ProxyBackend implements ServerBackend {
-  private _mcpProviders: MCPProvider[];
-  private _currentClient: Client | undefined;
-  private _contextSwitchTool: Tool;
-  private _clientInfo: ClientInfo | undefined;
-
-  constructor(mcpProviders: MCPProvider[]) {
-    this._mcpProviders = mcpProviders;
-    this._contextSwitchTool = this._defineContextSwitchTool();
-  }
-
-  async initialize(clientInfo: ClientInfo): Promise<void> {
-    this._clientInfo = clientInfo;
-  }
-
-  async listTools(): Promise<Tool[]> {
-    const currentClient = await this._ensureCurrentClient();
-    const response = await currentClient.listTools();
-    if (this._mcpProviders.length === 1)
-      return response.tools;
-    return [
-      ...response.tools,
-      this._contextSwitchTool,
-    ];
-  }
-
-  async callTool(name: string, args: CallToolRequest['params']['arguments']): Promise<CallToolResult> {
-    if (name === this._contextSwitchTool.name)
-      return this._callContextSwitchTool(args);
-    const currentClient = await this._ensureCurrentClient();
-    return await currentClient.callTool({
-      name,
-      arguments: args,
-    }) as CallToolResult;
-  }
-
-  serverClosed?(): void {
-    void this._currentClient?.close().catch(errorsDebug);
-  }
-
-  private async _callContextSwitchTool(params: any): Promise<CallToolResult> {
-    try {
-      const factory = this._mcpProviders.find(factory => factory.name === params.name);
-      if (!factory)
-        throw new Error('Unknown connection method: ' + params.name);
-
-      await this._setCurrentClient(factory);
-      return {
-        content: [{ type: 'text', text: '### Result\nSuccessfully changed connection method.\n' }],
-      };
-    } catch (error) {
-      return {
-        content: [{ type: 'text', text: `### Result\nError: ${error}\n` }],
-        isError: true,
-      };
-    }
-  }
-
-  private _defineContextSwitchTool(): Tool {
-    return {
-      name: 'browser_connect',
-      description: [
-        'Connect to a browser using one of the available methods:',
-        ...this._mcpProviders.map(factory => `- "${factory.name}": ${factory.description}`),
-      ].join('\n'),
-      inputSchema: zodToJsonSchema(z.object({
-        name: z.enum(this._mcpProviders.map(factory => factory.name) as [string, ...string[]]).default(this._mcpProviders[0].name).describe('The method to use to connect to the browser'),
-      }), { strictUnions: true }) as Tool['inputSchema'],
-      annotations: {
-        title: 'Connect to a browser context',
-        readOnlyHint: true,
-        openWorldHint: false,
-      },
-    };
-  }
-
-  private async _ensureCurrentClient(): Promise<Client> {
-    if (this._currentClient)
-      return this._currentClient;
-    return await this._setCurrentClient(this._mcpProviders[0]);
-  }
-
-  private async _setCurrentClient(factory: MCPProvider) {
-    await this._currentClient?.close();
-    this._currentClient = undefined;
-
-    const client = new mcpBundle.Client({ name: 'Playwright MCP Proxy', version: '0.0.0' });
-    client.registerCapabilities({
-      roots: {
-        listRoots: true,
-      },
-    });
-    client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._clientInfo?.roots || [] }));
-    client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
-
-    const transport = factory.connect();
-    await client.connect(transport);
-    this._currentClient = client;
-    return client;
-  }
-}
diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts
index 190e39174e32c..c17f6022c8770 100644
--- a/packages/protocol/src/channels.d.ts
+++ b/packages/protocol/src/channels.d.ts
@@ -1119,10 +1119,12 @@ export type BrowserTypeConnectOverCDPParams = {
   headers?: NameValue[],
   slowMo?: number,
   timeout: number,
+  isLocal?: boolean,
 };
 export type BrowserTypeConnectOverCDPOptions = {
   headers?: NameValue[],
   slowMo?: number,
+  isLocal?: boolean,
 };
 export type BrowserTypeConnectOverCDPResult = {
   browser: BrowserChannel,
diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml
index e2a194629d6f2..c6bd21db330ff 100644
--- a/packages/protocol/src/protocol.yml
+++ b/packages/protocol/src/protocol.yml
@@ -1007,6 +1007,7 @@ BrowserType:
           items: NameValue
         slowMo: float?
         timeout: float
+        isLocal: boolean?
       returns:
         browser: Browser
         defaultContext: BrowserContext?
diff --git a/tests/library/chromium/connect-over-cdp.spec.ts b/tests/library/chromium/connect-over-cdp.spec.ts
index b5347e2ab74bf..ad0a3c2948dc3 100644
--- a/tests/library/chromium/connect-over-cdp.spec.ts
+++ b/tests/library/chromium/connect-over-cdp.spec.ts
@@ -553,6 +553,22 @@ test('setInputFiles should preserve lastModified timestamp', async ({ browserTyp
   }
 });
 
+test('setInputFiles should use local path when isLocal is set', async ({ browserType, toImpl }) => {
+  const port = 9339 + test.info().workerIndex;
+  const browserServer = await browserType.launch({
+    args: ['--remote-debugging-port=' + port]
+  });
+  try {
+    const cdpBrowser1 = await browserType.connectOverCDP(`http://127.0.0.1:${port}/`);
+    expect(toImpl(cdpBrowser1)._isCollocatedWithServer).toBe(false);
+
+    const cdpBrowser2 = await browserType.connectOverCDP(`http://127.0.0.1:${port}/`, { isLocal: true });
+    expect(toImpl(cdpBrowser2)._isCollocatedWithServer).toBe(true);
+  } finally {
+    await browserServer.close();
+  }
+});
+
 test('should print custom ws close error', async ({ browserType, server }) => {
   server.onceWebSocketConnection((ws, request) => {
     ws.on('message', message => {
diff --git a/tests/mcp/capabilities.spec.ts b/tests/mcp/capabilities.spec.ts
index f7bc620a7842c..b5272a0ea395a 100644
--- a/tests/mcp/capabilities.spec.ts
+++ b/tests/mcp/capabilities.spec.ts
@@ -44,38 +44,6 @@ test('test snapshot tool list', async ({ client }) => {
   ]));
 });
 
-test('test tool list proxy mode', async ({ startClient }) => {
-  const { client } = await startClient({
-    args: ['--connect-tool'],
-  });
-  const { tools } = await client.listTools();
-  expect(new Set(tools.map(t => t.name))).toEqual(new Set([
-    'browser_click',
-    'browser_connect', // the extra tool
-    'browser_console_messages',
-    'browser_drag',
-    'browser_evaluate',
-    'browser_file_upload',
-    'browser_fill_form',
-    'browser_handle_dialog',
-    'browser_hover',
-    'browser_select_option',
-    'browser_type',
-    'browser_close',
-    'browser_install',
-    'browser_navigate_back',
-    'browser_navigate',
-    'browser_network_requests',
-    'browser_press_key',
-    'browser_resize',
-    'browser_run_code',
-    'browser_snapshot',
-    'browser_tabs',
-    'browser_take_screenshot',
-    'browser_wait_for',
-  ]));
-});
-
 test('test capabilities (pdf)', async ({ startClient }) => {
   const { client } = await startClient({
     args: ['--caps=pdf'],
