MCP chrome-devtools depuis WSL : piloter (et auto-lancer) une Chrome Windows

MCP chrome-devtools depuis WSL : piloter (et auto-lancer) une Chrome Windows

·12 min de lecture·Mis à jour le 29 mai 2026

Le problème : le MCP chrome-devtools en WSL veut une Chrome Linux

Je vis dans WSL2 sous Windows 11. Tout mon dev est là-dedans : Claude Code, mes serveurs de dev, mes domaines locaux en HTTPS servis par Caddy. Donc forcément, j'ai voulu brancher le MCP chrome-devtools pour que l'agent aille inspecter mes pages tout seul. Console, requêtes réseau, captures, sans lâcher le terminal. Le rêve.

Sauf que le mode par défaut se connecte en --remote-debugging-pipe et lance sa propre Chrome dans l'environnement Linux. Sous WSLg, le résultat est une fenêtre Chrome quasi morte : elle s'affiche, mais le clavier ne suit pas. Je m'en suis rendu compte de la pire façon, un soir, devant un écran de login Google : impossible de taper le mot de passe. Bloqué. Dès qu'une action humaine entre en jeu (login, captcha, 2FA), c'est mort.

Ce que je voulais, c'était l'inverse exact : piloter ma vraie Chrome Windows, celle où je tape normalement, et laisser le MCP l'inspecter en parallèle.

La solution officielle : viser la Chrome Windows

Comme je tourne en networkingMode=mirrored, Windows et WSL partagent localhost. L'idée tient en deux lignes :

  1. Lancer une Chrome côté Windows avec le port de debug ouvert.
  2. Dire au MCP de s'y brancher via --browserUrl http://127.0.0.1:9222.

La config MCP, dans ~/.claude.json :

"chrome-devtools": {
  "type": "stdio",
  "command": "npx",
  "args": [
    "chrome-devtools-mcp@latest",
    "--browserUrl", "http://127.0.0.1:9222",
    "--acceptInsecureCerts"
  ],
  "env": {}
}

Le --acceptInsecureCerts, ce n'est pas de la déco : grâce à lui, je charge mes domaines locaux auto-signés (*.dev.fransys.io derrière Caddy) sans que Chrome refuse le certificat.

Et là, bonne surprise en creusant : c'est mot pour mot la méthode WSL que le README officiel de chrome-devtools-mcp documente maintenant (le package est passé en 1.x en mai 2026). Mirrored networking, chrome.exe --remote-debugging-port=9222, --browser-url http://127.0.0.1:9222. Ce que je prenais pour un bricolage perso est en fait la voie officielle. Ça rassure.

Le prérequis réseau que personne ne documente

C'est ici que j'ai cramé le plus de temps. Les tutos s'arrêtent tous à "active le mirrored networking". Chez moi, pas suffisant : 127.0.0.1:9222 depuis WSL ne tombait pas toujours sur la Chrome Windows. Une fois sur deux, connexion refusée, sans raison apparente. Une bonne heure perdue à soupçonner le pare-feu Windows avant de comprendre que le problème était ailleurs.

Le maillon manquant est planqué dans le bloc [experimental] du .wslconfig (côté Windows, C:\Users\<user>\.wslconfig) :

[wsl2]
networkingMode=mirrored
[experimental]
hostAddressLoopback=true   # la clé : loopback bidirectionnel WSL <-> Windows
ignoredPorts=9000

hostAddressLoopback=true, c'est lui qui rend le loopback bidirectionnel entre l'hôte Windows et la VM WSL. Sans lui, mirrored couvre 90% des cas mais laisse des trous, et le port de debug tombe pile dans un trou. Après modif du .wslconfig, un wsl --shutdown côté Windows (puis on relance WSL) pour appliquer.

Si tu ne retiens qu'une ligne de cet article, prends celle-là.

Pourquoi --autoConnect ne marche pas en cross-OS

En lisant les release notes, je suis tombé sur --autoConnect (Chrome 144+). Sur le papier, le rêve : se brancher sur ta vraie Chrome loggée, sans profil jetable, via chrome://inspect/#remote-debugging. J'ai sauté dessus. Puis j'ai lu la petite ligne :

--autoConnect cherche un user-data-dir local à la machine du serveur MCP.

Or en WSL→Windows, le serveur MCP est côté Linux, Chrome côté Windows. Le flag ne trouvera jamais le profil Windows depuis Linux. Mort-né en cross-OS. Bref, pour ce setup, --browserUrl reste la bonne réponse. C'est typiquement la feature qui brille sur une seule OS et qui se casse la figure dès qu'on traverse la frontière WSL.

Autre détail, confirmé par la communauté et que j'ai validé à la dure : le profil dédié n'est pas optionnel.

--user-data-dir="C:\Temp\chrome-mcp"

Si ta Chrome perso est déjà ouverte, relancer chrome.exe --remote-debugging-port=9222 sans user-data-dir distinct ouvre juste un onglet dans l'instance existante. Sans ouvrir le port de debug. Tu crois que c'est lancé, et en fait non. Le profil séparé garantit qu'une nouvelle instance "debuggable" démarre, sans toucher à tes onglets, cookies et extensions perso.

Le déclic : la connexion MCP est lazy

J'aurais pu m'arrêter au lancement manuel de Chrome à chaque session. Mais non. Je voulais que ça se fasse tout seul : à chaque fois que je demande à l'agent de vérifier une page via MCP chrome, la Chrome Windows doit démarrer si besoin.

Reste à savoir quand déclencher le lancement. Et là, une info change tout :

Le serveur chrome-devtools-mcp ne se connecte pas au navigateur au démarrage. La connexion est lazy : elle se fait au premier appel d'outil qui a besoin du navigateur.

Conséquence directe : un hook PreToolUse qui matche mcp__chrome-devtools__.* se déclenche juste avant ce premier appel. Il lance Chrome, attend qu'elle réponde, et la connexion lazy du MCP enchaîne sans broncher. Le timing tombe pile.

La communauté, elle, met plutôt un wrapper à la place de la command du MCP. Ça marche, mais ce wrapper s'exécute au démarrage du serveur MCP, c'est-à-dire à chaque lancement de Claude Code. Donc une fenêtre Chrome qui pop à chaque session, même quand je ne touche pas au navigateur. Non merci. Le hook PreToolUse, lui, est lazy comme la connexion : Chrome ne démarre que quand j'en ai vraiment besoin.

Automatiser : le hook PreToolUse

Le hook va dans ~/.claude/settings.json. Point important : c'est bien un hook qu'il faut, pas une consigne dans CLAUDE.md ou en mémoire. Ces deux-là sont du contexte (l'agent "essaie" de suivre), pas de l'exécution garantie. Un comportement automatique déclenché par un évènement, c'est le boulot d'un hook, point.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__chrome-devtools__.*",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/ensure-chrome-windows.sh",
            "timeout": 30,
            "statusMessage": "Lancement de Chrome (Windows) pour MCP…"
          }
        ]
      }
    ]
  }
}

Le matcher mcp__chrome-devtools__.* colle au nommage des outils MCP (mcp__<serveur>__<outil>), donc il attrape tous les outils du serveur chrome-devtools.

Le script idempotent (avec log de preuve)

Le script doit être idempotent : ne rien faire si Chrome répond déjà, ne la lancer que si absente, et bloquer l'appel (sortie 2) avec un message clair plutôt que de laisser le MCP planter façon énigme. J'ai aussi collé une ligne de log par exécution. On verra juste après pourquoi c'est ça, le vrai morceau intéressant.

~/.claude/hooks/ensure-chrome-windows.sh
#!/usr/bin/env bash
# Garantit qu'une Chrome côté Windows expose le port de debug 9222
# AVANT tout appel d'outil chrome-devtools MCP (pont WSL -> Windows).
# Idempotent. Trace dans ~/.claude/chrome-hook.log (already-up | launched | FAIL).
set -u

PORT=9222
URL="http://127.0.0.1:${PORT}/json/version"
CHROME="/mnt/c/Program Files/Google/Chrome/Application/chrome.exe"
LOG="$HOME/.claude/chrome-hook.log"
ts() { date '+%Y-%m-%d %H:%M:%S'; }

# Déjà up ? rien à faire (cas le plus fréquent).
if curl -fsS "$URL" >/dev/null 2>&1; then
  echo "$(ts) already-up" >> "$LOG"
  exit 0
fi

if [ ! -f "$CHROME" ]; then
  echo "$(ts) FAIL chrome.exe introuvable ($CHROME)" >> "$LOG"
  echo "ensure-chrome-windows: chrome.exe introuvable ($CHROME)" >&2
  exit 2
fi

# Profil dédié OBLIGATOIRE : le port de debug ne s'ouvre que dans une
# instance avec --user-data-dir distinct.
mkdir -p /mnt/c/Temp/chrome-mcp 2>/dev/null
"$CHROME" \
  --remote-debugging-port="$PORT" \
  --remote-allow-origins='*' \
  --user-data-dir='C:\Temp\chrome-mcp' \
  --no-first-run --no-default-browser-check \
  >/dev/null 2>&1 &
disown

# Attendre que l'endpoint réponde (jusqu'à ~10s) pour que la connexion
# lazy du serveur MCP réussisse au moment de l'appel d'outil.
for i in $(seq 1 20); do
  if curl -fsS "$URL" >/dev/null 2>&1; then
    echo "$(ts) launched (~$((i*500))ms)" >> "$LOG"
    exit 0
  fi
  sleep 0.5
done

echo "$(ts) FAIL pas de réponse sur :${PORT} après 10s" >> "$LOG"
echo "ensure-chrome-windows: Chrome n'a pas exposé :${PORT} après 10s" >&2
exit 2

Ne pas oublier le bit exécutable : chmod +x ~/.claude/hooks/ensure-chrome-windows.sh. Et comme les hooks se chargent au démarrage de Claude Code, après avoir édité settings.json il faut ouvrir une fois /hooks (ça recharge la config) ou redémarrer.

La preuve par le log

C'est là que la ligne de log paie. Pendant un bon moment, j'étais incapable de distinguer "le hook a fait son no-op" de "le hook ne s'est pas déclenché, mais Chrome était là par hasard". Même résultat à l'écran. Frustrant.

Le log tranche. D'abord, des sessions avec Chrome déjà ouverte. Le hook tire et court-circuite :

13:07:42 already-up
13:07:45 already-up
13:07:59 already-up
13:08:13 already-up

Six déclenchements, un par appel d'outil chrome-devtools. Preuve que le hook est chargé et qu'il matche. Ensuite le test décisif : je ferme Chrome avant de lancer la session, et je redemande une vérif de page :

13:24:22 launched (~1000ms)
13:24:26 already-up
13:24:26 already-up

launched (~1000ms) : Chrome était absente, le hook l'a démarrée tout seul en une seconde, et les appels suivants l'ont trouvée up. Scénario complet, validé de bout en bout. Sans cette branche de log, j'aurais juré que "ça marche" sur la foi d'un faux positif. La leçon vaut bien au-delà de Chrome.

Détection des problèmes

Trois pièges croisés en route, et comment les diagnostiquer :

  • curl http://127.0.0.1:9222/json/version ne répond pas → le hook aurait dû relancer Chrome ; vérifie qu'il est actif (/hooks) et teste le script à la main.
  • list_pages te renvoie une vieille Chrome → si le MCP a démarré avec l'ancienne config (pipe), redémarre Claude Code pour qu'il recharge ses args.
  • Erreur de validation du header Host (connexion VM→hôte rejetée par Chrome) → c'est exactement à ça que sert --remote-allow-origins='*'. En dernier recours, le troubleshooting officiel propose un tunnel SSH depuis WSL :
ssh -N -L 127.0.0.1:9222:127.0.0.1:9222 <user>@<host-ip>

Tant que le mirroring fonctionne, ce tunnel ne sert à rien. Mais c'est confortable de savoir qu'il existe.

Stabiliser dans la durée : le piège OOM

Tout fonctionne. Sauf qu'après quelques jours d'usage intensif, WSL se met à planter sans raison apparente. Diagnostic : un bug de fuite mémoire confirmé dans chrome-devtools-mcp (issues #1192, #1214), aggravé par le fait que chaque session Claude Code lance sa propre instance MCP. Au bout de 4-5 sessions cumulées, la VM sature, le kernel Linux dégaine l'OOM killer, et il flingue systemd/dbus. WSL part en vrille. Reboot obligatoire.

Deux garde-fous, ensemble, suffisent.

Donner du mou à WSL. Dans .wslconfig, monter le swap (8 GB par défaut, c'est trop juste) et ne pas activer autoMemoryReclaim=dropCache (trop agressif, déstabilise la VM) :

[wsl2]
swap=24GB
# [experimental]
# autoMemoryReclaim=gradual   # la version douce — éviter dropCache

Plafonner chaque instance MCP via cgroup. Dans ~/.claude.json, enrober la commande dans systemd-run --user --scope :

"chrome-devtools": {
  "type": "stdio",
  "command": "bash",
  "args": [
    "-lc",
    "exec systemd-run --user --scope -p MemoryMax=6G -p MemorySwapMax=4G npx chrome-devtools-mcp@latest --browserUrl http://127.0.0.1:9222 --acceptInsecureCerts"
  ]
}

Si une instance fuit, le cgroup la tue elle seule au lieu de déclencher l'OOM global qui ferait tomber systemd. C'est la recommandation explicite du ticket upstream.

Détail piégeux : appliquer un nouveau .wslconfig demande un wsl --shutdown. Si tu tombes après ça sur 0x8007054f (CreateInstance/CreateVm/ConfigureNetworking), c'est connu et transitoire — un reboot Windows suffit (HNS/WinNat se réinitialisent). L'erreur ne survit jamais à un redémarrage.

Ce qui marche, ce qui ne marche pas

Ça marche :

  • Inspection complète d'une page (console, réseau, captures) sur une vraie Chrome Windows, interactive
  • Lancement automatique et paresseux de Chrome au premier appel d'outil, zéro geste manuel
  • Idempotence : pas de fenêtre Chrome en double, pas d'ouverture inutile sur les sessions sans navigateur
  • Actions humaines (OAuth, captcha, 2FA) : je tape direct dans la fenêtre Windows pendant que le MCP inspecte en parallèle

Ça ne marche pas (ou pas comme ça) :

  • --autoConnect en cross-OS WSL→Windows (cherche un profil local qui n'est pas là)
  • Lancer Chrome sans --user-data-dir dédié quand la Chrome perso tourne déjà (le port de debug reste fermé)
  • Compter sur CLAUDE.md ou la mémoire pour un comportement "à chaque fois" : il faut un hook

Voilà

Au départ, juste l'envie de débugger mes pages depuis WSL. À l'arrivée, un truc qui se déclenche tout seul au bon moment et auquel je ne pense même plus. Les deux leçons que je garde : hostAddressLoopback=true (le prérequis réseau qu'on oublie à chaque fois) et la connexion MCP lazy (qui rend le hook PreToolUse non seulement viable, mais carrément meilleur que le wrapper).

Et surtout cette habitude prise au passage : quand tu veux être sûr qu'une automatisation tire, colle-lui une ligne de log. Une seconde à écrire, et plus jamais le doute du faux positif.

PartagerLinkedInXBluesky

Articles similaires