Launch a real Chrome, find a tab, open its debugging socket, and send one CDP command by hand — every byte from a live local trace.
You already drive browsers through wrappers — Puppeteer, Playwright. This track goes one
layer down, to the protocol those wrappers speak: the Chrome DevTools Protocol.
By the end of this lesson you will have connected to a live Chrome yourself and sent a raw CDP
command over a WebSocket, and you'll be able to name every part of the exchange. That's the
foundation the Domains & events lesson, and later the
chrome-devtools-mcp bridge, sit on top of.
CDP is just JSON messages over a WebSocket. You ask the browser to find you a target (a tab) over plain HTTP, open the WebSocket URL it hands back, and then trade JSON: you send commands, it sends back responses and events. Puppeteer is a friendly face over exactly this.src
Chrome only speaks CDP over the network if you ask it to, with one flag:
The debugging port has no authentication. Anyone who can reach it gets
full, silent control of that browser — read cookies, run JS, open pages. Attackers abused
this to steal cookies, so from Chrome 136 the flag is ignored against your normal
profile and now requires a non-standard --user-data-dir.chrome
Always use a throwaway profile, and never bind the port to a public interface. This is the
CDP cousin of the "MUST validate Origin" warning from your MCP track.
Before any WebSocket, you discover what's debuggable over ordinary HTTP GETs. Two endpoints
matter. First, /json/version — who the browser is, plus the browser-level
socket:
Then /json/list — every target: tabs, but also workers and extension
pages. This is the real, unedited list from our Chrome, and it's noisy:
type: "page"
A "target" is anything debuggable — a tab, an iframe, a service worker, an extension's
background page, or the browser itself.docs
To drive a normal tab you want the one whose type is page. You don't
build the webSocketDebuggerUrl — the browser hands it to you.
Now open that WebSocket and send a command. Every command is one JSON object with an
id, a method of the form Domain.command, and optional
params.src
Here is a real one — asking the browser who it is — and its reply:
That's the whole request/response game: you pick an id, the browser echoes it
back on the result so you can match reply to command. Send another command with a
different id and the two replies can come back in any order — the id
is how you tell them apart.
Commands and responses aren't the only traffic. Watch what happens when we enable the
Page domain and navigate. This is the real stream, in the order it arrived:
id
Three message shapes share one socket:
a command you send (id + method + params),
a response (id + result), and an
event (method + params, no id).src
Notice two events arrived before the id:1 response — ordering isn't
request/response ping-pong. The rule: has id → a response to something
you asked; no id → an unsolicited event. If you know MCP, this is
request-vs-notification wearing a different hat.
You saw those Page.* events only because we sent Page.enable
first. Each domain stays silent until you call its Domain.enable command —
Network.enable for network traffic, and so on. Skip the enable and the command
still works, but the events never come.
A driver script mirrors the MCP track's mcp-curl.sh. From the cdp/
directory:
In step 4, find one line with an id and one without. Which is the response,
which is the event? Bring me anything that surprises you.
Page.* events. What did you most likely forget?/json/list, which entry do you open to drive a browser tab?Getting Started with Chrome DevTools Protocol, by Puppeteer maintainer Andrey Lushnikov. The clearest hands-on account of the wire protocol: targets, the WebSocket, the command/response/event shapes, and (soon for us) sessions. Read the "Targets & Sessions" section — you've now seen on the wire everything it opens with. For the exact method and parameter names, the reference is the official CDP docs.