ub-debugger, the CLI from the@unbound-app/debuggernpm package. It hosts a WebSocket server (the bridge) and draws the REPL.- The debugger built-in in the client. When enabled, it dials the bridge, runs the code it receives, and forwards console output.
Running the debugger
Start the bridge on your computer, then point the phone at it.ub-debugger is published on npm as @unbound-app/debugger and needs Bun, because it uses Bun’s WebSocket server and file APIs.
1
Start the bridge
Run the package with To keep
bunx. It listens on port 9090 unless you pass --port.ub-debugger on your PATH instead, install it globally:2
Note the device address
On startup the CLI prints a banner with two addresses:
Devicesis<LAN IP>:<port>. This is what the phone connects to.MCPisws://127.0.0.1:<port>?mcp, for other tools on the same computer (see other tools on the bridge).
3
Enable the debugger on the device
On the phone, turn on the debugger and point it at the
Devices address, as described in connecting a device. The terminal prints Device connected and shows the » prompt.--help to list the flags. --port is the only one.
The
Devices address is the first non-internal IPv4 address the CLI finds. If your computer has several network interfaces (a VPN, Docker, WSL), it may print one the phone can’t reach. Use the address of the interface the phone shares a network with.Connecting a device
Turn ondebugger.enabled and set debugger.address to the bridge’s Devices address. Both keys are in the unbound store of settings.json. See the settings reference for where that file lives and how keys map to it.
To change them from the app instead:
- In Unbound’s General settings, turn on Developer Mode.
- Open the Developer screen.
- In the Debugger section, turn on Enable Debugger.
- Enter the address in Debugger Address.
boolean
default:"false"
Starts the debugger built-in. The client checks this key once at startup, so turning it on takes effect the next time the app starts. Turning it off while the app is running disconnects from the bridge right away and stops forwarding logs.
string
default:"''"
The bridge’s
host:port, for example 192.168.1.20:9090. Leave out the scheme: the client connects to ws://<address>.When this is empty, the client uses the address baked into the bundle at build time. Development builds (UNBOUND_DEV=1 or --dev) bake in the dev host on port 9090, so they usually work without setting this key. Production builds bake in an empty address, so you have to set it.Changing the address while connected closes the socket and reconnects to the new address.- If the socket fails or closes, it retries every 2 seconds.
- When the app goes to the background, it closes the socket and stops retrying. It reconnects when the app becomes active again.
Using the REPL
Type JavaScript at the» prompt and press Enter. The client evaluates it in the global scope of Discord’s runtime and sends back the result, which is printed after «. Errors are printed in red.
How results are shown
The client converts the result to text before sending it:- Strings are printed as-is, without quotes.
- Functions print as
[Function name], because Hermes can’t return function source. - Errors print as
Name: messagefollowed by the stack trace. undefinedprints asundefined.- Everything else goes through
JSON.stringifywith 2-space indentation. Values it can’t serialize, such as objects with circular references, fall back toString(value), which usually prints[object Object].
Console output
While connected, every line Discord’s runtime logs (allconsole.* calls) appears in the terminal, prefixed with «. Lines are coloured by level: grey for logs, yellow for warnings, red for errors.
Output logged before the device connects is not buffered or replayed.
History
Press the up and down arrow keys to recall earlier inputs. Every line you evaluate is saved to a.history.json file, so history carries over between sessions.
For an npm install, the file is written to the node_modules/@unbound-app folder that contains @unbound-app/debugger.
Press Ctrl+C to quit.
Other tools on the bridge
Other programs on your computer can connect to the bridge as controllers atws://127.0.0.1:<port>?mcp. Any number of controllers can attach at once. A controller can:
- Evaluate code on the device. The terminal mirrors the request in magenta, tagged
[mcp], followed by the result. - Receive the device’s console output and connection status.
- Push a freshly built plugin bundle and manifest. The device reloads that plugin and shows a Hot reload toast, or Hot reload failed with the error message.
MCP connected and MCP disconnected with the number of attached controllers.
Wire protocol
@unbound-app/debugger-protocol defines the messages shared by the bridge, the client, and controllers. Every frame is a JSON object with a type field. Frames that don’t parse or have an unknown type are dropped.
The
id on a request is echoed on its result, which is how the bridge routes each result back to the controller that sent the request.
Troubleshooting
The terminal never shows "Device connected"
The terminal never shows "Device connected"
- Check that
debugger.enabledwas on when the app started. Turning it on later only applies after a restart. - Check the address. The phone must reach the computer’s LAN IP on the bridge’s port.
- In a production build, check that
debugger.addressis set. If it’s empty, the client logsNo debugger address configured; set debugger.address in developer settings.and doesn’t connect.
"A device is already connected."
"A device is already connected."
The bridge accepts one device at a time. A second device is refused with HTTP
503 and this message until the first one disconnects."No device is connected to the debugger."
"No device is connected to the debugger."
The bridge has no device socket, so there’s nothing to run the code on. Wait for
Device connected before evaluating."Device disconnected before the evaluation completed."
"Device disconnected before the evaluation completed."
The device dropped while code was running, for example because the app went to the background. Every pending evaluation fails with this message. The client reconnects when the app is active again.
"Parsing failed on message from device"
"Parsing failed on message from device"
The bridge received a frame that isn’t valid JSON or isn’t a known message type. It ignores the frame and keeps the connection open.