feat(browser): add CDP remote connection support for server environments (#52)
* feat(browser): add CDP remote connection support for server environments This feature enables OpenCLI to connect to a Chrome browser running on a different machine (e.g., your local computer) from a headless server environment via Chrome DevTools Protocol (CDP). Server environments (CI, cloud VMs, headless Linux) cannot run Chrome with a GUI or install the Playwright MCP Bridge extension. This makes it impossible to use OpenCLI commands that require browser authentication. Add support for the `OPENCLI_CDP_ENDPOINT` environment variable, which tells OpenCLI to connect to a remote Chrome instance via CDP instead of using the local extension mode. 1. Start Chrome with remote debugging on local machine: ``` chrome --remote-debugging-port=9222 --user-data-dir="$HOME/chrome-debug" ``` 2. Create SSH tunnel to forward port to server: ``` ssh -R 9222:localhost:9222 your-server ``` 3. Run OpenCLI on server: ``` export OPENCLI_CDP_ENDPOINT="http://localhost:9222" opencli bilibili hot --limit 5 ``` - src/browser.ts: Add CDP endpoint detection in buildMcpArgs() - src/doctor.ts: Show CDP mode status in doctor report - README.md: Add "Remote Chrome (Server/Headless)" section - README.zh-CN.md: Add corresponding Chinese documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: extract CDP connection guide into separate files * docs: clarify CDP vs SSH/Proxy distinction in CDP guides * docs: restructure CDP guides into 3 distinct phases (preparation, tunnel, execution) --------- Co-authored-by: ByteYue <yj976240184@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
9 files changed
+328
-9
No files matched your search
@@ -0,0 +1,103 @@
|
||||
# Connecting OpenCLI via CDP (Remote/Headless Servers)
|
||||
|
||||
If you cannot use the Playwright MCP Bridge extension (e.g., in a remote headless server environment without a UI), OpenCLI provides an alternative: connecting directly to Chrome via **CDP (Chrome DevTools Protocol)**.
|
||||
|
||||
Because CDP binds to `localhost` by default for security reasons, accessing it from a remote server requires an additional networking tunnel.
|
||||
|
||||
This guide is broken down into three phases:
|
||||
1. **Preparation**: Start Chrome with CDP enabled locally.
|
||||
2. **Network Tunnels**: Expose that CDP port to your remote server using either **SSH Tunnels** or **Reverse Proxies**.
|
||||
3. **Execution**: Run OpenCLI on your server.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Preparation (Local Machine)
|
||||
|
||||
First, you need to start a Chrome browser on your local machine with remote debugging enabled.
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
|
||||
--remote-debugging-port=9222 \
|
||||
--user-data-dir="$HOME/chrome-debug-profile" \
|
||||
--remote-allow-origins="*"
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
google-chrome \
|
||||
--remote-debugging-port=9222 \
|
||||
--user-data-dir="$HOME/chrome-debug-profile" \
|
||||
--remote-allow-origins="*"
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```cmd
|
||||
"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
|
||||
--remote-debugging-port=9222 ^
|
||||
--user-data-dir="%USERPROFILE%\chrome-debug-profile" ^
|
||||
--remote-allow-origins="*"
|
||||
```
|
||||
|
||||
> **Note**: The `--remote-allow-origins="*"` flag is often required for modern Chrome versions to accept cross-origin CDP WebSocket connections (e.g. from reverse proxies like ngrok).
|
||||
|
||||
Once this browser instance opens, **log into the target websites you want to use** (e.g., bilibili.com, zhihu.com) so that the session contains the correct cookies.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Remote Access Methods
|
||||
|
||||
Once CDP is running locally on port `9222`, you must securely expose this port to your remote server. Choose one of the two methods below depending on your network conditions.
|
||||
|
||||
### Method A: SSH Tunnel (Recommended)
|
||||
|
||||
If your local machine has SSH access to the remote server, this is the most secure and straightforward method.
|
||||
|
||||
Run this command on your **Local Machine** to forward the remote server's port `9222` back to your local port `9222`:
|
||||
|
||||
```bash
|
||||
ssh -R 9222:localhost:9222 your-server-user@your-server-ip
|
||||
```
|
||||
|
||||
Leave this SSH session running in the background.
|
||||
|
||||
### Method B: Reverse Proxy (ngrok / frp / socat)
|
||||
|
||||
If you cannot establish a direct SSH connection (e.g., due to NAT or firewalls), you can use an intranet penetration tool like `ngrok`.
|
||||
|
||||
Run this command on your **Local Machine** to expose your local port `9222` to the public internet securely via ngrok:
|
||||
|
||||
```bash
|
||||
ngrok http 9222
|
||||
```
|
||||
|
||||
This will print a forwarding URL, such as `https://abcdef.ngrok.app`. **Copy this URL**.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Execution (Remote Server)
|
||||
|
||||
Now switch to your **Remote Server** where OpenCLI is installed.
|
||||
|
||||
Depending on the network tunnel method you chose in Phase 2, set the `OPENCLI_CDP_ENDPOINT` environment variable and run your commands.
|
||||
|
||||
### If you used Method A (SSH Tunnel):
|
||||
|
||||
```bash
|
||||
export OPENCLI_CDP_ENDPOINT="http://localhost:9222"
|
||||
opencli doctor # Verify connection
|
||||
opencli bilibili hot --limit 5 # Test a command
|
||||
```
|
||||
|
||||
### If you used Method B (Reverse Proxy like ngrok):
|
||||
|
||||
```bash
|
||||
# Use the URL you copied from ngrok earlier
|
||||
export OPENCLI_CDP_ENDPOINT="https://abcdef.ngrok.app"
|
||||
opencli doctor # Verify connection
|
||||
opencli bilibili hot --limit 5 # Test a command
|
||||
```
|
||||
|
||||
> *Tip: OpenCLI automatically requests the `/json/version` HTTP endpoint to discover the underlying WebSocket URL if you provide a standard HTTP/HTTPS address.*
|
||||
|
||||
If you plan to use this setup frequently, you can persist the environment variable by adding the `export` line to your `~/.bashrc` or `~/.zshrc` on the server.
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
# 通过 CDP 远程连接 OpenCLI (服务器/无头环境)
|
||||
|
||||
如果你无法使用 Playwright MCP Bridge 浏览器扩展(例如:在无界面的远程服务器上运行 OpenCLI 时),OpenCLI 提供了备选方案:通过连接 **CDP (Chrome DevTools Protocol,即 Chrome 开发者工具协议)** 来直接控制本地 Chrome。
|
||||
|
||||
出于安全考虑,CDP 默认仅绑定在 `localhost` 的本地端口。所以,若是想让**远程服务器**调用本地的 CDP 服务,我们需要依靠一层额外的网络隧道。
|
||||
|
||||
本指南将整个过程拆分为三个阶段:
|
||||
1. **阶段一:准备工作**(在本地启动允许 CDP 调试的 Chrome)。
|
||||
2. **阶段二:建立网络隧道**(通过 **SSH反向隧道** 或 **反向代理工具**,将本地的 CDP 端口暴露给服务器)。
|
||||
3. **阶段三:执行命令**(在服务器端运行 OpenCLI)。
|
||||
|
||||
---
|
||||
|
||||
## 阶段一:准备工作 (本地电脑)
|
||||
|
||||
首先,你需要在你的本地电脑上,通过命令行参数启动一个开启了远程调试端口的 Chrome 实例。
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
|
||||
--remote-debugging-port=9222 \
|
||||
--user-data-dir="$HOME/chrome-debug-profile" \
|
||||
--remote-allow-origins="*"
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
google-chrome \
|
||||
--remote-debugging-port=9222 \
|
||||
--user-data-dir="$HOME/chrome-debug-profile" \
|
||||
--remote-allow-origins="*"
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```cmd
|
||||
"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
|
||||
--remote-debugging-port=9222 ^
|
||||
--user-data-dir="%USERPROFILE%\chrome-debug-profile" ^
|
||||
--remote-allow-origins="*"
|
||||
```
|
||||
|
||||
> **注意**:此处增加的 `--remote-allow-origins="*"` 参数对于较新版本的 Chrome 来说通常是[必需的],以允许来自反向代理(如 ngrok)的跨域 WebSocket 连接请求。
|
||||
|
||||
待这个新的浏览器实例打开后,**手工登录那些你打算使用的网站**(如 bilibili.com、zhihu.com 等),这可以让该浏览器的运行资料(Profile)保留上这些网站登录用的 Cookie。
|
||||
|
||||
---
|
||||
|
||||
## 阶段二:建立网络隧道
|
||||
|
||||
现在你的本地已经有了一个监听在 `9222` 端口的 CDP 服务,接下来,选择以下任意一种方式将其实际暴露给你的远端服务器。
|
||||
|
||||
### 方法 A:SSH 反向端口转发 (推荐)
|
||||
|
||||
如果你的本地电脑可以直连远程服务器的 SSH,那么这是最简单且最安全的做法。
|
||||
|
||||
在你的 **本地电脑** 终端上直接运行这条 ssh 命令,将远程服务器的 `9222` 端口反向映射回本地的 `9222` 端口:
|
||||
|
||||
```bash
|
||||
ssh -R 9222:localhost:9222 your-server-user@your-server-ip
|
||||
```
|
||||
|
||||
保持此 SSH 会话在后台运行即可。
|
||||
|
||||
### 方法 B:反向代理 / 内网穿透 (ngrok / frp / socat)
|
||||
|
||||
如果因为 NAT 或防火墙等因素导致无法直连 SSH 服务器,你可以使用 `ngrok` 等内网穿透工具。
|
||||
|
||||
在 **本地电脑** 运行 ngrok 将本地的 `9222` 端口暴露到公网:
|
||||
|
||||
```bash
|
||||
ngrok http 9222
|
||||
```
|
||||
|
||||
此时终端里会打印出一段专属的转发 URL 地址(如:`https://abcdef.ngrok.app`)。**复制这一段 URL 地址备用**。
|
||||
|
||||
---
|
||||
|
||||
## 阶段三:执行命令 (远程服务器)
|
||||
|
||||
现在,所有的准备工作已结束。请切换到你已安装好 OpenCLI 的 **远程服务器** 终端上。
|
||||
|
||||
根据你在上方阶段二所选择的隧道方案,在终端中配置对应的 `OPENCLI_CDP_ENDPOINT` 环境变量:
|
||||
|
||||
### 若使用 方法 A (SSH 反向隧道):
|
||||
|
||||
```bash
|
||||
export OPENCLI_CDP_ENDPOINT="http://localhost:9222"
|
||||
opencli doctor # 查看并验证连接是否通畅
|
||||
opencli bilibili hot --limit 5 # 执行目标命令
|
||||
```
|
||||
|
||||
### 若使用 方法 B (Ngrok 等反向代理):
|
||||
|
||||
```bash
|
||||
# 将刚刚使用 ngrok 得到的地址填入这里
|
||||
export OPENCLI_CDP_ENDPOINT="https://abcdef.ngrok.app"
|
||||
opencli doctor # 查看并验证连接是否通畅
|
||||
opencli bilibili hot --limit 5 # 执行目标命令
|
||||
```
|
||||
|
||||
> *Tip: 如果你填写的是一个普通 HTTP/HTTPS 的 URL 地址,OpenCLI 会自动尝试抓取该地址下的 `/json/version` 节点,来动态解析并连接真正底层依赖的 WebSocket 地址。*
|
||||
|
||||
如果你想在此服务器上永久启用该配置,可以将对应的 `export` 语句追加进入你的 `~/.bashrc` 或 `~/.zshrc` 配置文件中。
|
||||
@@ -21,6 +21,7 @@ A CLI tool that turns **any website** into a command-line interface — Bilibili
|
||||
- [Built-in Commands](#built-in-commands)
|
||||
- [Output Formats](#output-formats)
|
||||
- [For AI Agents (Developer Guide)](#for-ai-agents-developer-guide)
|
||||
- [Remote Chrome (Server/Headless)](#remote-chrome-serverheadless)
|
||||
- [Testing](#testing)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Releasing New Versions](#releasing-new-versions)
|
||||
@@ -69,6 +70,9 @@ The interactive TUI will:
|
||||
> opencli doctor --fix -y # Fix all configs non-interactively
|
||||
> ```
|
||||
|
||||
**Alternative: CDP Mode (For Servers/Headless)**
|
||||
If you cannot install the browser extension (e.g. running OpenCLI on a remote headless server), you can connect OpenCLI to your local Chrome via CDP using SSH tunnels or reverse proxies. See the [CDP Connection Guide](./CDP.md) for detailed instructions.
|
||||
|
||||
<details>
|
||||
<summary>Manual setup (alternative)</summary>
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ OpenCLI 将任何网站变成命令行工具 — B站、知乎、小红书、Twi
|
||||
- [内置命令](#内置命令)
|
||||
- [输出格式](#输出格式)
|
||||
- [致 AI Agent(开发者指南)](#致-ai-agent开发者指南)
|
||||
- [远程 Chrome(服务器/无头环境)](#远程-chrome服务器无头环境)
|
||||
- [常见问题排查](#常见问题排查)
|
||||
- [版本发布](#版本发布)
|
||||
- [License](#license)
|
||||
@@ -68,6 +69,9 @@ opencli setup
|
||||
> opencli doctor --fix -y # 无交互直接修复所有配置
|
||||
> ```
|
||||
|
||||
**备选方案:CDP 模式 (适用于服务器/无头环境)**
|
||||
如果你无法安装浏览器扩展(比如在远程无头服务器上运行 OpenCLI),你可以通过 SSH 隧道或反向代理,利用 CDP (Chrome DevTools Protocol) 连接到本地的 Chrome 浏览器。详细指南请参考 [CDP 连接教程](./CDP.zh-CN.md)。
|
||||
|
||||
<details>
|
||||
<summary>手动配置(备选方案)</summary>
|
||||
|
||||
|
||||
+79
-5
@@ -111,13 +111,87 @@ export function findMcpServerPath(): string | null {
|
||||
return _cachedMcpServerPath;
|
||||
}
|
||||
|
||||
function buildRuntimeArgs(input?: { executablePath?: string | null }): string[] {
|
||||
/**
|
||||
* Chrome 144+ auto-discovery: read DevToolsActivePort file to get CDP endpoint.
|
||||
*
|
||||
* Starting with Chrome 144, users can enable remote debugging from
|
||||
* chrome://inspect#remote-debugging without any command-line flags.
|
||||
* Chrome writes the active port and browser GUID to a DevToolsActivePort file
|
||||
* in the user data directory, which we read to construct the WebSocket endpoint.
|
||||
*/
|
||||
export function discoverChromeEndpoint(): string | null {
|
||||
const candidates: string[] = [];
|
||||
|
||||
// User-specified Chrome data dir takes highest priority
|
||||
if (process.env.CHROME_USER_DATA_DIR) {
|
||||
candidates.push(path.join(process.env.CHROME_USER_DATA_DIR, 'DevToolsActivePort'));
|
||||
}
|
||||
|
||||
// Standard Chrome/Edge user data dirs per platform
|
||||
if (process.platform === 'win32') {
|
||||
const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
|
||||
candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data', 'DevToolsActivePort'));
|
||||
candidates.push(path.join(localAppData, 'Microsoft', 'Edge', 'User Data', 'DevToolsActivePort'));
|
||||
} else if (process.platform === 'darwin') {
|
||||
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'DevToolsActivePort'));
|
||||
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge', 'DevToolsActivePort'));
|
||||
} else {
|
||||
candidates.push(path.join(os.homedir(), '.config', 'google-chrome', 'DevToolsActivePort'));
|
||||
candidates.push(path.join(os.homedir(), '.config', 'chromium', 'DevToolsActivePort'));
|
||||
candidates.push(path.join(os.homedir(), '.config', 'microsoft-edge', 'DevToolsActivePort'));
|
||||
}
|
||||
|
||||
for (const filePath of candidates) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
||||
const lines = content.split('\n');
|
||||
if (lines.length >= 2) {
|
||||
const port = parseInt(lines[0], 10);
|
||||
const browserPath = lines[1]; // e.g. /devtools/browser/<GUID>
|
||||
if (port > 0 && browserPath.startsWith('/devtools/browser/')) {
|
||||
return `ws://127.0.0.1:${port}${browserPath}`;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveCdpEndpoint(): { endpoint?: string; requestedCdp: boolean } {
|
||||
const envVal = process.env.OPENCLI_CDP_ENDPOINT;
|
||||
if (envVal === '1' || envVal?.toLowerCase() === 'true') {
|
||||
const autoDiscovered = discoverChromeEndpoint();
|
||||
return { endpoint: autoDiscovered ?? envVal, requestedCdp: true };
|
||||
}
|
||||
|
||||
if (envVal) {
|
||||
return { endpoint: envVal, requestedCdp: true };
|
||||
}
|
||||
|
||||
// Fallback to auto-discovery if not explicitly set
|
||||
const autoDiscovered = discoverChromeEndpoint();
|
||||
if (autoDiscovered) {
|
||||
return { endpoint: autoDiscovered, requestedCdp: true };
|
||||
}
|
||||
|
||||
return { requestedCdp: false };
|
||||
}
|
||||
|
||||
function buildRuntimeArgs(input?: { executablePath?: string | null; cdpEndpoint?: string }): string[] {
|
||||
const args: string[] = [];
|
||||
|
||||
// Priority 1: CDP endpoint (remote Chrome debugging or local Auto-Discovery)
|
||||
if (input?.cdpEndpoint) {
|
||||
args.push('--cdp-endpoint', input.cdpEndpoint);
|
||||
return args;
|
||||
}
|
||||
|
||||
// Priority 2: Extension mode (local Chrome with MCP Bridge extension)
|
||||
if (!process.env.CI) {
|
||||
// Local: always connect to user's running Chrome via MCP Bridge extension
|
||||
args.push('--extension');
|
||||
}
|
||||
// CI: standalone mode — @playwright/mcp launches its own browser (headed by default).
|
||||
|
||||
// CI/standalone mode: @playwright/mcp launches its own browser (headed by default).
|
||||
// xvfb provides a virtual display for headed mode in GitHub Actions.
|
||||
if (input?.executablePath) {
|
||||
args.push('--executable-path', input.executablePath);
|
||||
@@ -125,11 +199,11 @@ function buildRuntimeArgs(input?: { executablePath?: string | null }): string[]
|
||||
return args;
|
||||
}
|
||||
|
||||
export function buildMcpArgs(input: { mcpPath: string; executablePath?: string | null }): string[] {
|
||||
export function buildMcpArgs(input: { mcpPath: string; executablePath?: string | null; cdpEndpoint?: string }): string[] {
|
||||
return [input.mcpPath, ...buildRuntimeArgs(input)];
|
||||
}
|
||||
|
||||
export function buildMcpLaunchSpec(input: { mcpPath?: string | null; executablePath?: string | null }): {
|
||||
export function buildMcpLaunchSpec(input: { mcpPath?: string | null; executablePath?: string | null; cdpEndpoint?: string }): {
|
||||
command: string;
|
||||
args: string[];
|
||||
usedNpxFallback: boolean;
|
||||
|
||||
+17
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
export type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'unknown';
|
||||
export type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'cdp-connection-failed' | 'unknown';
|
||||
|
||||
export type ConnectFailureInput = {
|
||||
kind: ConnectFailureKind;
|
||||
@@ -26,6 +26,15 @@ export function formatBrowserConnectError(input: ConnectFailureInput): Error {
|
||||
const suffix = stderr ? `\n\nMCP stderr:\n${stderr}` : '';
|
||||
const tokenHint = input.tokenFingerprint ? ` Token fingerprint: ${input.tokenFingerprint}.` : '';
|
||||
|
||||
if (input.kind === 'cdp-connection-failed') {
|
||||
return new Error(
|
||||
`Failed to connect to remote Chrome via CDP endpoint.\n\n` +
|
||||
`Check if Chrome is running with remote debugging enabled (--remote-debugging-port=9222) or DevToolsActivePort is available under chrome://inspect#remote-debugging.\n` +
|
||||
`If you specified OPENCLI_CDP_ENDPOINT=1, auto-discovery might have failed.` +
|
||||
suffix,
|
||||
);
|
||||
}
|
||||
|
||||
if (input.kind === 'missing-token') {
|
||||
return new Error(
|
||||
'Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' +
|
||||
@@ -74,9 +83,16 @@ export function inferConnectFailureKind(args: {
|
||||
stderr: string;
|
||||
rawMessage?: string;
|
||||
exited?: boolean;
|
||||
isCdpMode?: boolean;
|
||||
}): ConnectFailureKind {
|
||||
const haystack = `${args.rawMessage ?? ''}\n${args.stderr}`.toLowerCase();
|
||||
|
||||
if (args.isCdpMode) {
|
||||
if (args.rawMessage?.startsWith('MCP init failed:')) return 'mcp-init';
|
||||
if (args.exited) return 'cdp-connection-failed';
|
||||
return 'cdp-connection-failed';
|
||||
}
|
||||
|
||||
if (!args.hasExtensionToken)
|
||||
return 'missing-token';
|
||||
if (haystack.includes('extension connection timeout') || haystack.includes('playwright mcp bridge'))
|
||||
|
||||
@@ -9,6 +9,7 @@ export { Page } from './page.js';
|
||||
export { PlaywrightMCP } from './mcp.js';
|
||||
export { getTokenFingerprint, formatBrowserConnectError } from './errors.js';
|
||||
export type { ConnectFailureKind, ConnectFailureInput } from './errors.js';
|
||||
export { resolveCdpEndpoint } from './discover.js';
|
||||
|
||||
// Test-only helpers — exposed for unit tests
|
||||
import { createJsonRpcRequest } from './mcp.js';
|
||||
|
||||
+8
-3
@@ -9,7 +9,7 @@ import { withTimeoutMs, DEFAULT_BROWSER_CONNECT_TIMEOUT } from '../runtime.js';
|
||||
import { PKG_VERSION } from '../version.js';
|
||||
import { Page } from './page.js';
|
||||
import { getTokenFingerprint, formatBrowserConnectError, inferConnectFailureKind } from './errors.js';
|
||||
import { findMcpServerPath, buildMcpLaunchSpec } from './discover.js';
|
||||
import { findMcpServerPath, buildMcpLaunchSpec, resolveCdpEndpoint } from './discover.js';
|
||||
import { extractTabIdentities, extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
|
||||
|
||||
const STDERR_BUFFER_LIMIT = 16 * 1024;
|
||||
@@ -114,7 +114,8 @@ export class PlaywrightMCP {
|
||||
return new Promise<Page>((resolve, reject) => {
|
||||
const isDebug = process.env.DEBUG?.includes('opencli:mcp');
|
||||
const debugLog = (msg: string) => isDebug && console.error(`[opencli:mcp] ${msg}`);
|
||||
const useExtension = !!process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
|
||||
const { endpoint: cdpEndpoint, requestedCdp } = resolveCdpEndpoint();
|
||||
const useExtension = !requestedCdp;
|
||||
const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
|
||||
const tokenFingerprint = getTokenFingerprint(extensionToken);
|
||||
let stderrBuffer = '';
|
||||
@@ -150,15 +151,17 @@ export class PlaywrightMCP {
|
||||
settleError(inferConnectFailureKind({
|
||||
hasExtensionToken: !!extensionToken,
|
||||
stderr: stderrBuffer,
|
||||
isCdpMode: requestedCdp,
|
||||
}));
|
||||
}, timeout * 1000);
|
||||
|
||||
const launchSpec = buildMcpLaunchSpec({
|
||||
mcpPath,
|
||||
executablePath: process.env.OPENCLI_BROWSER_EXECUTABLE_PATH,
|
||||
cdpEndpoint,
|
||||
});
|
||||
if (process.env.OPENCLI_VERBOSE) {
|
||||
console.error(`[opencli] Mode: ${useExtension ? 'extension' : 'standalone'}`);
|
||||
console.error(`[opencli] Mode: ${requestedCdp ? 'CDP' : useExtension ? 'extension' : 'standalone'}`);
|
||||
if (useExtension) console.error(`[opencli] Extension token: fingerprint ${tokenFingerprint}`);
|
||||
if (launchSpec.usedNpxFallback) {
|
||||
console.error('[opencli] Playwright MCP not found locally; bootstrapping via npx @playwright/mcp@latest');
|
||||
@@ -218,6 +221,7 @@ export class PlaywrightMCP {
|
||||
hasExtensionToken: !!extensionToken,
|
||||
stderr: stderrBuffer,
|
||||
exited: true,
|
||||
isCdpMode: requestedCdp,
|
||||
}), { exitCode: code });
|
||||
}
|
||||
});
|
||||
@@ -235,6 +239,7 @@ export class PlaywrightMCP {
|
||||
hasExtensionToken: !!extensionToken,
|
||||
stderr: stderrBuffer,
|
||||
rawMessage: `MCP init failed: ${resp.error.message}`,
|
||||
isCdpMode: requestedCdp,
|
||||
}), { rawMessage: resp.error.message });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -591,6 +591,15 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
|
||||
const hasMismatch = uniqueFingerprints.length > 1;
|
||||
const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), ''];
|
||||
|
||||
// CDP endpoint mode (for remote/server environments)
|
||||
const cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
|
||||
if (cdpEndpoint) {
|
||||
lines.push(statusLine('OK', `CDP endpoint: ${chalk.cyan(cdpEndpoint)}`));
|
||||
lines.push(chalk.dim(' → Remote Chrome mode: extension token not required'));
|
||||
lines.push('');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const installStatus: ReportStatus = report.extensionInstalled ? 'OK' : 'MISSING';
|
||||
const installDetail = report.extensionInstalled
|
||||
? `Extension installed (${report.extensionBrowsers.join(', ')})`
|
||||
|
||||
Reference in new issue
Block a user