fdb - Flutter Debug Bridge

pub.dev version pub.dev likes GitHub stars License: MIT

Give your AI agent eyes and hands on your Flutter app.

fdb is a CLI that lets AI coding agents launch, hot reload, screenshot, inspect, and kill Flutter apps running on real devices and simulators -- all from the terminal, no IDE required.

Claude Code  ·  OpenCode  ·  Cursor  ·  any agent that can run bash

fdb in action

Why fdb?

AI agents are great at writing Flutter code. But they can't see what the app looks like, read runtime logs, or inspect the widget tree.

fdb fixes that:

  • Launch a Flutter app on any connected device or simulator
  • Hot reload / restart after code changes
  • Screenshot so the agent can see the UI
  • Read logs filtered by tag
  • Inspect the widget tree
  • Select widgets by tapping on device

Zero dependencies. Pure Dart. Works on macOS and Linux.

Why not the official Flutter MCP server?

The Dart & Flutter MCP server handles code analysis, pub.dev search, formatting, and can also introspect a running app via the Flutter inspector. fdb does something different: runs the app on a real device and lets the agent interact with it via a bash-native CLI — no MCP client required.

fdb Flutter MCP server
Architecture CLI -- plain bash commands MCP protocol -- requires a compatible client
Context cost Minimal. Agent runs a command, gets text output. MCP tool schemas and responses are injected into context on every call, eating tokens even when unused.
Works with Any agent that can run bash (Claude Code, OpenCode, Cursor, custom scripts, CI) Only MCP-compatible clients
Progressive disclosure Ships a skill file — agent loads best practices on demand, not upfront All tools exposed at once
Device interaction Launch, hot reload, screenshot, logs, widget tree, tap, input, scroll, swipe, back, deeplink, SharedPreferences, clean Widget tree inspection and runtime errors via Flutter inspector (read-only)
Setup dart pub global activate -- done Per-client MCP config (JSON, YAML, or GUI depending on client)

tl;dr: Use the Flutter MCP server for code analysis and package management. Use fdb when your agent needs to run the app, see it, and interact with it.

Install

Using an AI agent? Just tell it:

Fetch and follow instructions from https://raw.githubusercontent.com/andrzejchm/fdb/main/doc/README.agents.md

Or install manually:

dart pub global activate fdb

Requires Dart SDK >= 3.0.0. Make sure ~/.pub-cache/bin is in your PATH.

Or from git (latest main):

dart pub global activate --source git https://github.com/andrzejchm/fdb.git

Quick Start

# Launch your app on a connected device
fdb launch --device <device_id> --project /path/to/your/flutter/app

# Or pass compile-time config through to flutter run
fdb launch --device <device_id> --project /path/to/your/flutter/app \
  --dart-define API_BASE_URL=https://example.com/v1,canary \
  --dart-define-from-file config/dev.json

# Or launch into an interactive fdb session
fdb launch --device <device_id> --project /path/to/your/flutter/app -i

# Or attach to an app already launched from Xcode, Android Studio, or another tool
fdb attach --device <device_id> --project /path/to/your/flutter/app

# See what it looks like
fdb screenshot

# Make a code change, then hot reload
fdb reload

# Check the widget hierarchy
fdb tree --depth 5 --user-only

# Read filtered logs
fdb logs --tag "MyFeature" --last 30

# Done? Kill the app
fdb kill

Commands

Device & app lifecycle

Command Description
fdb devices List connected devices
fdb attach --device <id> --project <path> [--app-id <id>] [--debug-url <url>] [-i] [--verbose] Attach fdb to an already-running debug/profile Flutter app launched from Xcode, Android Studio, simctl, adb, or another native workflow
fdb launch --device <id> --project <path> [--dart-define <k=v>] [--dart-define-from-file <path>] [-i] [--verbose] Launch app, wait for start; use repeatable define flags to pass compile-time config through to flutter run, and -i/--interactive to stay in an fdb REPL
fdb reload Hot reload
fdb restart Hot restart
fdb doctor Pre-flight check for app, VM service, fdb_helper, platform tools, and device state
fdb status Check if app is running
fdb kill Stop app, clean up

Visual inspection

Command Description
fdb screenshot [--output <path>] [--full] Screenshot (all platforms; --full skips downscaling)
fdb logs --tag <tag> --last <n> Filtered logs
fdb syslog [--since <dur>] [--predicate <str>] [--last <n>] [--follow] Native system logs (Android logcat, iOS syslog, macOS log)
fdb crash-report [--app-id <id>] [--last <dur>] [--all] Fetch the most recent OS-level crash record (jetsam, LMK, native crashes)
fdb tree --depth <n> [--user-only] Widget tree
fdb describe Compact screen snapshot: interactive elements + visible text (requires fdb_helper)
fdb select on/off Widget selection mode
fdb selected Get selected widget info

Interaction

Widget-targeted commands require fdb_helper; native-tap and deeplink do not.

Command Description
fdb double-tap --text/--key/--type <selector> [--index N] | --x X --y Y | --at X,Y Double-tap a widget or screen coordinates
fdb native-tap --at x,y Tap native (non-Flutter) UI — system dialogs, permission sheets (Android: adb shell input tap; iOS sim: falls back to in-process tap with a warning). Physical iOS and macOS not supported — use fdb tap --at instead.
fdb tap --text/--key/--type <selector>, --at x,y, or @N Tap a widget, coordinates, or describe ref
fdb longpress --text/--key/--type <selector> [--duration <ms>] or --at x,y Long-press a widget or coordinates
fdb input [--text/--key/--type <selector>] <text> Enter text into a field
fdb scroll <direction> [--at x,y] [--distance px] Scroll in a direction
fdb scroll --from x,y --to x,y Drag gesture between two points
fdb scroll-to --text/--key/--type <selector> [--index N] Scroll until widget is visible
fdb wait --key/--text/--type/--route <selector> --present|--absent [--timeout <ms>] Wait for a widget or route condition without shell polling
fdb swipe <direction> [--key/--text/--type <selector>] Swipe widget (PageView, Dismissible)
fdb back Navigate back (Navigator.maybePop)
fdb deeplink <url> Open a deep link

iOS Simulator (no session required — works against any booted simulator)

Command Description
fdb simulator appearance dark|light|get Toggle or query dark/light mode
fdb simulator push <payload.apns> [--bundle-id <id>] Send a simulated remote push notification
fdb simulator location set <lat,lon> Set a static GPS location
fdb simulator location route <scenario> Play a movement scenario (City Run, City Bicycle Ride, Freeway Drive)
fdb simulator location clear Stop location simulation
fdb simulator text-size <size>|get Set or query Dynamic Type content size (extra-smallaccessibility-extra-extra-extra-large)
fdb simulator status-bar override [--time <str>] [--battery-level <n>] … Override status bar (clean 9:41 screenshots)
fdb simulator status-bar clear Clear all status bar overrides
fdb simulator defaults read [--bundle-id <id>] [<key>] Read NSUserDefaults for an app
fdb simulator defaults write [--bundle-id <id>] <key> <value> [--type string|int|float|bool] Write an NSUserDefaults key
fdb simulator defaults delete [--bundle-id <id>] <key> Delete an NSUserDefaults key

Data & state

shared-prefs and clean require fdb_helper; grant-permission does not.

Command Description
fdb grant-permission <perm> [--revoke] [--reset] [--reset-all] [--bundle <id>] [--device <id>] Grant, revoke, or reset a runtime permission. iOS sim: xcrun simctl privacy; Android: adb pm grant/revoke. macOS: reset only (tccutil). Physical iOS, Windows, Linux: unsupported. Pass --bundle and --device to pre-grant an iOS simulator app before launch.
fdb shared-prefs get|get-all|set|remove|clear Read/write SharedPreferences
fdb clean Clear app cache and data directories

VM service extensions

Command Description
fdb ext list List all registered ext.* VM service extensions across isolates, sorted and deduplicated
fdb ext call <method> [--arg key=value ...] Invoke a VM service extension and print the JSON result
# Discover what debug hooks are registered in the running app
fdb ext list
# ext.dart.io.getOpenFiles
# ext.flutter.debugPaint
# ext.flutter.imageCache.clear
# ext.flutter.imageCache.size
# ext.flutter.platformOverride
# ext.myapp.clearAuthCache
# ext.myapp.dumpRouterStack

# Get image cache stats
fdb ext call ext.flutter.imageCache.size

# Switch platform overlay to iOS
fdb ext call ext.flutter.platformOverride --arg value=iOS

# Invoke an app-specific extension that collects leak reports (see fdb #46)
fdb ext call ext.flutter.collectLeaks

Memory & heap (pure VM service — no fdb_helper needed)

Command Description
fdb mem [--json] Per-isolate heap totals (usage, external, capacity)
fdb mem native [--app-id <id>] [--pid <pid>] [--tool <name>] Platform-native memory snapshot (dumpsys / footprint / vmmap)
fdb mem profile --output <file> [--isolate <id>] [--all-isolates] Capture a full allocation profile to a JSON file
fdb mem diff <before.json> <after.json> [--sort count|bytes] [--top N] [--all] [--json] Show classes that grew between two profiles
fdb gc [--json] Force a full garbage collection across all isolates
fdb heap dump --output <file> [--no-gc] [--isolate <id>] Capture a DevTools-loadable heap snapshot (.heapsnapshot)

Three-tier heap workflow:

  1. fdb mem — inspect live per-isolate heap state (usage, external, capacity) to get a quick health check.
  2. fdb mem profile --output before.json — capture a full allocation-profile snapshot as a baseline before exercising the feature under test.
  3. fdb mem profile --output after.json followed by fdb mem diff before.json after.json — compare the two snapshots to see exactly which classes grew in instance count or byte size.

Memory leak investigation (5-step ladder):

  1. Confirm growthfdb gc && fdb mem before and after the interaction; if heap returns to baseline after GC, no leak.
  2. Find the culprit classfdb mem profile --output before.json, repeat the interaction, fdb mem profile --output after.json, then fdb mem diff before.json after.json.
  3. Find what retains it — check fdb ext list | grep collectLeaks:
    • Present (leak_tracker path): fdb ext call ext.flutter.collectLeaks — returns structured JSON with retaining paths and culprit/victim analysis.
    • Absent (fallback): no dedicated fdb command today — use DevTools → Memory → Trace Instances → Retaining Path, or query the VM service directly via getInstances + getRetainingPath RPCs (see the skill for the websocat snippet).
  4. Suggest leak_tracker — when collectLeaks is missing, recommend adding leak_tracker to the app's dev dependencies for ongoing automated detection.
  5. DevTools fallback — for leaks the above can't resolve, use DevTools → Memory → Diff Snapshots / Trace Instances. Connect via the VM_SERVICE_URI from fdb status.

For the full walkthrough see the fdb skill → "Investigating memory leaks".

Utility

Command Description
fdb skill Print the AI agent skill file (SKILL.md)
fdb --version Print the fdb version

Global options

Option Description
fdb --session-dir <path> <command> Use a specific .fdb/ session directory instead of auto-resolving

fdb automatically locates the active .fdb/ session by walking up from the current directory, so you can run any command from a subdirectory without changing to the project root. Pass --session-dir to override this and point at a specific session directory.

Interactive REPL

Use -i or --interactive when you want to launch the app and stay in an fdb prompt instead of running separate shell commands:

fdb launch --device <device_id> --project /path/to/your/flutter/app -i

Repeat --dart-define and --dart-define-from-file when the app needs compile-time configuration from flutter run:

fdb launch --device <device_id> --project /path/to/your/flutter/app \
  --dart-define API_BASE_URL=https://example.com/v1,canary \
  --dart-define ENABLE_DIAGNOSTICS=true \
  --dart-define-from-file config/dev.json

Use fdb attach when the app must be started by native tooling first. This is the recommended workflow for iOS Firebase Analytics DebugView, because Firebase expects native Xcode launch arguments rather than Flutter --dart-define values:

Xcode -> Product -> Scheme -> Edit Scheme -> Run -> Arguments
Add: -FIRDebugEnabled
Optional console logging: -FIRAnalyticsDebugEnabled

Then launch the app from Xcode and attach fdb:

fdb attach --device <ios_device_or_simulator_id> \
  --project /path/to/your/flutter/app \
  --app-id <bundle_id>

Auto-discovery — when --debug-url is omitted, fdb scans the device logs for the Dart VM service URI automatically. Add fdb_helper to your app to emit a stable [FDB_VM_URI] marker that makes discovery more reliable across Flutter version changes.

Platform Discovery method Notes
Android (physical + emulator) adb logcat -d -s flutter Also runs adb forward so the device port is accessible on the host
iOS Simulator xcrun simctl spawn <udid> log show --last 5m Looks back up to 5 minutes; no permission prompts
Physical iOS idevicesyslog archive --age-limit 300 + log show --archive Looks back up to 5 minutes; requires brew install libimobiledevice
macOS / Linux / Windows Not supported Pass --debug-url from Xcode console, flutter logs, or fdb status

If auto-discovery fails or the platform is unsupported, copy the Dart VM service URL from Xcode, app logs, or fdb status and pass it directly. Both http://.../ and ws://.../ws forms are accepted:

fdb attach --device <device_id> --project /path/to/app --debug-url <vm_service_url>

The REPL accepts the same commands as the CLI, plus short aliases for the common launch/debug loop:

fdb> s                 # status
fdb> d                 # describe the current screen
fdb> tap @1            # tap a ref from describe
fdb> tap --text Jobs   # tap by visible text
fdb> r                 # hot reload
fdb> R                 # hot restart
fdb> back              # navigate back
fdb> logs --last 50    # show recent app logs
fdb> kill              # stop the app and exit
fdb> detach            # leave the app running and exit the REPL
fdb> q                 # stop the app and exit

detach exits only the REPL; the controller and app keep running, so later commands such as fdb status, fdb reload, or fdb kill still work from a normal shell.

Widget Interaction (tap, input, scroll)

Requires fdb_helper in your app. Adding it also enables automatic VM service URI discovery for fdb attach on Android and iOS (no manual --debug-url needed):

# pubspec.yaml
dev_dependencies:
  fdb_helper: ^1.9.0
// main.dart
import 'package:fdb_helper/fdb_helper.dart';
import 'package:flutter/foundation.dart';

void main() {
  if (!kReleaseMode) {
    FdbBinding.ensureInitialized();
  }
  runApp(MyApp());
}

Release builds compile a safe fdb_helper stub on Android, iOS, and macOS, so App Store / Play Store binaries no longer ship the private-API native tap. Debug and profile builds keep the real native tap implementation, so fdb native-tap works in profile builds distributed via Firebase App Distribution or TestFlight internal. See packages/fdb_helper/README.md for build-mode details.

Troubleshooting

fdb: command not found - Add ~/.pub-cache/bin to your PATH.

Launch hangs - Check the device ID (fdb devices) and the project path.

Screenshot fails - check the tool for your platform is on PATH: adb (Android), xcrun (iOS simulator), screencapture (macOS), xdotool + import (Linux X11). Physical iOS, Windows, and Linux Wayland use fdb_helper — add it to your app and call FdbBinding.ensureInitialized().

Empty widget tree - App may still be starting. Retry, or use fdb describe instead.

RUNNING=false right after launch - The process crashed. When the log contains a recognisable failure, fdb launch prints LAUNCH_ERROR=<CATEGORY>, LAUNCH_ERROR_CAUSE=<description>, and HINT: <remediation> to stderr so you can act on it directly without reading the raw log. If the category is UNKNOWN, inspect the flutter run output above for the first error near the end. See also fdb logs --last 50.

RUNNING=false when running from a subdirectory - fdb walks up the directory tree to find the nearest .fdb/ session automatically. If it still shows RUNNING=false, the app may not be running or the session has a stale PID. Use fdb --session-dir <project>/.fdb status to point directly at the session directory.

Widget interaction fails - fdb_helper missing from pubspec.yaml, or FdbBinding.ensureInitialized() not called.

Agent setup fails mid-flow - Run fdb doctor to check app process, VM service reachability, fdb_helper, platform tools, and stored device state before continuing.

Contributing

See AGENTS.md for coding conventions and the testing skill for running the test suite.

License

MIT

Libraries

cli/adapters/adapters.g
cli/adapters/attach_cli
cli/adapters/back_cli
cli/adapters/clean_cli
cli/adapters/crash_report_cli
cli/adapters/describe_cli
cli/adapters/devices_cli
cli/adapters/doctor_cli
cli/adapters/double_tap_cli
cli/adapters/ext_cli
cli/adapters/gc_cli
cli/adapters/grant_permission_cli
cli/adapters/heap_dump_cli
cli/adapters/input_cli
cli/adapters/kill_cli
cli/adapters/launch_cli
cli/adapters/logs_cli
cli/adapters/longpress_cli
cli/adapters/mem_cli
cli/adapters/native_tap_cli
cli/adapters/reload_cli
cli/adapters/restart_cli
cli/adapters/screenshot_cli
cli/adapters/scroll_cli
cli/adapters/scroll_to_cli
cli/adapters/select_cli
cli/adapters/selected_cli
cli/adapters/shared_prefs_cli
cli/adapters/simulator_cli
cli/adapters/skill_cli
cli/adapters/status_cli
cli/adapters/swipe_cli
cli/adapters/syslog_cli
cli/adapters/tap_cli
cli/adapters/tree_cli
cli/adapters/wait_cli
cli/args_helpers
cli/cli.g
cli/cli_command
cli/command_dispatch
cli/interactive_repl
constants
core/app_died_exception
core/commands/attach/attach
core/commands/attach/attach_models
core/commands/attach/vm_uri_discovery
core/commands/back/back
core/commands/back/back_models
core/commands/clean/clean
core/commands/clean/clean_models
core/commands/crash_report/crash_report
core/commands/crash_report/crash_report_models
core/commands/describe/describe
core/commands/describe/describe_models
core/commands/devices/devices
core/commands/devices/devices_models
core/commands/doctor/doctor
core/commands/doctor/doctor_models
core/commands/double_tap/double_tap
core/commands/double_tap/double_tap_models
core/commands/ext/ext
core/commands/ext/ext_models
core/commands/gc/gc
core/commands/gc/gc_models
core/commands/grant_permission/grant_permission
core/commands/grant_permission/grant_permission_models
core/commands/heap_dump/heap_dump
core/commands/heap_dump/heap_dump_models
core/commands/input/input
core/commands/input/input_models
core/commands/kill/kill
core/commands/kill/kill_models
core/commands/launch/launch
core/commands/launch/launch_models
core/commands/logs/logs
core/commands/logs/logs_models
core/commands/longpress/longpress
core/commands/longpress/longpress_models
core/commands/mem/mem
core/commands/mem/mem_models
core/commands/native_tap/native_tap
core/commands/native_tap/native_tap_models
core/commands/reload/reload
core/commands/reload/reload_models
core/commands/restart/restart
core/commands/restart/restart_models
core/commands/screenshot/screenshot
core/commands/screenshot/screenshot_models
core/commands/scroll/scroll
core/commands/scroll/scroll_models
core/commands/scroll_to/scroll_to
core/commands/scroll_to/scroll_to_models
core/commands/select/select
core/commands/select/select_models
core/commands/selected/selected
core/commands/selected/selected_models
core/commands/shared_prefs/shared_prefs
core/commands/shared_prefs/shared_prefs_models
core/commands/simulator/sim_appearance
core/commands/simulator/sim_defaults
core/commands/simulator/sim_location
core/commands/simulator/sim_push
core/commands/simulator/sim_status_bar
core/commands/simulator/sim_text_size
core/commands/simulator/simulator.g
core/commands/simulator/simulator_models
core/commands/simulator/simulator_utils
core/commands/skill/skill
core/commands/skill/skill_models
core/commands/status/status
core/commands/status/status_models
core/commands/swipe/swipe
core/commands/swipe/swipe_models
core/commands/syslog/syslog
core/commands/syslog/syslog_models
core/commands/tap/tap
core/commands/tap/tap_models
core/commands/tree/tree
core/commands/tree/tree_models
core/commands/wait/wait
core/commands/wait/wait_models
core/core.g
core/flutter_binary
core/launch_failure_analyzer
core/models/command_result
core/process_utils