# `PhoenixDevToolbar.Plug`
[🔗](https://github.com/zoten/phoenix_dev_toolbar/blob/0.0.2/lib/phoenix_dev_toolbar/plug.ex#L1)

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
    end

No 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` - the `conn.assigns` key holding a CSP nonce for the
    injected script tag. Defaults to `:csp_nonce`. A nonce based
    `script-src` needs 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 sid `PhoenixDevToolbar.Report`
    returns, or the `data-pdt-sid` of 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](https://github.com/zoten/phoenix_dev_toolbar/blob/main/adr/14-standalone-panel-and-row-attributes.md).

## 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]                   # anyone

Anything 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](https://github.com/zoten/phoenix_dev_toolbar/blob/main/adr/15-mcp-endpoint.md).

## 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`).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
