djust-browser-mcp
djust-browser-mcp is a sibling of djust's in-app MCP server.
The in-app MCP runs inside your Django process and lets agents introspect
server-side state — view assigns, handler timings, the last traceback.
djust-browser-mcp runs as a Chrome extension paired with a local Node
MCP server and lets agents interact with the running browser — clicking,
typing, recording, replaying, asserting, benchmarking, throttling
WebSockets, snapshotting the DOM. 53 tools, sub-100 ms interaction
latency, no Playwright in the loop.
Use it when you want an agent to:
- author a regression test by interacting once and saving the recording,
- verify a multi-user broadcast flow by spawning N tabs and watching them stay in sync,
- catch a perf regression by benchmarking an event before and after a PR,
- prove a click hit djust's Rust fast path,
- simulate flaky networks (latency + drop rate) without
tc qdisc, - scan accessibility violations as part of CI,
- find dead
dj-click="..."template handlers whose Python methods got renamed.
Why a separate MCP
| In-app MCP | djust-browser-mcp | |
|---|---|---|
| Runs in | Django process | Chrome extension + Node server |
| Sees | Server state, view assigns, handler timings, tracebacks, SQL queries | DOM, mutations, WebSocket frames, console, network, screenshots |
| Drives | Code generation, framework introspection, project structure queries | Real user interactions in a real browser |
| Install | pip install 'mcp[cli]', djust mcp install | npm install, npm run build, load Chrome extension, claude mcp add |
| Pairs with | djust-browser-mcp | In-app MCP |
The two are complementary. In a typical AI-driven debugging session both are connected: the agent uses the in-app MCP to read state and the last traceback, and uses djust-browser-mcp to reproduce the user's interaction in the browser.
Why not Playwright
The README's speed table:
| Operation | Playwright | djust-browser-mcp |
|---|---|---|
| Click a button | ~1–3 s | ~50–100 ms |
| Read element state | ~1–2 s | ~20–50 ms |
| Fill a form field | ~1–2 s | ~30–50 ms |
| Check a WebSocket frame | n/a | ~10 ms |
| Wait for VDOM patch | ~5–15 s (poll) | ~100 ms (event-driven) |
| Screenshot | ~2–3 s | ~200 ms |
Three reasons it's faster:
- Smart patch waiting via VDOM version. Playwright polls; djust knows when the next VDOM patch landed because the framework increments a version counter on each one. The MCP waits on that counter changing, not on a CSS-selector-becoming-visible timeout.
- Real WebSocket frame visibility. Playwright cannot read the
WebSocket frames djust uses for state sync. djust-browser-mcp's
page-worldinjection script hooksWebSocket.prototypeand captures every frame in a ring buffer. - In-process Chrome. No protocol round-trip to a remote browser,
no driver layer. The extension talks to the page via direct
content/page-world JS and to the MCP server via a single
ws://127.0.0.1:9377connection.
The trade-off: Chrome only, dev-server only, no headless, no cross-browser. See Limitations.
Architecture
Claude / Cursor / Windsurf
│ stdio (MCP protocol)
▼
MCP server (Node.js, port 9377) ← src/index.js, registers 53 tools
│ WebSocket
▼
Chrome extension
├── background.js ← message router, tab management,
│ chrome.tabs.captureVisibleTab
├── content.js (isolated) ← DOM access, clicks, MutationObserver
└── injected.js (page world)← hooks WebSocket constructor + fetch/XHR,
reads window.djust state
│
▼
Your djust app on http://localhost
│
▼
djust observability endpoints (optional)
/_djust/observability/handler_timings/
/_djust/observability/last_traceback/
/_djust/observability/sql_queries/
/_djust/observability/log/
The two-channel design is deliberate: DOM and screenshots travel via
Chrome extension APIs (which need elevated permissions); WebSocket
frames and window.djust state travel via the page-world injection
(which doesn't, but can't captureVisibleTab). The MCP server
multiplexes both.
Install
Prerequisites
- Chrome (Manifest V3)
- Node.js 18+
- A djust project running on
http://localhost:<port>withDEBUG=True - Claude Code, Cursor, Windsurf, or any MCP-capable client
One-time setup
# 1. Build the MCP server
cd ~/online_projects/ai/djust-browser-mcp
npm install
npm run build # produces dist/index.js
# 2. Register the MCP server with Claude Code
claude mcp add djust-browser -- node ~/online_projects/ai/djust-browser-mcp/dist/index.js
Equivalent manual config in ~/.claude/settings.json:
{
"mcpServers": {
"djust-browser": {
"command": "node",
"args": ["/Users/YOU/online_projects/ai/djust-browser-mcp/dist/index.js"]
}
}
}
For Cursor: same JSON, dropped into Cursor's MCP settings.
Load the Chrome extension
- Open
chrome://extensions - Toggle Developer mode (top right).
- Click Load unpacked → pick
~/online_projects/ai/djust-browser-mcp/extension/.
The extension's popup should now show Connected when your djust dev
server is running. The MCP server picks the first free port between
9377 and 9386 (override with DJUST_MCP_PORT=...).
Start using it
# Start your djust dev server (DEBUG=True)
make dev
# Open http://localhost:<port> in Chrome (tools default to localhost tab targeting)
# Open Claude Code in a connected project
# Ask: "use the djust-browser MCP to take a dom_snapshot of the page"
If the agent reports the tool isn't available, the connection is broken — see Troubleshooting.
Tool catalog
53 tools, grouped by intent. Every tool returns JSON; agents compose them freely.
DOM inspection (6)
dom_snapshot— Full structured tree with attributes, computed styles, bounding boxes, template-source mapping. Params:selector?,include_styles?,max_depth?.inspect_element— Deep single-element inspection: attrs, styles, bound events, visibility. Params:selector.query_selector— Find elements by CSS selector. Params:selector,limit?.find_by_template— Find elements rendered from a specific template file (usesdata-dj-srcmarkers, DEBUG-only). Params:template,line?.dom_mutations— Replay attribute and node changes captured byMutationObserver. Params:since?,limit?,type?,enabled?.dom_changes— VDOM patches reconstructed from observed WebSocket frames (the patches the server sent), useful when correlating visual changes with handler activity. Params:since?,limit?.
Interaction (7)
click— Click with smart VDOM-version waiting; doesn't return until the resulting patch lands. Params:selector,wait_for_patch?.type_text— Type into a field with properinput/changeevents. Params:selector,text,clear_first?,trigger_events?.select_option— Pick a<select>option. Params:selector,value.submit_form— Fill multiple fields and submit in one call. Params:selector,fields?,submit?.navigate— Drive the URL bar. Params:url.scroll_to— Scroll an element into view. Params:selector?,position?.highlight_element— Visual red-border overlay for confirming the agent picked the right element. Params:selector,duration?.
WebSocket (4)
ws_frames— Captured frames (in/out) with ring-buffer cap. Params:limit?,direction?,since?,event_name?.ws_status—readyState, frame counts, current VDOM version.ws_diagnostics— Auto-detect duplicate events, missing responses, large payloads, high event rates.ws_inject— Inject a synthetic frame as if the server sent it (useful for testing presence updates without a second user). Params:data(string | object).
State (3)
djust_state— Current loading-manager state, bound events, VDOM version, components. Params:scope?(all|loading|events|vdom|components).djust_state_diff— Snapshot before / after an action; report only what changed. Params:trigger_selector,observe_selector?,include_keys?,timeout_ms?.djust_eval— Run JS in page context, scoped towindow.djust. Params:expression.
Performance (3)
benchmark_event— Measure click → DOM-settled latency, return min / p50 / avg / p90 / max. Params:trigger_selector,observe_selector?,samples?,warmup?,gap_ms?,timeout_ms?.benchmark_compare— Run two benchmarks back-to-back and report the ratio. Params:a: {trigger_selector, observe_selector?},b: {…},samples?.verify_fast_path— Assert a click hit djust's Rust fast path (parse_msbelow threshold). Params:trigger_selector,observe_selector?,expect?,fast_threshold_ms?,timeout_ms?.
Assertion (2)
assert_event_dispatched— Click and verify the server received the expected event with expected params. Params:trigger_selector,expect_event,expect_params?,timeout_ms?.assert_no_console_errors— Scan the console buffer for error-level entries since a timestamp. Params:since_ms?,patterns_to_ignore?.
Diagnostics (4)
trace_event— Single-click forensic timeline: click → WS send → WS recv → DOM mutation → DOM settled. Params:trigger_selector,observe_selector?,timeout_ms?.client_perf_trace—PerformanceObserverintegration: long tasks, layout shifts, custom marks. Params:trigger_selector,observe_selector?,settle_ms?,timeout_ms?,mark_prefix?.stress_test— Fire events at a fixed rate, match SENT vs RECEIVED frame counts. Params:trigger_selector,rate_hz,duration_s,observe_selector?,settle_timeout_ms?.ws_throttle— Add outgoing-frame latency / drop rate. Useful for reproducing flaky-network bugs withouttc qdiscor a proxy. Params:delay_ms?,drop_rate?,clear?.
Recording / replay / fixtures (8 tools)
record_start— Begin recording user-driven interactions. Params:include_events?.record_stop— End recording; returns the action list.replay_actions— Replay an action list at original or accelerated speed. Params:actions,speed_multiplier?,timeout_ms?.save_fixture— Persist an action list to~/.djust-browser-fixtures/<name>.json. Params:name,actions,url?,description?,overwrite?.load_fixture— Read a saved fixture by name. Params:name.list_fixtures— Enumerate every saved fixture.run_fixture_suite— Replay a glob of fixtures with assertions between them. Params:pattern?,assert_console_clean?,inter_fixture_delay_ms?,speed_multiplier?,stop_on_failure?,patterns_to_ignore?.regression_suite— Compose checks ad-hoc (event + state + fast-path- console + a11y) without persisting them as fixtures first.
Params:
checks,stop_on_failure?,a11y_impact_at_least?.
- console + a11y) without persisting them as fixtures first.
Params:
Accessibility / visual (2)
accessibility_scan— Runaxe-coreagainst the page or a region. Params:selector?,include_tags?,exclude_tags?,impact_at_least?.visual_diff— Screenshot before / after an action, pixel-diff, return change ratio. Params:trigger_selector,observe_selector?,settle_ms?,timeout_ms?,threshold?.
Multi-session (1)
multi_session_simulate— Spawn N tabs (1–20), each clicking M times, collect aggregated stats. Params:url,count,trigger_selector,clicks_per_tab?,per_click_delay_ms?,hold_ms?.
Introspection (4)
list_event_handlers— Enumerate everydj-click/dj-submit/dj-change/dj-input/dj-keydown/dj-keyupin the current DOM. Params:include_disabled?.find_dead_bindings— Cross-reference DOM handlers against the view's declared handler list; flag handlers in the template that no longer exist on the view. Params:view_handlers,framework_handlers?.inspect_source— Match a DOM element back to the template file + line that rendered it (DEBUG-only; reads the template from disk). Params:selector,template_file_path,context_lines?.check_dj_id_stability— Verifydj-idmappings don't change unexpectedly across re-renders. Params:trigger_selector?,observe_selector?,settle_ms?.
Utilities (9)
console_messages— Browser console buffer (filter by level / regex).network_requests— Fetch + XHR ring buffer (filter by URL pattern / status).page_info— URL, title, djust-detected, dev-server-running.wait_for— Wait for a selector / text / WS event.screenshot—chrome.tabs.captureVisibleTabPNG (returned as base64).list_tabs— Enumerate localhost tabs Chrome can see.set_target_tab— Explicitly pick which tab the tools act on (matters when multiple djust apps are open).memory_snapshot— Heap stats + DOM node count.summarize_session— Token-efficient digest of the last N ms across WS frames + console + DOM mutations + (optional) djust observability endpoints.
Tally: 6 (DOM) + 7 (Interaction) + 4 (WebSocket) + 3 (State) + 3 (Performance) + 2 (Assertion) + 4 (Diagnostics) + 8 (Recording / replay / fixtures) + 2 (A11y / visual) + 1 (Multi-session) + 4 (Introspection) + 9 (Utilities) = 53 tools across 14 modules.
Workflows
A. AI-authored regression test (record → save → replay)
The pitch: stop writing test scaffolding. Show the agent the bug once, have it record what you did, save it as a fixture, then replay-with- asserts in CI.
agent → record_start({include_events: true})
[user (or agent) clicks through the bug repro]
agent → record_stop() → returns {actions: [...]}
agent → save_fixture({
name: "checkout-discount-bug",
actions: <the recording>,
url: "/checkout/",
description: "Discount stacking regression #1234",
})
[later, in CI:]
agent → run_fixture_suite({
pattern: "checkout-*",
assert_console_clean: true,
stop_on_failure: true,
})
Fixtures land at ~/.djust-browser-fixtures/<name>.json and are
plain JSON — commit them, share them, replay them at any speed.
B. Multi-user flow test
Verify presence avatars render correctly when 5 users join at once, or
that a LISTEN/NOTIFY broadcast hits every connected reader.
agent → multi_session_simulate({
url: "http://localhost:8000/dashboard/",
count: 5,
trigger_selector: "[dj-click=\"add_widget\"]",
clicks_per_tab: 10,
per_click_delay_ms: 200,
hold_ms: 1500,
})
Returns aggregated stats: per-tab click counts, drop rate, latency distribution, any stuck loading indicators. Catches: race conditions in handler logic, broadcast misses, rate-limiting bugs, session-isolation leaks.
C. Performance regression catch
Land a PR that touches a hot path? Benchmark before and after.
agent → benchmark_event({
trigger_selector: "[dj-click=\"place_order\"]",
observe_selector: "#order-confirmation",
samples: 50,
warmup: 5,
}) → {min_ms, p50_ms, avg_ms, p90_ms, max_ms}
[on the PR branch:]
agent → benchmark_compare({
a: {label: "main", trigger_selector: "..."},
b: {label: "PR-branch", trigger_selector: "..."},
samples: 50,
}) → {ratio: 1.34, regression: true}
# And to confirm the Rust fast path actually fired:
agent → verify_fast_path({
trigger_selector: "[dj-click=\"place_order\"]",
fast_threshold_ms: 2,
})
D. WebSocket resilience test
Simulate a flaky mobile connection without tc qdisc or a proxy.
agent → ws_throttle({delay_ms: 500, drop_rate: 0.1})
agent → click({selector: "[dj-click=\"submit_form\"]"})
agent → ws_status() → readyState, frame counts, stuck loaders
agent → ws_inject({ # synthetic server push
data: {type: "presence", users: [...]}
})
agent → trace_event({ # single-click forensic timeline
trigger_selector: "[dj-click=\"reload_dashboard\"]",
timeout_ms: 8000,
})
agent → ws_throttle({clear: true}) # reset
E. Accessibility baseline
Run axe-core in CI and fail builds on regressions.
agent → accessibility_scan({
include_tags: ["wcag2aa"],
impact_at_least: "serious",
}) → {violations: [...]}
Or compose into a regression suite that asserts a11y and event correctness in one pass:
agent → regression_suite({
checks: [
{name: "open-dialog",
trigger: "[dj-click=\"open_settings\"]",
assert_event: "open_settings",
assert_a11y_clean: true},
{name: "save-form",
trigger: "[dj-click=\"save\"]",
assert_event: "save",
assert_state_changed: true,
assert_console_clean: true,
assert_a11y_clean: true},
],
a11y_impact_at_least: "serious",
stop_on_failure: true,
})
F. Dead-binding sweep
Find dj-click="foo" template handlers whose Python methods got
renamed or deleted.
# Get the canonical handler list from the in-app MCP:
agent → (in-app) get_view_schema({view_name: "DashboardView"})
→ {handlers: ["refresh", "filter_by", "export_csv", ...]}
agent → find_dead_bindings({
view_handlers: <the canonical list>,
framework_handlers: ["validate_field"],
})
→ {
dead_bindings: [
{selector: "button[dj-click=\"refesh\"]", handler: "refesh"},
...
],
unreachable_handlers: ["export_pdf"],
}
The agent tells you exactly which template attributes need editing. The two MCPs compose naturally — the in-app MCP knows the authoritative handler list from Python introspection; djust-browser-mcp knows what's actually wired in the rendered DOM.
Pairing with the in-app MCP
Best results come from running both. Example: an exception fires while a user is in a wizard.
- In-app MCP →
get_last_traceback()— see the server-side stack. - In-app MCP →
get_handler_timings()— confirm which handler ran. - djust-browser-mcp →
summarize_session({window_ms: 60000})— get the last minute of WS frames + console + mutations on the client. - djust-browser-mcp →
record_start(), ask the user to redo the action,record_stop()+save_fixture()— now you have a regression repro. - djust-browser-mcp →
replay_actions()with the wizard reset to step 1 — verify the fix.
summarize_session({include_server: true}) automatically pulls from
the in-app observability endpoints if your djust app has them mounted
— one tool call to get both halves.
Limitations
- Chrome only. Manifest V3 extension. No Firefox, no Safari, no Edge story shipping.
- DEBUG-only template source mapping.
find_by_templateandinspect_sourcerely on thedata-dj-srcmarkers djust emits whensettings.DEBUG = True. They can't run against production builds. - Dev-server intent, broad permission grant. The extension manifest
declares
<all_urls>host permissions becausechrome.tabs.captureVisibleTabrequires it. In practice the tools default to localhost tab targeting (set_target_tabdefaults to the first localhost tab). Treat it as a dev tool — install it in a dev Chrome profile, not your everyday browser. - Not headless. Screenshots use
chrome.tabs.captureVisibleTab, which requires a visible window. CI usage means a Chrome session in aXvfbor similar virtual display. - Single-session MCP server. One Chrome extension talks to one MCP server process at a time (single WebSocket upgrade).
- In-memory ring buffers. WS frames, console messages, network requests, and DOM mutations are kept in last-N buffers (50–500 entries depending on type). Page reload clears them.
- Accessibility scans depend on CSP.
accessibility_scanloads axe-core fromcdnjs.cloudflare.com. Pages with strict CSP that blocks the CDN will fail the scan; whitelist or load axe-core locally. - Templates must be filesystem-accessible.
inspect_sourcereads template files from disk — the MCP server needs read access.
These are deliberate trade-offs against Playwright. The 20–30× speedup above buys them.
Troubleshooting
"Tool not available" in Claude
The MCP server didn't start, or the registration is wrong.
# Confirm the server runs standalone
node ~/online_projects/ai/djust-browser-mcp/dist/index.js
# Should log "djust-browser-mcp listening on ws://127.0.0.1:9377"
# Confirm the registration
claude mcp list # should show "djust-browser"
# Restart Claude Code after registration changes
Extension popup says "Disconnected"
The extension can't reach the MCP server.
- Check
~/.claude/settings.jsonfor the right path todist/index.js. - Check whether something else is holding port 9377:
lsof -i :9377. Kill it or override withDJUST_MCP_PORT=9378. - Confirm Chrome's extension page shows no errors (
chrome://extensions, toggle Developer mode, look for red badges).
"Page not detected as djust"
By default set_target_tab picks the first localhost tab. If you're
proxying via a hostname like app.local, pass that tab explicitly via
set_target_tab({url_pattern: "app.local"}) or by tab_id.
find_by_template and inspect_source return nothing
data-dj-src markers are DEBUG-only. Confirm settings.DEBUG = True
and that you're hitting the dev server (not a built static deploy).
accessibility_scan fails with CSP error
axe-core loads from https://cdnjs.cloudflare.com. Add it to your CSP
script-src allowlist for the dev server, or vendor axe-core locally
(see the README's "Self-hosting axe-core" section).
Recording captures nothing
record_start only captures real interactions, not synthetic ones from
other tools. To record an agent-driven flow, drive the interactions
through click / type_text / etc. between record_start and
record_stop — these do capture into the recording.
CI fails with "no display"
screenshot and visual_diff need a visible Chrome window. Run under
Xvfb:
xvfb-run --server-args="-screen 0 1920x1080x24" \
npm test
See also
- MCP Server — the in-app sibling MCP for server-side state, handler timings, and traceback access.
- Time-Travel Debugging — pairs
with
record_start/replay_actionsfor stateful repro. - Hot View Replacement — keeps recorded fixtures viable across template edits.
- Source:
djust-browser-mcpon GitHub — README, SPEC, ROADMAP, the 14 tool modules.