|
| 1 | +import fs from 'node:fs'; |
| 2 | +import os from 'node:os'; |
| 3 | +import path from 'node:path'; |
| 4 | +import { fileURLToPath, pathToFileURL } from 'node:url'; |
| 5 | + |
| 6 | +const AUTO_CONNECT_FLAGS = new Set(['--autoConnect', '--auto-connect']); |
| 7 | +const FALLBACK_PATCH_FILENAME = 'mcporter-chrome-devtools-auto-connect-patch.js'; |
| 8 | +const FALLBACK_PATCH_SOURCE = `import fs from 'node:fs'; |
| 9 | +import path from 'node:path'; |
| 10 | +
|
| 11 | +const MARKER = 'MCPORTER_DEVTOOLS_TIMEOUT_PATCH'; |
| 12 | +const HELPER = \`// \${MARKER} |
| 13 | +const MCPORTER_DEVTOOLS_DETECTION_TIMEOUT = 1_000; |
| 14 | +async function mcporterWithTimeout(promise, fallback) { |
| 15 | + let timer; |
| 16 | + try { |
| 17 | + return await Promise.race([ |
| 18 | + promise, |
| 19 | + new Promise(resolve => { |
| 20 | + timer = setTimeout(resolve, MCPORTER_DEVTOOLS_DETECTION_TIMEOUT, fallback); |
| 21 | + timer.unref?.(); |
| 22 | + }), |
| 23 | + ]); |
| 24 | + } |
| 25 | + finally { |
| 26 | + if (timer) { |
| 27 | + clearTimeout(timer); |
| 28 | + } |
| 29 | + } |
| 30 | +} |
| 31 | +\`; |
| 32 | +
|
| 33 | +const DETECTION_BLOCK = \`if (await page.hasDevTools()) { |
| 34 | + mcpPage.devToolsPage = await page.openDevTools(); |
| 35 | + }\`; |
| 36 | +
|
| 37 | +const PATCHED_DETECTION_BLOCK = \`if (await mcporterWithTimeout(page.hasDevTools(), false)) { |
| 38 | + mcpPage.devToolsPage = await mcporterWithTimeout(page.openDevTools(), undefined); |
| 39 | + }\`; |
| 40 | +
|
| 41 | +patchChromeDevtoolsMcp(); |
| 42 | +
|
| 43 | +function patchChromeDevtoolsMcp(mainPath = process.argv[1]) { |
| 44 | + if (!mainPath || !mainPath.includes('chrome-devtools-mcp')) { |
| 45 | + return; |
| 46 | + } |
| 47 | + let resolvedMainPath; |
| 48 | + try { |
| 49 | + resolvedMainPath = fs.realpathSync(mainPath); |
| 50 | + } catch { |
| 51 | + return; |
| 52 | + } |
| 53 | + if (!resolvedMainPath.endsWith(path.join('bin', 'chrome-devtools-mcp.js'))) { |
| 54 | + return; |
| 55 | + } |
| 56 | + const contextPath = path.resolve(path.dirname(resolvedMainPath), '..', 'McpContext.js'); |
| 57 | + let source; |
| 58 | + try { |
| 59 | + source = fs.readFileSync(contextPath, 'utf8'); |
| 60 | + } catch { |
| 61 | + return; |
| 62 | + } |
| 63 | + if (source.includes(MARKER)) { |
| 64 | + return; |
| 65 | + } |
| 66 | + if (!source.includes(DETECTION_BLOCK)) { |
| 67 | + return; |
| 68 | + } |
| 69 | + const withHelper = source.replace( |
| 70 | + 'const NAVIGATION_TIMEOUT = 10_000;\\n', |
| 71 | + \`const NAVIGATION_TIMEOUT = 10_000;\\n\${HELPER}\` |
| 72 | + ); |
| 73 | + const patched = withHelper.replace(DETECTION_BLOCK, PATCHED_DETECTION_BLOCK); |
| 74 | + try { |
| 75 | + fs.writeFileSync(contextPath, patched); |
| 76 | + } catch { |
| 77 | + return; |
| 78 | + } |
| 79 | +} |
| 80 | +`; |
| 81 | + |
| 82 | +export interface ChromeDevtoolsCompatResult { |
| 83 | + readonly env: Record<string, string>; |
| 84 | + readonly applied: boolean; |
| 85 | + readonly patchPath?: string; |
| 86 | +} |
| 87 | + |
| 88 | +export function applyChromeDevtoolsCompat( |
| 89 | + env: Record<string, string>, |
| 90 | + command: string, |
| 91 | + args: readonly string[] |
| 92 | +): ChromeDevtoolsCompatResult { |
| 93 | + if (!shouldApplyChromeDevtoolsCompat(command, args, env)) { |
| 94 | + return { env, applied: false }; |
| 95 | + } |
| 96 | + const patchPath = resolveChromeDevtoolsCompatPatchPath(); |
| 97 | + if (!patchPath) { |
| 98 | + return { env, applied: false }; |
| 99 | + } |
| 100 | + const importFlag = `--import=${pathToFileURL(patchPath).href}`; |
| 101 | + const existingOptions = env.NODE_OPTIONS?.trim(); |
| 102 | + if (existingOptions?.includes(importFlag)) { |
| 103 | + return { env, applied: true, patchPath }; |
| 104 | + } |
| 105 | + return { |
| 106 | + env: { |
| 107 | + ...env, |
| 108 | + NODE_OPTIONS: existingOptions ? `${existingOptions} ${importFlag}` : importFlag, |
| 109 | + }, |
| 110 | + applied: true, |
| 111 | + patchPath, |
| 112 | + }; |
| 113 | +} |
| 114 | + |
| 115 | +export function shouldApplyChromeDevtoolsCompat( |
| 116 | + command: string, |
| 117 | + args: readonly string[], |
| 118 | + env: NodeJS.ProcessEnv | Record<string, string> = process.env |
| 119 | +): boolean { |
| 120 | + if (env.MCPORTER_DISABLE_CHROME_DEVTOOLS_COMPAT === '1') { |
| 121 | + return false; |
| 122 | + } |
| 123 | + const tokens = [command, ...args]; |
| 124 | + return tokens.some(isChromeDevtoolsToken) && args.some((arg) => AUTO_CONNECT_FLAGS.has(arg)); |
| 125 | +} |
| 126 | + |
| 127 | +function isChromeDevtoolsToken(token: string): boolean { |
| 128 | + return ( |
| 129 | + token === 'chrome-devtools-mcp' || |
| 130 | + token.startsWith('chrome-devtools-mcp@') || |
| 131 | + token.includes('/chrome-devtools-mcp') |
| 132 | + ); |
| 133 | +} |
| 134 | + |
| 135 | +export function resolveChromeDevtoolsCompatPatchPath( |
| 136 | + candidates = defaultChromeDevtoolsPatchCandidates(), |
| 137 | + fallbackDir = os.tmpdir() |
| 138 | +): string | undefined { |
| 139 | + const existing = candidates.find((candidate) => fs.existsSync(candidate)); |
| 140 | + if (existing) { |
| 141 | + return existing; |
| 142 | + } |
| 143 | + return writeFallbackPatch(fallbackDir); |
| 144 | +} |
| 145 | + |
| 146 | +function defaultChromeDevtoolsPatchCandidates(): string[] { |
| 147 | + const here = path.dirname(fileURLToPath(import.meta.url)); |
| 148 | + return [ |
| 149 | + path.join(here, 'chrome-devtools-auto-connect-patch.js'), |
| 150 | + path.resolve(here, '..', 'dist', 'chrome-devtools-auto-connect-patch.js'), |
| 151 | + ]; |
| 152 | +} |
| 153 | + |
| 154 | +function writeFallbackPatch(fallbackDir: string): string | undefined { |
| 155 | + const patchPath = path.join(fallbackDir, FALLBACK_PATCH_FILENAME); |
| 156 | + try { |
| 157 | + fs.writeFileSync(patchPath, FALLBACK_PATCH_SOURCE, { mode: 0o600 }); |
| 158 | + return patchPath; |
| 159 | + } catch { |
| 160 | + return undefined; |
| 161 | + } |
| 162 | +} |
0 commit comments