Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ npx vitest run tests/e2e/ # E2E tests
- **"Failed to connect to Playwright MCP Bridge"**
- Ensure the Playwright MCP extension is installed and **enabled** in your running Chrome.
- Restart the Chrome browser if you just installed the extension.
- If you are running on a VPS or CI host, force standalone mode instead of the extension:
```bash
export OPENCLI_BROWSER_MODE=standalone
export OPENCLI_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium
```
- If Chromium is running as root on Linux, also set `OPENCLI_MCP_NO_SANDBOX=1`.
- **Empty data returns or 'Unauthorized' error**
- Your login session in Chrome might have expired. Open a normal Chrome tab, navigate to the target site, and log in or refresh the page to prove you are human.
- **Node API errors**
Expand Down
16 changes: 16 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ opencli setup
> opencli doctor --fix # 修复不一致的配置(交互确认)
> opencli doctor --fix -y # 无交互直接修复所有配置
> ```
>
> **VPS / 无头环境提示**:如果你希望启动独立浏览器,而不是连接本地 Chrome 扩展模式,请设置:
> ```bash
> export OPENCLI_BROWSER_MODE=standalone
> export OPENCLI_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium
> ```
> 如果是在 Linux root 环境跑 Chromium,还需要再加:
> ```bash
> export OPENCLI_MCP_NO_SANDBOX=1
> ```

<details>
<summary>手动配置(备选方案)</summary>
Expand Down Expand Up @@ -201,6 +211,12 @@ opencli cascade https://api.example.com/data
- **"Failed to connect to Playwright MCP Bridge"** 报错
- 确保你当前的 Chrome 已安装且**开启了** Playwright MCP Bridge 浏览器插件。
- 如果是刚装完插件,需要重启 Chrome 浏览器。
- 如果你跑在 VPS 或 CI 上,改成强制 standalone 模式,不要走扩展连接:
```bash
export OPENCLI_BROWSER_MODE=standalone
export OPENCLI_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium
```
- 如果是在 Linux root 环境跑 Chromium,再设置 `OPENCLI_MCP_NO_SANDBOX=1`。
- **返回空数据,或者报错 "Unauthorized"**
- Chrome 里的登录态可能已经过期(甚至被要求过滑动验证码)。请打开当前 Chrome 页面,在新标签页重新手工登录或刷新该页面。
- **Node API 错误 (如 parseArgs, fs 等)**
Expand Down
8 changes: 8 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,17 @@ CI 中使用 `OPENCLI_BROWSER_EXECUTABLE_PATH` 指定真实 Chrome 路径:

```yaml
env:
OPENCLI_BROWSER_MODE: standalone
OPENCLI_BROWSER_EXECUTABLE_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
```

如果在 Linux root 环境运行 Chromium,可额外设置:

```yaml
env:
OPENCLI_MCP_NO_SANDBOX: '1'
```

---

## 站点兼容性
Expand Down
114 changes: 101 additions & 13 deletions src/browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@ import { describe, it, expect } from 'vitest';
import { PlaywrightMCP, __test__ } from './browser/index.js';

describe('browser helpers', () => {
const setUidForTest = (uid: number) => {
const saved = process.getuid;
Object.defineProperty(process, 'getuid', {
value: () => uid,
configurable: true,
});
return () => {
if (saved) {
Object.defineProperty(process, 'getuid', {
value: saved,
configurable: true,
});
} else {
delete (process as NodeJS.Process & { getuid?: () => number }).getuid;
}
};
};

it('creates JSON-RPC requests with unique ids', () => {
const first = __test__.createJsonRpcRequest('tools/call', { name: 'browser_tabs' });
const second = __test__.createJsonRpcRequest('tools/call', { name: 'browser_snapshot' });
Expand Down Expand Up @@ -51,7 +69,12 @@ describe('browser helpers', () => {

it('builds extension MCP args in local mode (no CI)', () => {
const savedCI = process.env.CI;
const savedMode = process.env.OPENCLI_BROWSER_MODE;
const savedNoSandbox = process.env.OPENCLI_MCP_NO_SANDBOX;
const restoreUid = setUidForTest(1000);
delete process.env.CI;
delete process.env.OPENCLI_BROWSER_MODE;
delete process.env.OPENCLI_MCP_NO_SANDBOX;
try {
expect(__test__.buildMcpArgs({
mcpPath: '/tmp/cli.js',
Expand All @@ -70,19 +93,22 @@ describe('browser helpers', () => {
'--extension',
]);
} finally {
if (savedCI !== undefined) {
process.env.CI = savedCI;
} else {
delete process.env.CI;
}
restoreUid();
if (savedCI !== undefined) process.env.CI = savedCI; else delete process.env.CI;
if (savedMode !== undefined) process.env.OPENCLI_BROWSER_MODE = savedMode; else delete process.env.OPENCLI_BROWSER_MODE;
if (savedNoSandbox !== undefined) process.env.OPENCLI_MCP_NO_SANDBOX = savedNoSandbox; else delete process.env.OPENCLI_MCP_NO_SANDBOX;
}
});

it('builds standalone MCP args in CI mode', () => {
const savedCI = process.env.CI;
const savedMode = process.env.OPENCLI_BROWSER_MODE;
const savedNoSandbox = process.env.OPENCLI_MCP_NO_SANDBOX;
const restoreUid = setUidForTest(1000);
process.env.CI = 'true';
delete process.env.OPENCLI_BROWSER_MODE;
delete process.env.OPENCLI_MCP_NO_SANDBOX;
try {
// CI mode: no --extension — browser launches in standalone headed mode
expect(__test__.buildMcpArgs({
mcpPath: '/tmp/cli.js',
})).toEqual([
Expand All @@ -98,11 +124,75 @@ describe('browser helpers', () => {
'/usr/bin/chromium',
]);
} finally {
if (savedCI !== undefined) {
process.env.CI = savedCI;
} else {
delete process.env.CI;
}
restoreUid();
if (savedCI !== undefined) process.env.CI = savedCI; else delete process.env.CI;
if (savedMode !== undefined) process.env.OPENCLI_BROWSER_MODE = savedMode; else delete process.env.OPENCLI_BROWSER_MODE;
if (savedNoSandbox !== undefined) process.env.OPENCLI_MCP_NO_SANDBOX = savedNoSandbox; else delete process.env.OPENCLI_MCP_NO_SANDBOX;
}
});

it('allows forcing standalone mode outside CI', () => {
const savedCI = process.env.CI;
const savedMode = process.env.OPENCLI_BROWSER_MODE;
const restoreUid = setUidForTest(1000);
delete process.env.CI;
process.env.OPENCLI_BROWSER_MODE = 'standalone';
try {
expect(__test__.buildMcpArgs({
mcpPath: '/tmp/cli.js',
})).toEqual([
'/tmp/cli.js',
]);
} finally {
restoreUid();
if (savedCI !== undefined) process.env.CI = savedCI; else delete process.env.CI;
if (savedMode !== undefined) process.env.OPENCLI_BROWSER_MODE = savedMode; else delete process.env.OPENCLI_BROWSER_MODE;
}
});

it('allows forcing extension mode in CI', () => {
const savedCI = process.env.CI;
const savedMode = process.env.OPENCLI_BROWSER_MODE;
const restoreUid = setUidForTest(1000);
process.env.CI = 'true';
process.env.OPENCLI_BROWSER_MODE = 'extension';
try {
expect(__test__.buildMcpArgs({
mcpPath: '/tmp/cli.js',
})).toEqual([
'/tmp/cli.js',
'--extension',
]);
} finally {
restoreUid();
if (savedCI !== undefined) process.env.CI = savedCI; else delete process.env.CI;
if (savedMode !== undefined) process.env.OPENCLI_BROWSER_MODE = savedMode; else delete process.env.OPENCLI_BROWSER_MODE;
}
});

it('adds no-sandbox when explicitly requested', () => {
const savedCI = process.env.CI;
const savedMode = process.env.OPENCLI_BROWSER_MODE;
const savedNoSandbox = process.env.OPENCLI_MCP_NO_SANDBOX;
const restoreUid = setUidForTest(1000);
process.env.CI = 'true';
delete process.env.OPENCLI_BROWSER_MODE;
process.env.OPENCLI_MCP_NO_SANDBOX = '1';
try {
expect(__test__.buildMcpArgs({
mcpPath: '/tmp/cli.js',
executablePath: '/usr/bin/chromium',
})).toEqual([
'/tmp/cli.js',
'--executable-path',
'/usr/bin/chromium',
'--no-sandbox',
]);
} finally {
restoreUid();
if (savedCI !== undefined) process.env.CI = savedCI; else delete process.env.CI;
if (savedMode !== undefined) process.env.OPENCLI_BROWSER_MODE = savedMode; else delete process.env.OPENCLI_BROWSER_MODE;
if (savedNoSandbox !== undefined) process.env.OPENCLI_MCP_NO_SANDBOX = savedNoSandbox; else delete process.env.OPENCLI_MCP_NO_SANDBOX;
}
});

Expand Down Expand Up @@ -142,6 +232,4 @@ describe('PlaywrightMCP state', () => {

await expect(mcp.connect()).rejects.toThrow('Playwright MCP is closing');
});


});
23 changes: 19 additions & 4 deletions src/browser/discover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,31 @@ export function findMcpServerPath(): string | null {
return _cachedMcpServerPath;
}

type BrowserMode = 'extension' | 'standalone';

function getBrowserMode(): BrowserMode {
const override = process.env.OPENCLI_BROWSER_MODE?.trim().toLowerCase();
if (override === 'extension' || override === 'standalone') return override;
return process.env.CI ? 'standalone' : 'extension';
}

function shouldDisableSandbox(): boolean {
if (process.env.OPENCLI_MCP_NO_SANDBOX === '1') return true;
return process.platform === 'linux' && process.getuid?.() === 0;
}

export function buildMcpArgs(input: { mcpPath: string; executablePath?: string | null }): string[] {
const args = [input.mcpPath];
if (!process.env.CI) {
// Local: always connect to user's running Chrome via MCP Bridge extension
if (getBrowserMode() === 'extension') {
// Extension mode connects to the user's running Chrome via MCP Bridge.
args.push('--extension');
}
// CI: standalone mode — @playwright/mcp launches its own browser (headed by default).
// xvfb provides a virtual display for headed mode in GitHub Actions.
// Standalone mode launches its own browser. CI uses this by default.
if (input.executablePath) {
args.push('--executable-path', input.executablePath);
}
if (shouldDisableSandbox()) {
args.push('--no-sandbox');
}
return args;
}