mcporter/src/chrome-devtools-compat.ts
2026-05-14 12:51:16 +01:00

163 lines
4.6 KiB
TypeScript

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const AUTO_CONNECT_FLAGS = new Set(['--autoConnect', '--auto-connect']);
const FALLBACK_PATCH_FILENAME = 'mcporter-chrome-devtools-auto-connect-patch.js';
const FALLBACK_PATCH_SOURCE = `import fs from 'node:fs';
import path from 'node:path';
const MARKER = 'MCPORTER_DEVTOOLS_TIMEOUT_PATCH';
const HELPER = \`// \${MARKER}
const MCPORTER_DEVTOOLS_DETECTION_TIMEOUT = 1_000;
async function mcporterWithTimeout(promise, fallback) {
let timer;
try {
return await Promise.race([
promise,
new Promise(resolve => {
timer = setTimeout(resolve, MCPORTER_DEVTOOLS_DETECTION_TIMEOUT, fallback);
timer.unref?.();
}),
]);
}
finally {
if (timer) {
clearTimeout(timer);
}
}
}
\`;
const DETECTION_BLOCK = \`if (await page.hasDevTools()) {
mcpPage.devToolsPage = await page.openDevTools();
}\`;
const PATCHED_DETECTION_BLOCK = \`if (await mcporterWithTimeout(page.hasDevTools(), false)) {
mcpPage.devToolsPage = await mcporterWithTimeout(page.openDevTools(), undefined);
}\`;
patchChromeDevtoolsMcp();
function patchChromeDevtoolsMcp(mainPath = process.argv[1]) {
if (!mainPath || !mainPath.includes('chrome-devtools-mcp')) {
return;
}
let resolvedMainPath;
try {
resolvedMainPath = fs.realpathSync(mainPath);
} catch {
return;
}
if (!resolvedMainPath.endsWith(path.join('bin', 'chrome-devtools-mcp.js'))) {
return;
}
const contextPath = path.resolve(path.dirname(resolvedMainPath), '..', 'McpContext.js');
let source;
try {
source = fs.readFileSync(contextPath, 'utf8');
} catch {
return;
}
if (source.includes(MARKER)) {
return;
}
if (!source.includes(DETECTION_BLOCK)) {
return;
}
const withHelper = source.replace(
'const NAVIGATION_TIMEOUT = 10_000;\\n',
\`const NAVIGATION_TIMEOUT = 10_000;\\n\${HELPER}\`
);
const patched = withHelper.replace(DETECTION_BLOCK, PATCHED_DETECTION_BLOCK);
try {
fs.writeFileSync(contextPath, patched);
} catch {
return;
}
}
`;
export interface ChromeDevtoolsCompatResult {
readonly env: Record<string, string>;
readonly applied: boolean;
readonly patchPath?: string;
}
export function applyChromeDevtoolsCompat(
env: Record<string, string>,
command: string,
args: readonly string[]
): ChromeDevtoolsCompatResult {
if (!shouldApplyChromeDevtoolsCompat(command, args, env)) {
return { env, applied: false };
}
const patchPath = resolveChromeDevtoolsCompatPatchPath();
if (!patchPath) {
return { env, applied: false };
}
const importFlag = `--import=${pathToFileURL(patchPath).href}`;
const existingOptions = env.NODE_OPTIONS?.trim();
if (existingOptions?.includes(importFlag)) {
return { env, applied: true, patchPath };
}
return {
env: {
...env,
NODE_OPTIONS: existingOptions ? `${existingOptions} ${importFlag}` : importFlag,
},
applied: true,
patchPath,
};
}
export function shouldApplyChromeDevtoolsCompat(
command: string,
args: readonly string[],
env: NodeJS.ProcessEnv | Record<string, string> = process.env
): boolean {
if (env.MCPORTER_DISABLE_CHROME_DEVTOOLS_COMPAT === '1') {
return false;
}
const tokens = [command, ...args];
return tokens.some(isChromeDevtoolsToken) && args.some((arg) => AUTO_CONNECT_FLAGS.has(arg));
}
function isChromeDevtoolsToken(token: string): boolean {
return (
token === 'chrome-devtools-mcp' ||
token.startsWith('chrome-devtools-mcp@') ||
token.includes('/chrome-devtools-mcp')
);
}
export function resolveChromeDevtoolsCompatPatchPath(
candidates = defaultChromeDevtoolsPatchCandidates(),
fallbackDir = os.tmpdir()
): string | undefined {
const existing = candidates.find((candidate) => fs.existsSync(candidate));
if (existing) {
return existing;
}
return writeFallbackPatch(fallbackDir);
}
function defaultChromeDevtoolsPatchCandidates(): string[] {
const here = path.dirname(fileURLToPath(import.meta.url));
return [
path.join(here, 'chrome-devtools-auto-connect-patch.js'),
path.resolve(here, '..', 'dist', 'chrome-devtools-auto-connect-patch.js'),
];
}
function writeFallbackPatch(fallbackDir: string): string | undefined {
const patchPath = path.join(fallbackDir, FALLBACK_PATCH_FILENAME);
try {
fs.writeFileSync(patchPath, FALLBACK_PATCH_SOURCE, { mode: 0o600 });
return patchPath;
} catch {
return undefined;
}
}