Three Bugs Deep: Getting Chrome DevTools MCP Working from WSL2

by

this post has been formatted by AI

How we wired up live browser debugging across a secure LAN of Linux boxes, discovered our networking layer had a bug hiding under another bug, and why letting Claude drive PowerShell directly turned a day of round-trips into a few minutes of commands.

The problem we were actually trying to solve

This wasn’t a networking project. It was a video player bug.

next.reelreport.com‘s video-details page was throwing NotSupportedError: The element has no supported sources when a real user clicked play, in their real Chrome. Meanwhile, thirteen separate automated reproduction attempts — headless Chromium, every viewport, every timing variation we could script — played the exact same video cleanly, every single time. Same code, same data, same Mux CDN response. Nothing wrong, allegedly.

That gap is a specific kind of frustrating: the bug is real, but every tool we have for inspecting it automatically refuses to see it. The only way through was to stop guessing and get eyes on an actual failing browser session — Network tab, console, the works — live. Which meant one thing: Claude needed to be able to drive a real Chrome instance and read its DevTools protocol directly, not just script a headless copy of it.

That’s a solved problem in isolation — Chrome DevTools MCP exists for exactly this. Making it reach across a WSL2 boundary onto a real, running Chrome, without exposing anything we shouldn’t, turned out to be its own small adventure.

The constraint that shaped everything: this had to work for more than one box

The obvious first move for “WSL needs to reach something running on Windows” is WSL2 mirrored networking mode — a .wslconfig flag that makes WSL share the host’s network stack directly, so localhost means the same thing on both sides. Officially supported, no relay, no firewall rule. Clean.

We backed away from it, and not because it didn’t work. Two things ruled it out:

  1. This machine isn’t the only one that needs access. Development here isn’t confined to one WSL box — it also happens over remote SSH on two other Linux machines. Mirrored networking mode is a property of this specific WSL instance’s virtual network stack. It does nothing for a completely separate Linux box on the LAN trying to reach the same Chrome window. Whatever we built had to be reachable from outside the host too, not just from WSL running on top of it.
  2. The LAN itself is trusted. This is a private 192.168.1.0/24 network — the user’s own devices only, no guest network, no IoT sprawl. Once you accept that premise, the shape of the right answer changes: instead of a WSL-specific networking mode, you want a relay that any trusted machine on that LAN can hit, guarded by a real firewall rule rather than relying on WSL’s internal plumbing.

So the actual design was: a dedicated debug Chrome profile on Windows, bound to --remote-debugging-port=9222; a netsh interface portproxy rule relaying 0.0.0.0:9222 to Chrome’s real listener; and a Windows Firewall rule scoping inbound access to the trusted subnet.

Chrome itself never leaves loopback. Nothing about the setup cares whether the caller is this WSL instance, a different WSL instance, or one of the other Linux boxes over SSH — it’s just a LAN-reachable port, the same for everyone.

Good plan. It took three separate bugs to actually get there.

Bug #1: the networking mode we rejected turned itself back on

Mid-investigation, an unrelated VSCode restart quietly reactivated mirrored networking mode anyway. Not because anyone re-enabled it — a .wslconfig change from an earlier, abandoned attempt had been left in place “dormant,” and closing/reopening VSCode was apparently enough to tear down and respawn the idle WSL2 VM, which picked the setting back up on its own.

That one config flag broke Chrome access in two completely different ways at once:

  • Loopback (127.0.0.1) connections from WSL now technically reached Chrome’s listener, but Chrome’s own anti-DNS-rebinding protection reset them — the mirrored-mode forwarding path didn’t look enough like real loopback traffic to satisfy Chrome’s own security check.
  • The LAN IP stopped working entirely, because mirrored mode gives WSL’s own network interface the same IP address as the Windows host. A request to “the LAN IP” from inside WSL no longer left the machine — WSL’s own kernel intercepted it as a request to itself, found nothing listening, and refused the connection immediately.

Diagnosing that took nc -zv and curl -v side by side to tell “refused instantly” apart from “connected, then reset” — two very different failure shapes that both just looked like “doesn’t work” from the outside.

Once separated, the fix was straightforward: comment out the mirrored networking setting (with the reasoning written inline, so it doesn’t silently resurrect itself a third time), and wsl --shutdown back to plain NAT mode — the mode the LAN-relay design had actually been built and verified against in the first place.

Bug #2: a bug that was probably there from minute one

With mirrored mode gone, raw reachability came back — for about as long as it took to notice it still didn’t work reliably. The relay was listening. Chrome was listening. Get-NetTCPConnection on Windows showed everything green. And yet a plain curl to the DevTools JSON endpoint would connect fine and then just… sit there, eventually returning an empty reply.

The actual bug had nothing to do with the mirrored-mode detour at all: the netsh portproxy rule was configured v4tov4, relaying to 127.0.0.1:9222 — but Chrome, on this machine, only ever binds its DevTools port to ::1, the IPv6 loopback address.

The relay was faithfully forwarding traffic to an address nothing was listening on. It likely never worked correctly, and an earlier “verified end-to-end” check had probably just gotten lucky catching Chrome in a moment where it happened to also be reachable on IPv4.

The fix was one command: delete the v4tov4 rule, add a v4tov6 one pointing at ::1 instead.

Same external port, same firewall rule, completely different, actually-correct target. First real end-to-end success of the day — a genuine DevTools protocol JSON response, from both the WSL side and the LAN side.

Bug #3: the network layer wasn’t even the last problem

Raw reachability working is not the same as the MCP tool working. Every previous “it’s fixed” claim in this saga had only ever been a curl against the DevTools JSON endpoint — the actual chrome-devtools MCP server had never once successfully connected in any session.

The reason: chrome-devtools-mcp requires Node 20.19 or newer. The shell’s actual default Node version was 20.10 — pinned there by a hardcoded PATH line in .bashrc that silently overrode whatever nvm‘s own default-version logic would otherwise have picked.

A newer Node was already installed and just… not selected.

Given the choice between patching around it locally or fixing it properly, the call was to update the global default outright and deal with whatever else that touched, rather than leave another quiet version pin sitting around waiting to cause the next multi-hour detour.

The cleanup that followed is worth calling out on its own: rather than hardcoding the new version number in both .bashrc and the MCP server’s config, we pointed both at a single ~/.nvm/versions/node/latest symlink, plus set nvm alias default node so the shell’s own default self-updates on every future nvm install with zero manual steps at all.

One next upgrade, one symlink repoint, everywhere that matters stays in sync. Small thing, but it’s exactly the kind of fix that means this specific class of bug doesn’t get to happen a second time.

The actual unlock: letting Claude drive PowerShell directly

For most of this saga, verifying anything on the Windows side meant asking the user to open PowerShell, run a command, and paste the output back into the conversation.

That’s a real tax — every single diagnostic step became a round trip, and round trips are where debugging sessions go to die.

Partway through, the workflow changed: instead of asking, Claude was told outright to invoke powershell.exe, cmd.exe, and other Windows executables directly from WSL — inspecting and configuring the Windows side itself wherever possible, only stopping to explain first when a change was destructive or security-sensitive.

The effect was immediate. Get-NetTCPConnection checks that used to take a full message round-trip became a single tool call. netsh interface portproxy show all — the command that actually revealed the v4tov4/::1 mismatch — got run and read in the same breath as the hypothesis that prompted it, instead of waiting on a copy-paste.

Elevated commands (adding a new portproxy rule) still needed the user, since a non-elevated shell can’t grant itself admin rights — but everything read-only, which was most of the debugging, stopped needing a human in the loop at all.

The clearest example came later, verifying the eventual fix in production. The debug Chrome window had quietly closed between sessions — the exact same “everything’s configured but nothing is actually running” failure from earlier in the saga.

Old habit would have been to ask the user to relaunch it. Instead: cmd.exe /c "C:\Users\dan\chrome-debug.bat", straight from WSL, and straight back to verifying.

No round trip, no waiting.

Where it landed

Once all three bugs were actually fixed — mirrored mode off, the relay pointed at the address Chrome really listens on, Node upgraded — the Chrome DevTools MCP connection worked for the first time, verified for real this time: listing open tabs, reading console output, inspecting live network requests against an actual running browser.

It paid for itself immediately.

Within the same session, that live access reproduced the video-playback bug that thirteen headless automation attempts couldn’t — and not just reproduced it, but caught the exact mechanism live: the network layer fetching a valid Mux manifest and a real video segment successfully, while the <video> element itself independently ended up in an unsupported-source error state, disconnected from those successful fetches.

That observation pointed straight at a missing playback-mode configuration, which turned into an actual fix, tested, and verified live in production against the exact browser and the exact video from the original report.

Three stacked bugs, one rejected shortcut that turned out to be the right call for reasons that had nothing to do with any of those bugs, and a workflow change that made every step after the first one faster.

That’s the saga.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *