import { access, readFile, realpath } from "node:fs/promises"; import { constants } from "node:fs"; import { dirname, join, parse, resolve } from "node:path"; import { z } from "zod"; const dockerCommand = ["docker", "run", "-i", "--rm", "--init", "mcr.microsoft.com/playwright/mcp"]; const directPackageManifest = z.object({ name: z.literal("@playwright/mcp"), bin: z.string(), }); const namedPackageManifest = z.object({ name: z.literal("@playwright/mcp"), bin: z.object({ "playwright-mcp": z.string() }), }); const playwrightPackageManifest = z.object({ name: z.literal("playwright"), bin: z.object({ playwright: z.string() }), }); const playwrightTestPackageManifest = z.object({ name: z.literal("@playwright/test"), bin: z.object({ playwright: z.string() }), dependencies: z.object({ playwright: z.string() }), }); async function packageExecutable(packageRoot: string) { try { const input = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")); const direct = directPackageManifest.safeParse(input); const bin = direct.success ? direct.data.bin : namedPackageManifest.parse(input).bin["playwright-mcp"]; const executable = resolve(packageRoot, bin); await access(executable, constants.X_OK); return executable; } catch { return; } } async function ordinaryPackageExecutable(packageRoot: string) { try { const manifest = playwrightPackageManifest.parse( JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")), ); const executable = resolve(packageRoot, manifest.bin.playwright); await access(executable, constants.X_OK); return executable; } catch { return; } } async function playwrightDependency(packageRoot: string) { let current = await realpath(packageRoot); const root = parse(current).root; while (true) { const executable = await ordinaryPackageExecutable( join(current, "node_modules/playwright"), ); if (executable) return executable; if (current === root) return; current = dirname(current); } } async function testPackageExecutable(packageRoot: string) { try { const manifest = playwrightTestPackageManifest.parse( JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")), ); if (!(await playwrightDependency(packageRoot))) return; const executable = resolve(packageRoot, manifest.bin.playwright); await access(executable, constants.X_OK); return executable; } catch { return; } } async function verifiedPathExecutable(candidate: string, kind: "mcp" | "playwright") { try { await access(candidate, constants.X_OK); const target = await realpath(candidate); let directory = dirname(target); const root = parse(directory).root; while (true) { const executable = kind === "mcp" ? await packageExecutable(directory) : ((await ordinaryPackageExecutable(directory)) ?? (await testPackageExecutable(directory))); if (executable && (await realpath(executable)) === target) return candidate; if (directory === root) return; directory = dirname(directory); } } catch { return; } } export async function playwrightMcpCommand( directory: string, path = process.env.PATH ?? "", ): Promise { let current = resolve(directory); const root = parse(current).root; while (true) { const executable = await packageExecutable(join(current, "node_modules/@playwright/mcp")); if (executable) return [executable]; if (current === root) break; current = dirname(current); } for (const entry of path.split(process.platform === "win32" ? ";" : ":")) { if (!entry) continue; const executable = await verifiedPathExecutable(join(entry, "playwright-mcp"), "mcp"); if (executable) return [executable]; } current = resolve(directory); while (true) { const playwright = await ordinaryPackageExecutable( join(current, "node_modules/playwright"), ); const test = await testPackageExecutable(join(current, "node_modules/@playwright/test")); if (playwright || test) return ["npx", "-y", "@playwright/mcp@latest"]; if (current === root) break; current = dirname(current); } for (const entry of path.split(process.platform === "win32" ? ";" : ":")) { if (!entry) continue; const executable = await verifiedPathExecutable(join(entry, "playwright"), "playwright"); if (executable) return ["npx", "-y", "@playwright/mcp@latest"]; } return [...dockerCommand]; }