Files
opencli/src/browser/discover.ts
T
Zhang ShengYanandjackwener d6222ff932 fix: discover global @playwright/mcp for nvm/npm installs (#42)
* fix: discover global @playwright/mcp in nvm/npm installs

* test: cover global mcp discovery paths

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-17 22:36:06 +08:00

127 lines
4.1 KiB
TypeScript

/**
* MCP server path discovery and argument building.
*/
import { execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
let _cachedMcpServerPath: string | null | undefined;
let _existsSync = fs.existsSync;
let _execSync = execSync;
export function resetMcpServerPathCache(): void {
_cachedMcpServerPath = undefined;
}
export function setMcpDiscoveryTestHooks(input?: {
existsSync?: typeof fs.existsSync;
execSync?: typeof execSync;
}): void {
_existsSync = input?.existsSync ?? fs.existsSync;
_execSync = input?.execSync ?? execSync;
}
export function findMcpServerPath(): string | null {
if (_cachedMcpServerPath !== undefined) return _cachedMcpServerPath;
const envMcp = process.env.OPENCLI_MCP_SERVER_PATH;
if (envMcp && _existsSync(envMcp)) {
_cachedMcpServerPath = envMcp;
return _cachedMcpServerPath;
}
// Check local node_modules first (@playwright/mcp is the modern package)
const localMcp = path.resolve('node_modules', '@playwright', 'mcp', 'cli.js');
if (_existsSync(localMcp)) {
_cachedMcpServerPath = localMcp;
return _cachedMcpServerPath;
}
// Check project-relative path
const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
const projectMcp = path.resolve(__dirname2, '..', '..', 'node_modules', '@playwright', 'mcp', 'cli.js');
if (_existsSync(projectMcp)) {
_cachedMcpServerPath = projectMcp;
return _cachedMcpServerPath;
}
// Check global npm/yarn locations derived from current Node runtime.
const nodePrefix = path.resolve(path.dirname(process.execPath), '..');
const globalNodeModules = path.join(nodePrefix, 'lib', 'node_modules');
const globalMcp = path.join(globalNodeModules, '@playwright', 'mcp', 'cli.js');
if (_existsSync(globalMcp)) {
_cachedMcpServerPath = globalMcp;
return _cachedMcpServerPath;
}
// Check npm global root directly.
try {
const npmRootGlobal = _execSync('npm root -g 2>/dev/null', {
encoding: 'utf-8',
timeout: 5000,
}).trim();
const npmGlobalMcp = path.join(npmRootGlobal, '@playwright', 'mcp', 'cli.js');
if (npmRootGlobal && _existsSync(npmGlobalMcp)) {
_cachedMcpServerPath = npmGlobalMcp;
return _cachedMcpServerPath;
}
} catch {}
// Check common locations
const candidates = [
path.join(os.homedir(), '.npm', '_npx'),
path.join(os.homedir(), 'node_modules', '.bin'),
'/usr/local/lib/node_modules',
];
// Try npx resolution (legacy package name)
try {
const result = _execSync('npx -y --package=@playwright/mcp which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 10000 }).trim();
if (result && _existsSync(result)) {
_cachedMcpServerPath = result;
return _cachedMcpServerPath;
}
} catch {}
// Try which
try {
const result = _execSync('which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 5000 }).trim();
if (result && _existsSync(result)) {
_cachedMcpServerPath = result;
return _cachedMcpServerPath;
}
} catch {}
// Search in common npx cache
for (const base of candidates) {
if (!_existsSync(base)) continue;
try {
const found = _execSync(`find "${base}" -name "cli.js" -path "*playwright*mcp*" 2>/dev/null | head -1`, { encoding: 'utf-8', timeout: 5000 }).trim();
if (found) {
_cachedMcpServerPath = found;
return _cachedMcpServerPath;
}
} catch {}
}
_cachedMcpServerPath = null;
return _cachedMcpServerPath;
}
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
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.
if (input.executablePath) {
args.push('--executable-path', input.executablePath);
}
return args;
}