odda
odda is a toolkit for agent-driven web security research, composable from a single MCP server. It gives an AI agent a real Chrome it can drive and observe, a transparent mitmproxy that writes every flow to disk, and a raw-HTTP send path for smuggling and race conditions.
- Drive Chrome — open, navigate, run JS, screenshot (optionally annotated with numbered markers + a ref/box legend), and interact with the page by accessibility-tree refs.
- Capture every flow — a transparent proxy writes every request/response to disk, including media bodies browser capture drops.
- Observe JS in progress — wrap functions/properties, plant source logpoints, record block coverage.
- Craft raw HTTP — byte-faithful sends for smuggling, parser-differentials, and races.
Requirements
- Python 3.14+
- Chrome or Chromium installed and on
PATH
Installation
uv tool install odda
This installs the odda command (an MCP server plus one helper subcommand, with --version for the version probe). Then register the server with your harness — one MCP config entry, and every odda capability becomes typed tools your agent can call.
Harness configuration
All configs spawn the same stdio server: odda mcp. One MCP process = one odda session with its own Chrome, proxy, and .odda/ data dir (created lazily under the process's working directory on first use).
Claude Code — project .mcp.json (or ~/.claude.json under mcpServers):
{
"mcpServers": {
"odda": {
"command": "odda",
"args": ["mcp"]
}
}
}
OpenCode — ~/.config/opencode/opencode.json:
{
"mcp": {
"odda": {
"type": "local",
"command": ["odda", "mcp"]
}
}
}
pi — install the MCP adapter once (pi install npm:pi-mcp-adapter), then add to project .mcp.json (same mcpServers shape as Claude Code):
{
"mcpServers": {
"odda": {
"command": "odda",
"args": ["mcp"]
}
}
}
Chrome profile (one-time)
To give odda's isolated browser sessions a base profile (cookies, extensions, preferences):
odda init-chrome-profile
This opens a visible Chrome window pointed at odda's base profile directory. Log in, install extensions, and set preferences as you want them; close the window when done. odda copies this profile into each isolated browser session opened by browser_open. Skip this step to start from a clean profile each time.
Usage
Agents drive odda through the MCP tools (browser_open, navigate, page_snapshot, request_send, ...). IDs are integers, monotonic within a session, and never reused; every browser/tab tool takes explicit browser_id / tab_id parameters.
A typical session:
browser_open → {browser_id, tab_id}
navigate → tab loads https://target.example/
read .odda/flows/flows.jsonl → every request/response captured on disk
request_clone + request_send → the Burp Repeater loop, as tools
All captured traffic is stored under .odda/flows/<id>/ — request bytes, response body, headers, timing — plus the append-only flows.jsonl index. Grep the index to find flows; read .odda/flows/<id>/response_body.* for the body (including image/video/audio/font Content-Types that browser capture drops).
Features
Drive Chrome
A real Chrome (via patchright/Playwright), headless by default:
- Open / navigate / eval / wait-for / screenshot — drive the browser and run JS in the page.
- Ref-driven page interaction —
page_snapshotreturns the accessibility tree with[ref=eN]tags; pass the ref topage_click/page_fill/page_hover/page_upload.page_snapshotcaps render depth — when the tree actually reaches the cap, cut points carry their hidden subtree depth ([deeper=k]) and the result ends with a note stating the real depth;page_findregex-searches the snapshot and returns matches with context instead of the whole tree (the cheap way to locate a ref on a large page);diff: truereturns only the-/+lines that changed since the tab's previous same-depth snapshot (fresh refs on+lines are directly actionable; scrolling never reads as change; navigation clears the baseline);boxes: trueadds element geometry for coordinate clicks.page_clickandpage_hoveralso accept viewport coordinates as a raw trusted event for targets the a11y tree can't name.screenshot(annotate=true)draws a numbered marker on every ref's box and returns a CSV legend mapping each number to its ref and viewport box — pixels, refs, and coordinates in one artifact for coordinate targeting (canvas, overlays, anything the a11y tree can't name). Cross-iframe is transparent. - Per-browser userscripts — JS that auto-runs at
document_starton every navigation, before the page's own scripts. - Dialog blocking —
alert/confirm/prompt/beforeunloadstay open until handled: the action that opens one returns its details, other tools on that tab wait, and every same-browser result lists open dialogs. Resolve withdialog_handle(accept/dismiss, prompt text); a human closing it in a headed window works too.
Capture every flow
A transparent mitmproxy sits between Chrome and the network; driving the browser is traffic capture. Every request/response is saved under .odda/flows/<id>/; the index is .odda/flows/flows.jsonl. The proxy can also chain through an upstream forward proxy (proxy_upstream_set) to change the session's network vantage. See the odda://docs/flows resource for the file layout and schema.
Observe JS in progress
Three lenses, chosen by what you know:
- Wrap — wrap a named function or property; record each call/access with
this,args,ret, andstack. Takes effect on the next navigation. - Logpoint — plant a non-pausing observation at a source
url+line+col; the expression is evaluated in the paused frame's scope, so it reads locals by name. - Coverage — record which code blocks execute across one or more navigations; start, trigger behavior, snapshot or stop.
Wrap and Logpoint records wipe on navigation — dump before navigating again. The odda://docs/dynamic-analysis resource documents the full surface; odda://docs/recipes has a worked example tracing untrusted DOM data to a sink.
Craft raw HTTP
Byte-faithful raw HTTP sends, bypassing the browser — the Burp Repeater model, as tools:
- Wire-verbatim HTTP/1.1 — the request file is the wire; nothing is re-framed.
- HTTP/2 frame-source — the request file is parsed into H2 frames; a custom line terminator lets a literal CRLF live inside an H2 pseudo-header for downgrade smuggling.
- Clone or craft —
request_clonecopies a captured flow's exact bytes;request_newstarts an empty file. - Multi-name pipeline — pass several names to send several requests on one connection (H1 keep-alive, or H2 concurrent stream-multiplex) for same-connection attacks.
- Concurrent send —
repeatfires N copies of one request concurrently for race conditions and limit-overrun attacks. fix_content_length— recompute Content-Length after body edits.
See the odda://docs/request-crafting resource for framing details.
How it works
odda mcpis the only odda process: a stdio MCP server whose lifespan owns the proxy, the browser manager, and the flow storage. Your harness spawns it per agent session; closing the session tears everything down.- The remaining CLI surface is helpers, not the automation surface:
odda init-chrome-profile(interactive, human-run) andodda --version/-V. - Project state lives in
.odda/under the MCP process's working directory:flows/,requests/,browsers/. It's created lazily on the first state-producing call, not when the server boots. - Errors from tools are odda's messages verbatim (as tool errors); unanticipated crashes log their traceback to the server's stderr, which the harness captures.
Release files for odda 0.54.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| odda-0.54.2.tar.gz | 123.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| odda-0.54.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 258.7 kB
Release files / odda-0.54.2.tar.gz
| Download URL | odda-0.54.2.tar.gz |
|---|---|
| Size | 123.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
359b47bb45cc07ed455e8ee16662394c4a3dc3809d27f51d38ace71f0a5c4aa1
|
|
BLAKE2b-256 checksum How to use checksums |
073c418f93e077183639cfe526e1edb73198059d397b4f0240f8c4f81a55c7ca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.
Transparency logRelease files / odda-0.54.2-py3-none-any.whl
| Download URL | odda-0.54.2-py3-none-any.whl |
|---|---|
| Size | 134.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8fc51cf4faed14d6189f8b14e278687ff3f404f66a50b2268102417f0440c4c8
|
|
BLAKE2b-256 checksum How to use checksums |
139a2fc36901036a256d9875c1aae2d28431082b50f7f376a41f29fa30f38c60
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.
Transparency log