Page
Library
Module
Module type
Parameter
Class
Class type
Source
OCaml client for the Chrome DevTools Protocol (CDP): typed protocol modules generated from the official protocol JSON, plus an Lwt connection over libcurl WebSockets.
Status: experimental, not released. The full stack works end to end against a real Chrome (see the demo below); the API may still change.
Package | What it is |
|---|---|
| Typed protocol domains: records, enums, commands, events, with JSON codecs |
| The connection: WebSocket transport, typed |
| The generator CLI: protocol JSON in, OCaml out |
cdp ships 10 domains: Browser, DOM, Debugger, Emulation, IO, Network, Page, Runtime, Security, and Target. The generator covers all 58 — see How generation works to build your own selection.
The packages are not on opam yet; build from a clone:
git clone https://github.com/ahrefs/ocaml-cdp.git
cd ocaml-cdp
opam switch create . 5.4.1 --no-install
opam install . --deps-only --with-test
make build testcdp-lwt uses libcurl's WebSocket API, which ocurl has not released yet (latest release: 0.10.0) — cdp-lwt.opam therefore pins ocurl master via pin-depends, and opam install picks that up by itself.
Your system libcurl must be 7.86 or newer (check with curl-config --version) and built with WebSocket support. Chrome-launching code and examples need a Chrome: by default the executable is google-chrome from PATH; set the CDP_CHROME environment variable (or pass ~executable to Chrome.launch) to use another binary, e.g. chromium or CDP_CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" on macOS.
make demo launches a headless Chrome and runs examples/navigate.ml:
let%lwt chrome = Cdp_lwt.Chrome.launch () in
let%lwt transport = Cdp_lwt.Curl_transport.connect ~url:chrome.ws_url () in
let connection = Cdp_lwt.Connection.create transport in
let call ?session command = Cdp_lwt.Connection.call connection ?session ~timeout:10.0 command in
let%lwt created = call (Cdp.Target.Create_target.command (Cdp.Target.Create_target.make_params ~url:"about:blank" ())) in
let%lwt attached =
call (Cdp.Target.Attach_to_target.command
(Cdp.Target.Attach_to_target.make_params ~target_id:created.target_id ~flatten:true ()))
in
let session = attached.session_id in
let%lwt () = call ~session (Cdp.Page.Enable.command (Cdp.Page.Enable.make_params ())) in
let loaded = Cdp_lwt.Connection.next_event connection ~session Cdp.Page.Load_event_fired.event in
let%lwt _navigation = call ~session (Cdp.Page.Navigate.command (Cdp.Page.Navigate.make_params ~url:"data:text/html,<title>Hello from OCaml CDP</title>" ())) in
let%lwt _fired = loaded in
let%lwt evaluated =
call ~session (Cdp.Runtime.Evaluate.command (Cdp.Runtime.Evaluate.make_params ~expression:"document.title" ()))
inEvery step is a typed command; failures are typed too (Protocol_error, Call_timeout, Session_detached, Connection_closed). call waits up to 180 seconds by default — pass ~timeout to change it (the demo shortens it to 10), or Float.infinity to wait forever.
One rule to know: next_event catches events arriving after it is called — subscribe first, then trigger (as the demo does around navigate). next_event waits for one occurrence; for a persistent subscription use Connection.on_event, which fires on every occurrence until unsubscribed.
All examples live in examples/ and run against a real Chrome:
Example | Command | What it does |
|---|---|---|
| launch Chrome, open a page, read its title back | |
| report the main document's HTTP status, headers, and rendered HTML | |
| save a full-size screenshot and a small thumbnail into | |
| screenshot through a Chrome that is already running, closing only its own tab |
navigate, render, and screenshot launch their own headless Chrome. attach connects to an existing one: start Chrome with --remote-debugging-port=<port>, read webSocketDebuggerUrl from http://127.0.0.1:<port>/json/version, and pass it as WS=.
Chrome.launch keeps Chrome's sandbox on — it is the isolation layer between web pages and your machine. In environments where the sandbox cannot start (typically running as root in a container without user namespaces), pass ~no_sandbox:true and treat every page you open as untrusted.
Request_id.t cannot be confused with a Frame_id.t.Other fallback, so new Chrome enum values never crash a decode.option; None fields are omitted on the wire."😀".substring(0, 1)), and Chrome sends the lone half as-is. OCaml strings are UTF-8 and cannot represent it, and dropping the message would hang the pending call — so each unpaired half becomes U+FFFD (�) before parsing.protocol/*.json --(cdp-gen)--> lib/cdp_*.ml --(dune + ppx)--> the cdp libraryThe generated code is committed; users never run the generator. The vendored protocol snapshot is pinned in protocol/REVISION.
make generate # regenerate lib/ after changing gen/gen.ml
make update-protocol # fetch the latest protocol and regenerate
make update-protocol REV=1680125 # fetch a specific revision
make check # CI guard: lib/ matches the generatorTo generate types for your own protocol snapshot (any revision, or your own JSON), use the CLI directly:
cdp-gen fetch mydir 1650000
cdp-gen generate mydir/browser_protocol.json mydir/js_protocol.json out Network,PageTo compile the output as a library: copy the four hand-written glue files cdp_json.ml, cdp_command.ml, cdp_event.ml, and cdp_envelope.ml from lib/ next to the generated files, and use this dune stanza (the one make check-full uses, under whatever library name you like):
(library
(name my_cdp)
(wrapped false)
(libraries jsonkit yojson)
(preprocess
(pps jsonkit.ppx ppx_deriving.show ppx_deriving.eq ppx_deriving.make))
(flags
(:standard -w -a -alert -all)))make help lists all targets. Tests:
test/cram/*.t): small protocol JSON in, generated OCaml out — review diffs with dune runtest, accept with dune promote;test/gen/) and schema-driven roundtrip tests over every generated type (test/roundtrip/, regenerated by make generate);test/, test/lwt/);make test-browser: opt-in smoke tests against a local headless Chrome;make check-full: generates and compiles ALL 58 protocol domains.MIT, with one exception: the vendored protocol definitions protocol/browser_protocol.json and protocol/js_protocol.json come from the Chrome DevTools Protocol and are BSD-3-Clause, Copyright 2014 The Chromium Authors — see protocol/LICENSE. make update-protocol refreshes that license file together with the JSONs.