The whole toolbar integration: tracks page requests, injects the toolbar into HTML responses, and serves the toolbar's own assets and panel.
Add it to your endpoint, next to Phoenix.LiveReloader:
if code_reloading? do
plug PhoenixDevToolbar.Plug
endNo router changes are needed: the panel is a LiveView rendered by this plug
under :path, and it connects through its own socket at <path>/live, not
through the host's LiveSocket or its app.js. This keeps the package
installable with only: :dev, since nothing outside the code_reloading?
block references it.
Options
:path- where the toolbar lives, an absolute path below the root. Defaults to"/__pdt". The panel, its assets, its socket and the export are served under it.:nonce_assign- theconn.assignskey holding a CSP nonce for the injected script tag. Defaults to:csp_nonce. A nonce basedscript-srcneeds the same nonce in the assign:# in a plug before this one, or in your CSP plug conn = Plug.Conn.assign(conn, :csp_nonce, nonce) # or, if your plug stores it elsewhere plug PhoenixDevToolbar.Plug, nonce_assign: :my_nonce
Standalone panel
The panel also opens in a browser tab of its own, for example from a browser-driving agent:
<path>/panel?path=/posts- the newest page whose path is exactly/posts<path>/panel- the newest page<path>/panel?sid=<sid>- one page, by the sidPhoenixDevToolbar.Reportreturns, or thedata-pdt-sidof the toolbar's script tag on the page
A page is the request and the LiveView it mounted, as the toolbar shows it.
The page is picked when the panel loads: reload the panel for a newer one.
?path= matches the path the page started at (a live patch keeps its
LiveView's mount path).
?path= and the newest page find a page without its sid, so they follow the
MCP client rule below: this machine unless mcp: [clients: ...] says
otherwise. Other clients get an empty panel that says why; ?sid= works for
any client the access rules let in.
Rows carry stable data-pdt-* attributes with raw values. Booleans are
present only when true; missing values are left out.
| Element | Attributes |
|---|---|
#pdt-panel | data-pdt-root (root sid), data-pdt-sids (comma separated) |
| tab section | data-pdt-tab (plugin id); hidden tabs are in the DOM too |
| every event row | data-pdt-row, data-pdt-sid, data-pdt-seq, data-pdt-duration-us |
query row | data-pdt-source, data-pdt-error, data-pdt-n-plus-one |
n-plus-one row | data-pdt-count, data-pdt-callbacks |
callback row | data-pdt-callback, data-pdt-event, data-pdt-error |
render row | data-pdt-error |
client row | data-pdt-event (the phx:* type) |
request row | data-pdt-kind, data-pdt-method, data-pdt-status, data-pdt-error |
profile row | data-pdt-callback, data-pdt-count, data-pdt-total-us, data-pdt-max-us |
#pdt-empty | data-pdt-lookup: sid, path, newest or gated |
The decision is ADR 14.
MCP server
<path>/mcp is an MCP server (Streamable HTTP, MCP 2025-03-26 to 2026-07-28)
with read-only tools over PhoenixDevToolbar.Report: list_pages,
page_summary, page_queries, page_callbacks, page_n_plus_one and
page_errors. By default it answers only clients on this machine (a loopback
peer or one of the machine's own addresses, no proxy headers), with an allowed
Host and no foreign Origin. It is on with the toolbar:
config :phoenix_dev_toolbar, mcp: false # off
config :phoenix_dev_toolbar, mcp: [clients: ["192.168.1.0/24"]] # also these
config :phoenix_dev_toolbar, mcp: [clients: :any] # anyoneAnything on this machine that relays to the dev server (an SSH forward, socat,
a local proxy without X-Forwarded-* headers, a VM or container with
user-mode NAT) counts as this machine. Client setup is in the README, the
protocol details in
ADR 15.
Access rules
The panel, its socket, the export and the MCP endpoint answer only a request whose Host cannot
be rebound: an IP address, localhost, your endpoint's url: [host: ...] or a
name listed in config :phoenix_dev_toolbar, allowed_hosts: [...]. The socket
also refuses a missing Origin, or one that differs from the host the request
was sent to, unless it is listed in allowed_origins: [...] (for a proxy or
tunnel that rewrites the Host header). The MCP endpoint and the standalone
panel's sid-less lookups add the client rule of the "MCP server" section;
process actions (actions:) and the Eval plugin's code use the same
"this machine" rule (:local). The README's "Docker or LAN access" section
sums it all up.
The plug does nothing when the toolbar is disabled
(config :phoenix_dev_toolbar, enabled: false).