* fix: discover global @playwright/mcp in nvm/npm installs * test: cover global mcp discovery paths --------- Co-authored-by: jackwener <jakevingoo@gmail.com>
127 lines
4.1 KiB
TypeScript
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;
|
|
}
|