{"slug": "mincdp-give-claude-eyes-and-hands-in-a-browser-in-410-lines-of-c", "title": "Mincdp – Give Claude eyes and hands in a browser, in ~410 lines of C", "summary": "Mincdp, a minimal C and Java client library (~410 lines), allows direct control of headless Chrome via the Chrome DevTools Protocol without Selenium or chromedriver, enabling browser automation for testing and AI agent tasks. The library focuses on simplicity and zero dependencies, supporting navigation, JavaScript evaluation, typing, and screenshots.", "body_md": "Drive a real headless Chrome for tests without Selenium or a version-matched chromedriver. Two tiny, dependency-free clients that speak the Chrome DevTools Protocol (CDP) straight to the browser over a WebSocket, in the language of your stack:\n\n**C**:`c/cdp.h`\n\n, a single header-only library (~410 lines, libc + POSIX sockets only).**Java**:`java/Cdp.java`\n\n, a single file (JDK`java.net.http`\n\nonly, nothing to vendor).\n\nSame idea, same small command set, one demonstration each against the same page.\n\n*The image above was produced by mincdp itself: the demo typed into the page,\npressed Enter, then captured this driven state with cdp_screenshot\n(Page.captureScreenshot).*\n\nCDP is the wire protocol Puppeteer and Playwright use under the hood. Talking it directly means no chromedriver middleman and no external dependency to vendor and keep version-matched to Chrome. For a fixed set of internal pages, headless, one browser, that is a good trade: the client is small enough to read in a sitting, and if Chrome ever ships a breaking CDP change you fix the one file rather than wait for an upstream driver release.\n\nIt is deliberately **not** a general framework. It knows only the commands a\nclick-and-assert test needs: navigate, evaluate JS, type text, press a key. If\nyou need cross-browser (Firefox/Safari) or a rich interaction API, use\nPlaywright; this earns its place only where minimalism and zero dependencies do.\n\nNeeds a headless Chrome/Chromium on `PATH`\n\n(and `curl`\n\n, and `cc`\n\n/ `javac`\n\n).\n\n```\nmake ut          # the whole regression suite (see Tests)\nmake demo-c      # build c/demo and run it against page.html\nmake demo-java   # build java/Demo and run it against page.html\nmake demo        # both\nmake shot        # screenshot page.html so you can SEE it\nmake agent GOAL=\"...\"   # let Claude drive the page toward a goal (needs an API key)\n```\n\nEach demo prints a series of assertions and a `demo: N passed, 0 failed`\n\nline. It\nfirst inspects the page's internals (title, attribute, text, computed style,\ngeometry), then drives the real input path: type \"mincdp works\", press Enter, and\nassert the page echoes it. Set `CDP_DEBUG=1`\n\nto see every CDP frame.\n\n`make ut`\n\nruns the full regression, in two kinds, so a test set has both hands\nand eyes:\n\n**codeut**-- \"regular\" browser-free unit tests of the client's pure helpers (base64, JSON escaping), several examples each. No Chrome needed, so it always runs (`tests/codeut.c`\n\n).**uiut**-- the client driving real headless Chrome, in two kinds:*page internals + hands*-- the demos inspect the rendered DOM (presence, attribute, text, computed style, geometry) through`Runtime.evaluate`\n\n, then interact (type, press Enter, assert the DOM changed), then screenshot the*driven*state over the protocol (`cdp_screenshot`\n\n->`Page.captureScreenshot`\n\n) and assert a valid PNG (`c/demo.c`\n\n,`java/Demo.java`\n\n).*eyes, for agents*--`tests/shot.sh`\n\nis the standalone shell version: screenshots a fresh page load with Chrome's`--screenshot`\n\n, no client code needed. It complements the demos' protocol screenshot (which captures a driven state). Either PNG is there to be looked at: open it, or have an agent Read it.\n\nEach layer reports `PASS`\n\n/ `FAIL`\n\n/ `SKIP`\n\n(a missing toolchain SKIPs, exit 77);\nthe suite fails only if a non-skipped layer fails:\n\n```\nmincdp regression:\n  codeut (units)       PASS\n  uiut hands: C        PASS\n  uiut hands: Java     PASS\n  uiut eyes: shot      PASS\n```\n\nThe demos double as the interaction tests; `make demo-c`\n\n/ `demo-java`\n\nrun them\non their own.\n\nYou start Chrome; the client attaches. `demo.sh`\n\ndoes that wiring (launch a\nheadless Chrome with `--remote-debugging-port`\n\n, run the demo, tear down), and it\ninvokes both demos with the same contract, so one launcher and one `page.html`\n\nserve both languages:\n\n```\nPROG... 127.0.0.1 <chrome-port> file://<abs>/page.html\n```\n\nThe same two primitives that make a smoke test, a screenshot (eyes) and the\ninput calls (hands), are the entire surface an LLM needs to drive a browser.\n`c/agent.c`\n\nis that loop and nothing else. See, think, act, repeated.\n\n**See.**`cdp_screenshot`\n\ncaptures the live page, whatever state prior actions drove it to.**Think.** The PNG plus a one-line goal go to the Claude API over`curl`\n\n(the one hop raw sockets can't do here is the TLS to`api.anthropic.com`\n\n). The model replies with exactly one action.**Act.** The client replays that one action against the page, then loops.\n\nThe model gets a screenshot and answers with a single line, one of:\n\n| Reply | Hand |\n|---|---|\n`NAV <url>` |\n`cdp_navigate` |\n`TYPE <text>` |\n`cdp_insert_text` |\n`KEY <name>` |\n`cdp_key` (e.g. `KEY Enter` ) |\n`JS <expr>` |\n`cdp_eval_bool` , a `querySelector(...).click() |\n`DONE <note>` |\nstop |\n\nSame substring trick as the client: the reply is one short line, so pulling the\naction out of the response is a single `\"text\":\"...\"`\n\nfind, not a JSON parser.\n\n```\nexport ANTHROPIC_API_KEY=sk-ant-...\nmake agent GOAL=\"type 'mincdp works' into the box and press Enter\"\n```\n\nA run against `page.html`\n\nlooks like:\n\n```\nstep  1: JS document.getElementById('q').focus()||true\nstep  2: TYPE mincdp works\nstep  3: KEY Enter\nstep  4: DONE the box shows \"echo: mincdp works\"\n```\n\nEach step is a full loop: the model looked at a fresh screenshot, chose the next\nkeystroke, and mincdp replayed it, closing on `DONE`\n\nwhen the screenshot showed\nthe goal met.\n\nIt is deliberately a sketch: one action per turn, substring parsing, no retries,\nthinking off for speed. That minimalism is the point. It is small enough to read\nend to end before you let a model drive a browser. Like the demos, it SKIPs\n(exit 77) when Chrome, `curl`\n\n, or `ANTHROPIC_API_KEY`\n\nis missing. See\n`c/agent.c`\n\nand `agent.sh`\n\n.\n\nBoth clients expose the same shape (C names shown; Java is the camelCase twin):\n\n| Call | Does |\n|---|---|\n`cdp_open(host, port)` / `Cdp.attach` |\nattach to the first page target of a running Chrome |\n`cdp_navigate(url)` |\n`Page.navigate` (does not wait for load) |\n`cdp_eval_bool(js, &out)` / `evalBool` |\n`Runtime.evaluate` a boolean expression |\n`cdp_wait_bool(js, ms)` / `waitBool` |\npoll a boolean expression until true or timeout |\n`cdp_insert_text(text)` / `insertText` |\n`Input.insertText` into the focused element |\n`cdp_key(\"Enter\")` / `key` |\n`Input.dispatchKeyEvent` keyDown+keyUp |\n`cdp_screenshot(path)` / `screenshot` |\n`Page.captureScreenshot` -> base64 -> write a PNG |\n`cdp_close()` / `close` |\ndetach |\n\nResponses are matched by id and read with targeted substring checks, not a JSON\nparser. That is the trick that keeps both clients dependency-free: every command\nreturns a boolean, a short ack, or one flat field (the screenshot's base64), so\nlooking for `\"value\":true`\n\n/ `exceptionDetails`\n\n/ `\"data\":\"...\"`\n\nis enough. A\ncommand that returned deeply nested JSON would need a real parser; none does.\n\n`c/cdp.h`\n\nis a single-header library. In exactly one translation unit:\n\n```\n#define CDP_IMPLEMENTATION\n#include \"cdp.h\"\n```\n\nand just `#include \"cdp.h\"`\n\nelsewhere for the declarations. See `c/demo.c`\n\n.\n\nSame category as a Selenium test (out-of-process automation: your code in one process, a real browser in another, talking over a wire protocol), minus the chromedriver middleman and the external dependency. Closer in spirit to a minimal, hand-rolled Puppeteer.\n\n| Selenium | mincdp |\n|---|---|\n`WebDriver driver` |\n`cdp` handle |\n`driver.get(url)` |\n`cdp_navigate(url)` |\n`findElement` / waits |\n`cdp_wait_bool(sel-exists)` |\n`((JavascriptExecutor)driver).executeScript` |\n`cdp_eval_bool(js)` |\n`element.sendKeys` |\n`cdp_insert_text` |\n| key press | `cdp_key(\"Enter\")` |\n\nWhat differs:\n\n**Selenium** talks the WebDriver protocol to a separate`chromedriver`\n\nbinary that then drives Chrome: three processes, and you must keep chromedriver's version matched to Chrome's. It is a jar plus a versioned native binary to vendor and update.**mincdp** talks CDP directly to Chrome over a WebSocket, no middleman. Each client is one file using only its language's standard library.\n\nThe trade, honestly: Selenium and Playwright give a big, polished, cross-browser API (rich waits, action chains, a grid). mincdp gives the handful of commands a headless smoke test needs, in code you can read end to end. Pick accordingly.\n\n```\npage.html          self-contained demo target (no server, no network)\ndemo.sh            launch Chrome, run a demo, tear down (shared by both)\nMakefile           make ut / demo-c / demo-java / demo / shot / clean\nc/cdp.h            the C client (single-header library)\nc/agent.c          the agent loop: screenshot, Claude, replay one action\nagent.sh           launch Chrome, run the agent toward a GOAL, tear down\nc/demo.c           the C demonstration = the uiut interaction test (hands)\njava/Cdp.java      the Java client (single file)\njava/Demo.java     the Java demonstration = the uiut interaction test (hands)\ntests/run.sh       the regression runner (make ut): codeut + uiut, PASS/FAIL/SKIP\ntests/codeut.c     browser-free unit tests of the client's pure helpers\ntests/shot.sh      the eyes: screenshot page.html to a PNG (Chrome --screenshot)\n```\n\n`legacy/`\n\nholds the 2005 ancestor of this project: an Xvfb-based screenshot\ncapture proof of concept, from before browsers had a headless mode or a remote\nprotocol. It is where mincdp started. See `legacy/README.md`\n\n.\n\nThe dated record of how the \"eyes\" came about (the origin dialog, the\ntimeline, and the attribution ask) is in [ legacy/PROVENANCE.md](/Anode1/mincdp/blob/main/legacy/PROVENANCE.md).\n\nMIT. See `LICENSE`\n\n.\n\nAll product names, logos, and brands are property of their respective owners. Chrome is a trademark of Google; Selenium, Puppeteer, and Playwright are the names of their respective projects. They are used here only for identification and comparison, and no affiliation or endorsement is implied.", "url": "https://wpnews.pro/news/mincdp-give-claude-eyes-and-hands-in-a-browser-in-410-lines-of-c", "canonical_source": "https://github.com/Anode1/mincdp", "published_at": "2026-07-17 16:08:04+00:00", "updated_at": "2026-07-17 16:21:08.063655+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "ai-agents"], "entities": ["Chrome", "Chrome DevTools Protocol", "Selenium", "Puppeteer", "Playwright", "Claude"], "alternates": {"html": "https://wpnews.pro/news/mincdp-give-claude-eyes-and-hands-in-a-browser-in-410-lines-of-c", "markdown": "https://wpnews.pro/news/mincdp-give-claude-eyes-and-hands-in-a-browser-in-410-lines-of-c.md", "text": "https://wpnews.pro/news/mincdp-give-claude-eyes-and-hands-in-a-browser-in-410-lines-of-c.txt", "jsonld": "https://wpnews.pro/news/mincdp-give-claude-eyes-and-hands-in-a-browser-in-410-lines-of-c.jsonld"}}