diff --git a/src/browser.test.ts b/src/browser.test.ts index b8ce11e..31fba4e 100644 --- a/src/browser.test.ts +++ b/src/browser.test.ts @@ -1,4 +1,6 @@ import { afterEach, describe, it, expect, vi } from 'vitest'; +import * as os from 'node:os'; +import * as path from 'node:path'; import { PlaywrightMCP, __test__ } from './browser/index.js'; afterEach(() => { @@ -248,6 +250,27 @@ describe('browser helpers', () => { }); } }); + + it('ignores non-server playwright cli paths discovered from fallback scans', () => { + const wrongCli = '/root/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/lib/mcp/terminal/cli.js'; + const npxCacheBase = path.join(os.homedir(), '.npm', '_npx'); + + const existsSync = vi.fn((candidate: any) => { + const value = String(candidate); + return value === npxCacheBase || value === wrongCli; + }); + + const execSync = vi.fn((command: string) => { + if (String(command).includes('npm root -g')) return '/missing/global/node_modules\n' as any; + if (String(command).includes('--package=@playwright/mcp which mcp-server-playwright')) return `${wrongCli}\n` as any; + if (String(command).includes('which mcp-server-playwright')) return '' as any; + if (String(command).includes(`find "${npxCacheBase}"`)) return `${wrongCli}\n` as any; + throw new Error(`unexpected command: ${String(command)}`); + }); + __test__.setMcpDiscoveryTestHooks({ existsSync, execSync: execSync as any }); + + expect(__test__.findMcpServerPath()).toBeNull(); + }); }); describe('PlaywrightMCP state', () => { diff --git a/src/browser/discover.ts b/src/browser/discover.ts index c9783be..1a51fb0 100644 --- a/src/browser/discover.ts +++ b/src/browser/discover.ts @@ -12,6 +12,19 @@ let _cachedMcpServerPath: string | null | undefined; let _existsSync = fs.existsSync; let _execSync = execSync; +function isSupportedMcpEntrypoint(candidate: string): boolean { + const normalized = candidate.replace(/\\/g, '/').toLowerCase(); + return normalized.endsWith('/@playwright/mcp/cli.js') || + normalized.endsWith('/mcp-server-playwright') || + normalized.endsWith('/mcp-server-playwright.js'); +} + +function resolveSupportedMcpPath(candidate: string | null | undefined): string | null { + const trimmed = candidate?.trim(); + if (!trimmed || !_existsSync(trimmed)) return null; + return isSupportedMcpEntrypoint(trimmed) ? trimmed : null; +} + export function resetMcpServerPathCache(): void { _cachedMcpServerPath = undefined; } @@ -80,8 +93,9 @@ export function findMcpServerPath(): string | null { // 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; + const resolved = resolveSupportedMcpPath(result); + if (resolved) { + _cachedMcpServerPath = resolved; return _cachedMcpServerPath; } } catch {} @@ -89,8 +103,9 @@ export function findMcpServerPath(): string | null { // 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; + const resolved = resolveSupportedMcpPath(result); + if (resolved) { + _cachedMcpServerPath = resolved; return _cachedMcpServerPath; } } catch {} @@ -99,9 +114,10 @@ export function findMcpServerPath(): string | null { 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; + const found = _execSync(`find "${base}" -type f -path "*/@playwright/mcp/cli.js" 2>/dev/null | head -1`, { encoding: 'utf-8', timeout: 5000 }).trim(); + const resolved = resolveSupportedMcpPath(found); + if (resolved) { + _cachedMcpServerPath = resolved; return _cachedMcpServerPath; } } catch {}