Skip to content

Installation

View raw .md

Add crumbs to a Flutter app, install it behind a call-site guard, register it with dowel, and optionally write the MCP entry for your AI client.

On this page

The recommended path is one command, dart run crumbs crumbs:install, which patches lib/main.dart and registers the plugin with dowel; the manual route is described step by step below.

Quick Reference#

Step Command Result
1. Add the package flutter pub add crumbs crumbs is a dependency; dowel comes with it.
2. Wire the app dart run crumbs crumbs:install The dowel harness exists, crumbs is a registered plugin, and lib/main.dart installs crumbs in a kDebugMode block.
3. Register the MCP server (optional) dart run crumbs mcp:install A dowel entry in .mcp.json at the project root.
4. Start the app dart run crumbs start --device=chrome dowel records the session every crumbs:* command dials.
5. Read a buffer dart run crumbs crumbs:tail The newest log records of the running app.

Requirements#

crumbs is a Flutter package: the VM Service extensions register inside a running Flutter app, so a pure Dart program is not supported.

Dependency Minimum Notes
Dart 3.13
Flutter 3.47
dowel ^0.0.21 A dependency of crumbs. When crumbs:install scaffolds the harness, dowel's install step adds dowel: any to the app's pubspec.yaml, because the generated barrels import it.

Run the installer#

bash
flutter pub add crumbs
dart run crumbs crumbs:install

dart run crumbs is crumbs's own entry point: it carries the whole dowel CLI plus the crumbs commands, so the installer works on a project that has no dowel setup yet. It runs three steps in order:

  1. When bin/dispatcher.dart is missing, it runs dart run crumbs install, which scaffolds the dowel harness: bin/dispatcher.dart, the fast ./bin/dowel launcher and the codegen barrels under lib/app/. A project that already has the dispatcher skips this step.
  2. It runs dart run crumbs plugin:install crumbs, which records crumbs in .dowel/plugins.json and regenerates lib/app/_plugins.g.dart.
  3. It patches lib/main.dart: the kDebugMode and package:crumbs/crumbs.dart imports, WidgetsFlutterBinding.ensureInitialized() when missing, and a kDebugMode block with CrumbsPlugin.install() and the exception and dump watchers. The block lands before await Magic.init( on a Magic app, so ExceptionWatcher sees boot errors, and before runApp( otherwise. Without a lib/main.dart the step is skipped with a message.

On a Magic app whose pubspec.yaml lists magic_devtools, step 3 also adds import 'package:magic_devtools/crumbs.dart'; and a kDebugMode block with MagicCrumbsIntegration.install() after Magic.init(). That library exists only in a magic_devtools release built on crumbs, so the installer writes these lines only when the resolved magic_devtools ships it; with an older release it warns and leaves the Magic lines out. Until that release ships, MagicTelescopeIntegration feeds fluttersdk_telescope, not crumbs, so the HTTP, event, gate, query and cache buffers of crumbs stay empty in a Magic app.

Every step is idempotent: re-running crumbs:install changes nothing.

Manual wiring#

Skip the installer and edit lib/main.dart directly. Call CrumbsPlugin.install() inside a guard, after WidgetsFlutterBinding.ensureInitialized() and before runApp():

dart
import 'package:crumbs/crumbs.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  if (kDebugMode) {
    CrumbsPlugin.install(); // LogWatcher and the ext.crumbs.* extensions.
    CrumbsPlugin.registerWatcher(ExceptionWatcher());
    CrumbsPlugin.registerWatcher(DumpWatcher());
  }

  runApp(const MyApp()); // MyApp is the host app's root widget.
}

kDebugMode keeps crumbs in debug builds only; !kReleaseMode keeps it in profile builds too, which a frame measurement needs. Both tree-shake a release build; see Call-site guard. CrumbsPlugin.install() is idempotent, so a hot restart registers each extension once.

On a Magic app, install crumbs before Magic.init() and the Magic integration after it, because the integration resolves the Http facade and the event dispatcher from the container:

dart
import 'package:crumbs/crumbs.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:magic/magic.dart';
import 'package:magic_devtools/crumbs.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  if (kDebugMode) {
    CrumbsPlugin.install();
    CrumbsPlugin.registerWatcher(ExceptionWatcher());
    CrumbsPlugin.registerWatcher(DumpWatcher());
  }

  await Magic.init(configFactories: appConfigFactories); // The host's config list.

  if (kDebugMode) {
    MagicCrumbsIntegration.install();
  }

  runApp(const MyApp());
}

On a vanilla app that uses Dio, route Dio traffic into the HTTP buffer from an interceptor; crumbs has no dio dependency, so the interceptor lives in the app (the bundled example/ app has a complete one):

dart
import 'package:crumbs/crumbs.dart';
import 'package:dio/dio.dart';

class CrumbsDioInterceptor extends Interceptor {
  final Map<RequestOptions, Stopwatch> _watches = {};

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    _watches[options] = Stopwatch()..start();
    handler.next(options);
  }

  @override
  void onResponse(Response<dynamic> response, ResponseInterceptorHandler handler) {
    final options = response.requestOptions;
    DioHttpAdapter.recordRequest(
      url: options.uri.toString(),
      method: options.method,
      statusCode: response.statusCode ?? 0,
      durationMs: _watches.remove(options)?.elapsedMilliseconds ?? 0,
    );
    handler.next(response);
  }
}

Add it with dio.interceptors.add(CrumbsDioInterceptor()). CrumbsPlugin.registerHttpAdapter(DioHttpAdapter()) is optional here: DioHttpAdapter hooks nothing itself, and recordRequest writes to the store either way.

Register with dowel#

crumbs:install already does this. plugin:install crumbs writes CrumbsDowelProvider into lib/app/_plugins.g.dart, which the scaffolded bin/dispatcher.dart reads through plugins.autoDiscoveredProviders(), so ./bin/dowel list shows the crumbs:* commands and ./bin/dowel mcp:serve serves the crumbs_* tools.

To finish a skipped step by hand:

bash
dart run crumbs install               # only when bin/dispatcher.dart is missing
dart run crumbs plugin:install crumbs # idempotent; refreshes the barrel on re-run

A dispatcher wired by hand imports the provider from the Flutter-free cli.dart barrel, the same import plugins:refresh generates:

dart
import 'dart:io';

import 'package:crumbs/cli.dart' show CrumbsDowelProvider;
import 'package:dowel/dowel.dart';

Future<void> main(List<String> args) async {
  exit(
    await runDowel(
      args,
      baseProviders: [CrumbsDowelProvider()],
      delegateToConsumer: false,
      collectMcpTools: args.isNotEmpty && args.first == 'mcp:serve',
    ),
  );
}

Wire MCP tools#

bash
dart run crumbs mcp:install

This writes (or updates) the dowel entry in .mcp.json at the project root and keeps other entries. With ./bin/dowel present on a POSIX host the entry runs ./bin/dowel mcp:serve; otherwise, and always on Windows, it runs dart run crumbs mcp:serve. Reconnect the client afterwards (/mcp reconnect dowel in Claude Code). See MCP setup for other clients and tool filtering.

Verify installation#

Start the app through dowel, so the session (the VM Service URI every crumbs:* command dials) is recorded:

bash
dart run crumbs start --device=chrome

Then read the log buffer and list the commands:

bash
./bin/dowel crumbs:tail
./bin/dowel list

crumbs:tail prints the newest log records; list shows the crumbs: namespace. dart run crumbs <command> runs the same commands when ./bin/dowel is not built. If a command finds no app, start it with dart run crumbs start first; if it finds the app but no ext.crumbs.* extension, confirm CrumbsPlugin.install() is reachable in main.dart and that the app is a debug build (or a profile build under !kReleaseMode). Stop the app with dart run crumbs stop.

Gotchas#

Mistake Fix
CrumbsPlugin.install() sits outside a guard. Wrap it in if (kDebugMode) or if (!kReleaseMode); crumbs installs no guard of its own.
The guard sits inside a helper function. Keep it at the call site in main(); the tree-shake depends on it.
crumbs_exceptions stays empty after a crash. Register ExceptionWatcher: CrumbsPlugin.install() installs only LogWatcher.
MagicCrumbsIntegration.install() runs before Magic.init(). Call it after Magic.init(); the HTTP adapter registers nothing while the network driver is unbound.
The installer warned and wrote no Magic lines. The resolved magic_devtools predates crumbs; upgrade it and rerun crumbs:install.
The app was started with a plain flutter run. Start it with dart run crumbs start, which records the session the commands dial.
.mcp.json points at the wrong launcher. Rerun dart run crumbs mcp:install; it picks the launcher from the scaffold state.
  • Quickstart: install, start the app and make the first MCP tool call.
  • Watchers: every watcher and adapter, and the call-site guard.
  • MCP setup: client configuration, reconnecting and tool filtering.
  • Redaction: mask secrets before a record is buffered.
esc

Search index did not load.

No match for "".

Try a class like rounded-lg or a widget like WButton.