jevstrudel.git / tools / mcp / browser.mjs

Opening a URL in the user's browser, from the proxy.

Under WSL the browser is Windows': rundll32.exe url.dll,FileProtocolHandler <url> through interop hands the URL to Windows' default browser whole. Measured 2026-09-26 on nyx: it opened Chrome with every query parameter intact, and Chrome's request to http://127.0.0.1:<port>/ reached a listener bound to 127.0.0.1 in the guest (WSL's localhost forwarding). explorer.exe <url> exited 1 and opened nothing; cmd.exe /c start would split the URL at its first &. windows-exec (SSH) cannot show a window at all: its logon is session 0.

Elsewhere, xdg-open or macOS's open. Whatever happens, the caller also hands the URL to the user, so a launch that fails costs one click. JEVSTRUDEL_MCP_OPEN_BROWSER=0 opens nothing: for tests, whose own headless browser takes the URL instead.

16import { spawn } from 'node:child_process';
17import { existsSync, readFileSync } from 'node:fs';
19const RUNDLL32 = '/mnt/c/Windows/System32/rundll32.exe';
20
21function isWsl() {
22  try {
23    return /microsoft/i.test(readFileSync('/proc/sys/kernel/osrelease', 'utf8'));
24  } catch {
25    return false;
26  }
27}
28
29function launcher(url) {
30  if (isWsl() && existsSync(RUNDLL32)) return [RUNDLL32, ['url.dll,FileProtocolHandler', url]];
31  if (process.platform === 'darwin') return ['open', [url]];
32  return ['xdg-open', [url]];
33}

Resolves to how it was opened, or rejects with why not.

36export function openBrowser(url) {
37  if (process.env.JEVSTRUDEL_MCP_OPEN_BROWSER === '0') return Promise.reject(new Error('JEVSTRUDEL_MCP_OPEN_BROWSER=0'));
38  const [command, args] = launcher(url);
39  return new Promise((resolve, reject) => {
40    const child = spawn(command, args, { stdio: 'ignore', detached: true });
41    child.on('error', reject);
42    child.on('exit', (code) => (code === 0 ? resolve(command) : reject(new Error(`${command} exited ${code}`))));
43    child.unref();
44  });
45}