Crumbs
Let your AI agent read what your Flutter app just did.
HTTP traffic, logs, uncaught exceptions, debug prints, events, gate checks, DB queries and cache operations, captured in memory while the app runs and read from one CLI and one MCP server. No stack traces pasted into the chat, no proxy, no SaaS account.
Documentation · pub.dev · Issues
crumbs is a passive inspector: it records what the app does and drives nothing. Watchers hook package:logging, FlutterError.onError, debugPrint and the frame scheduler, and call any handler set before them; the log watcher also turns on hierarchical logging and sets Logger.root.level to Level.ALL, so every log record reaches the buffer. HTTP adapters and the Magic watchers feed the rest. Every record lands in a ring buffer inside the app, and ext.crumbs.* VM Service extensions serve those buffers to crumbs:* commands and crumbs_* MCP tools on dowel. Its sibling tapir drives the app; crumbs reads what the app captured. crumbs is inspired by Laravel Telescope, which records the requests, exceptions, logs, queries, cache hits and events of a Laravel app into a queryable timeline; crumbs serves the same kind of timeline to a terminal and an AI agent instead of a web dashboard.
At a glance
| Fact | Value |
|---|---|
| Package | crumbs |
| Install | flutter pub add crumbs, then dart run crumbs crumbs:install |
| Import | package:crumbs/crumbs.dart (CrumbsPlugin.install(), behind if (kDebugMode) or if (!kReleaseMode)) |
| Entry points | dart run crumbs <command>, ./bin/dowel <command>, CrumbsPlugin.install(), CrumbsPlugin.registerWatcher(), CrumbsPlugin.registerHttpAdapter(), CrumbsStore, CrumbsRedaction, CrumbsFileSink, CrumbsDowelProvider (package:crumbs/cli.dart), dart run crumbs mcp:serve |
| Platforms | Any Flutter target that exposes a VM Service in a debug or profile build: Android, iOS, web, macOS, Windows, Linux. CrumbsFileSink writes through dart:io, so the timeline on disk is not available on the web; on a Windows host mcp:install writes a dart run entry, never ./bin/dowel |
| Requires | Dart 3.13, Flutter 3.47; a plugin of dowel (^0.0.21) |
| Dependencies | dowel, logging, meta, the Flutter SDK |
| License | MIT |
Install
flutter pub add crumbs
dart run crumbs crumbs:install
dart run crumbs start --device=macos # dowel records the session every crumbs:* command dials
dart run crumbs crumbs:tail
crumbs:install scaffolds the dowel harness when bin/dispatcher.dart is missing (including the fast ./bin/dowel launcher), registers crumbs as a dowel plugin, and patches lib/main.dart: the imports, WidgetsFlutterBinding.ensureInitialized() when missing, and a kDebugMode block that installs crumbs with the exception and dump watchers, placed before await Magic.init( on a Magic app and before runApp( otherwise. Re-running it changes nothing. Wired by hand, main.dart looks like this:
import 'package:crumbs/crumbs.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
if (kDebugMode) {
CrumbsPlugin.install();
CrumbsPlugin.registerWatcher(ExceptionWatcher());
CrumbsPlugin.registerWatcher(DumpWatcher());
}
runApp(const MyApp()); // MyApp is the host app's root widget.
}
The guard belongs to the call site: crumbs installs none of its own. kDebugMode is what the installer writes; !kReleaseMode also keeps crumbs in a profile build, which a frame measurement needs. Either way a release build tree-shakes crumbs. Without a guard crumbs ships in release, and anyone who reaches that app's VM Service can read every buffer. --dart-define=CRUMBS_DISABLE=1 skips registration in a build that must not expose the extensions. The example/ app wires the core watchers and routes Dio traffic into the HTTP buffer.
On a Magic app, MagicCrumbsIntegration.install() from package:magic_devtools/crumbs.dart, called after Magic.init(), registers the Magic HTTP adapter and the model, cache, event, gate and query watchers. It exists only in a magic_devtools release built on crumbs: crumbs:install writes those lines when the resolved magic_devtools ships it, and warns and skips them otherwise. 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.
What it captures
| Record | Captured by | CLI | MCP tool |
|---|---|---|---|
package:logging records |
LogWatcher, installed by CrumbsPlugin.install() |
crumbs:tail |
crumbs_tail |
| Uncaught exceptions | ExceptionWatcher (FlutterError.onError and PlatformDispatcher.onError, chained to any handler already set, such as Sentry's) |
none | crumbs_exceptions |
debugPrint output |
DumpWatcher (debug builds only unless allowInRelease is set) |
crumbs:dumps |
crumbs_dumps |
| Frame timings and build attribution | FramePerfWatcher |
crumbs:frames |
crumbs_frames |
| HTTP requests | A CrumbsHttpAdapter; with Dio, an interceptor that calls DioHttpAdapter.recordRequest(); on Magic, MagicHttpFacadeAdapter |
crumbs:requests |
crumbs_requests |
| In-app events | MagicEventWatcher |
crumbs:events |
crumbs_events |
| Gate checks | MagicGateWatcher |
crumbs:gates |
crumbs_gates |
| DB queries | MagicQueryWatcher |
crumbs:queries |
crumbs_queries |
| Cache operations | MagicCacheWatcher |
crumbs:caches |
crumbs_caches |
| Model lifecycle | MagicModelWatcher |
none (CrumbsStore.recentModels() in the app) |
none |
crumbs:clear and crumbs_clear empty every buffer, which turns the next read into the output of one action. Each buffer keeps the newest 500 records by default and the frame buffer 3600; CrumbsStore.setCapacity() changes one or all of them. A read returns records oldest first. crumbs:tail and crumbs:events take --since, --json and --follow to stream a buffer from a cursor.
Secrets and the timeline on disk
| Surface | Behaviour |
|---|---|
| Credential masking | HTTP records are masked before they are buffered: Authorization, Cookie, X-Api-Key and similar headers, and password, token style keys in a JSON or form body, read ********. Extend the lists with CrumbsRedaction.hideRequestHeaders(), hideRequestParameters() and hideResponseParameters() |
| Text redactor | CrumbsRedaction.redactor, a String Function(String) you set, runs over every text field of a log, event, exception and HTTP record at insert. A redactor that throws drops the record rather than let it through unmasked; each of those records carries a read-only redacted flag |
| File sink | CrumbsFileSink.start(directory: ...) writes a rotating JSONL timeline of log, event and exception records, and writes only records whose redacted flag is true. Read it back with crumbs:files |
CrumbsRedaction.redactor = (String text) => text.replaceAll(RegExp(r'\b\d{13,19}\b'), '[card]');
await CrumbsFileSink.start(directory: '${supportDir.path}/crumbs'); // supportDir is the host's choice.
MCP server and clients
The crumbs_* tools ride on the dowel MCP server, so the client config key is dowel. dart run crumbs mcp:install writes the entry into .mcp.json: ./bin/dowel mcp:serve when the fast launcher exists on a POSIX host, and dart run crumbs mcp:serve otherwise. Standard config for any client that reads .mcp.json:
{ "mcpServers": { "dowel": { "command": "./bin/dowel", "args": ["mcp:serve"], "cwd": "." } } }
| Client | Install |
|---|---|
| Claude Code | claude mcp add dowel -- ./bin/dowel mcp:serve, then /mcp reconnect dowel after a change |
| Cursor | .cursor/mcp.json (project) or ~/.cursor/mcp.json (user) |
| Windsurf | ~/.codeium/windsurf/mcp_config.json |
| Claude Desktop | claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/, Windows: %APPDATA%\Claude\) |
A typical agent session, the app started with dowel_start and driven by hand or by tapir:
crumbs_clear {}
# the user (or tapir) exercises the feature
crumbs_requests {"limit": 20}
crumbs_exceptions {}
crumbs_tail {"level": "WARNING"}
crumbs_queries {}
| Without crumbs | With crumbs |
|---|---|
| A stack trace scrolls past in the console; you copy it into the chat, then the agent asks for the HTTP response that caused it. | The agent calls crumbs_exceptions and crumbs_requests and reads both itself. |
You add print calls, hot reload and retry to see what the app sent. |
The agent calls crumbs_clear, the action runs again, and the next read holds only its records. |
For AI coding agents
| Do | Because |
|---|---|
Install crumbs only behind if (kDebugMode) or if (!kReleaseMode) |
Anyone who reaches the VM Service of a build that carries crumbs can read every buffer |
Start the app with dart run crumbs start or ./bin/dowel start, not flutter run |
Every crumbs:* command and crumbs_* tool dials the session dowel records |
Call crumbs_clear before a repro |
A buffer holds the whole run, so an old record reads like a new one |
Register ExceptionWatcher (and DumpWatcher) when wiring by hand |
CrumbsPlugin.install() installs only LogWatcher; an empty exception buffer otherwise proves nothing |
Read livenessCounter with an empty crumbs_frames answer |
A counter that does not advance means FramePerfWatcher is not registered or the engine draws nothing, not a smooth app |
Treat ******** in an HTTP record as crumbs's mask |
The app sent the real credential; crumbs hid it before buffering |
Set CrumbsRedaction.redactor before CrumbsFileSink.start() |
The sink writes only redacted records and counts the rest in droppedUnredacted |
Page logs and events by passing the cursor of one answer back as since (CLI: --since, or --json --follow) |
The MCP descriptors declare only limit (and level on crumbs_tail), but dowel forwards every argument, so since, logger and type work over MCP too |
Point the MCP client at the dowel key and reconnect after mcp:install |
The tools ride on the dowel server, and clients do not reload an entry on their own |
Moving from fluttersdk_telescope
crumbs is the new name of fluttersdk_telescope, and dowel the new name of the CLI substrate it runs on. Nothing is migrated automatically:
flutter pub remove fluttersdk_telescope fluttersdk_artisan && flutter pub add crumbs dowel
mv .artisan .dowel && rm -f .dowel/plugins.json .dowel/installed/fluttersdk_telescope.json
rm bin/dispatcher.dart bin/fsa lib/app/_plugins.g.dart lib/app/commands/_index.g.dart
dart run crumbs crumbs:install
dart run dowel commands:refresh
dart run crumbs mcp:install
The old dispatcher and codegen barrels import fluttersdk_artisan, and the installer keeps a file that exists, so they go first. Rename the imports and identifiers below, and in your own commands under lib/app/commands/ replace package:fluttersdk_artisan/artisan.dart with package:dowel/dowel.dart and Artisan* with Dowel* before commands:refresh. Delete the fluttersdk entry from .mcp.json and run /mcp reconnect dowel. On a Magic app, MagicTelescopeIntegration keeps feeding fluttersdk_telescope, not crumbs, until a magic_devtools release built on crumbs ships; upgrade to it then and rerun dart run crumbs crumbs:install.
| Before | After |
|---|---|
package:fluttersdk_telescope/telescope.dart |
package:crumbs/crumbs.dart |
Telescope* identifiers (TelescopePlugin, TelescopeStore, TelescopeRedaction, TelescopeFileSink) |
Crumbs* (CrumbsPlugin, CrumbsStore, CrumbsRedaction, CrumbsFileSink) |
telescope:* commands, telescope_* tools, ext.telescope.* |
crumbs:*, crumbs_*, ext.crumbs.* |
TELESCOPE_DISABLE |
CRUMBS_DISABLE |
The fluttersdk server key in .mcp.json |
dowel |
package:fluttersdk_artisan/artisan.dart, Artisan* in your own commands |
package:dowel/dowel.dart, Dowel* |
The changelog lists the rest.
Dependencies
dowel for the CLI, the MCP server and the VM Service connection; logging for the log watcher; meta. The rest is the Flutter SDK. HTTP libraries stay out: the app adds dio or any other client itself.
Links
- Documentation: installation, quickstart, watchers, redaction, the file sink, buffers, MCP setup and the MCP tool reference
- API reference
- llms.txt and the agent skill for AI coding agents:
npx skills add anilcancakir/crumbs - Changelog, report an issue, security policy
License
MIT, see LICENSE. Built by Anılcan Çakır.