Flutter Lamp
<div align="center">
# ๐ก Flutter Lamp
### Give your AI **live** eyes on a running Flutter app โ no more pasting logs.
[](https://github.com/itsonu/flutter-lamp/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/flutter-lamp)
[](LICENSE)
[](https://nodejs.org)
[](https://modelcontextprotocol.io)
An **MCP server** that connects Claude Code, Claude Desktop, Cursor, Codex & Gemini
directly to a running Flutter app through the **Dart VM Service Protocol** โ
streaming exceptions, logs, network calls, frame timings and memory as structured
data, plus an evidence-first root-cause **diagnosis** engine and a live browser
**dashboard**.
</div>
---
## Why
Today you debug Flutter with your AI by copy-pasting stack traces, `flutter run`
output and DevTools screenshots. The AI is blind between messages.
Flutter Lamp makes the AI **runtime-aware**. It reads the app's live
state over official Flutter/Dart APIs (never scraping DevTools), so instead of
*"paste the error"* the AI can ask the app *"what just happened, and why?"*
```
โ You: *pastes 40 lines of red stack trace*
โ
AI: connect_vm โ get_exceptions โ diagnose_runtime
โ "RenderFlex overflow in Column at home.dart:42, triggered right after
GET /api/user returned 500. Confidence 85%. Fix: โฆ"
```
## Features
- ๐ **One-line connect** to any running Flutter app's VM Service
- ๐ฅ **Realtime exceptions with reconstructed stack traces** (framework + unhandled)
- ๐ **Console & structured logs** (Stdout / Stderr / `dart:developer`)
- ๐ **Network capture** via `dart:io` profiling โ covers Dio & `package:http`, no interceptor needed
- ๐๏ธ **Frame timings** with jank detection, percentiles, and a build-vs-raster verdict
- ๐งญ **Route awareness** โ current screen, transitions, and failures attributed to the screen they happened on
- ๐ง **State-management activity** โ Riverpod, Provider and Bloc churn correlated with rebuild storms
- ๐ **Widget rebuild attribution** โ which widget rebuilt, how often, at which file and line, with your code ranked apart from package code
- ๐งฌ **Widget tree & selected-widget** snapshots from the Inspector
- ๐งฎ **Memory** (Dart heap / external) and **VM timeline** events
- ๐ฉบ **`diagnose_runtime`** โ correlates evidence into *summary ยท root cause ยท evidence ยท confidence ยท fixes*; says **"Unknown"** below 70% instead of hallucinating
- ๐ **Live browser dashboard** at `http://127.0.0.1:7373` โ streams everything over WebSocket, independent of the AI connection
- ๐งฉ Ships a reusable **`flutter-runtime-diagnosis`** Claude Code skill
## Tools
| Tool | Purpose | Safety |
| --- | --- | --- |
| `connect_vm` | Connect to a running app's Dart VM Service (`ws://` or `http://`). | **mutates** |
| `ensure_tcp_device` | Android transports; recommend wireless, optionally promote USB. | **mutates** |
| `runtime_health` | One-call triage โ verdict plus exception/network/frame/log/memory summary. | read-only |
| `what_changed` | Evidence from the window before a failure, with a timeline. | read-only |
| `get_navigation` | Current route and recent transitions, with per-route failures. | read-only |
| `get_rebuilds` | Widget rebuild hotspots resolved to widget, file and line. | read-only |
| `get_state_activity` | Riverpod, Provider and Bloc activity over time, and how often it coincides with build-heavy frames. | read-only |
| `export_session` | The whole session as versioned JSON โ `brief` (diagnoses + cited evidence) or `full`. | read-only |
| `explain_diagnosis` | Why a diagnosis was reached: resolved evidence, alternatives, gaps. | read-only |
| `get_capabilities` | Active collectors, tool safety classes, what cannot be observed. | read-only |
| `runtime_status` | Connection, session, reconnect state, event counts, retention window, what the session has cost in responses, and the VM-to-host clock offset. | read-only |
| `get_logs` | Console + `dart:developer` logs (filter by severity / source / text). | read-only |
| `get_exceptions` | Framework & unhandled exceptions **with stack traces**. | read-only |
| `get_frames` | Frame build/raster timings; `onlyJanky` filter. | read-only |
| `get_network` | HTTP requests (Dio & `package:http`); headers + timing on failures. | read-only |
| `get_widget_tree` | Widget-tree snapshot from the Flutter Inspector. | read-only |
| `get_selected_widget` | Widget currently selected in the Inspector. | read-only |
| `get_memory` | Dart heap + external memory (MB). | read-only |
| `get_timeline` | Recent VM timeline events (build/paint/layout/GC). | **mutates** |
| `diagnose_runtime` | Root cause with a stable `cause` label, evidence ids, timeline, alternatives, and coverage โ including which categories nothing could observe. | read-only |
| `diagnose_performance` | Why the app is janky โ percentiles, phase split, rebuild attribution. | read-only |
| `get_dashboard_url` | URL of the live browser dashboard. | read-only |
**mutates** means the tool changes app or VM state, not your project โ nothing
here writes code or files. `connect_vm` enables `dart:io` HTTP timeline logging
on the app so network capture works; `get_timeline` with `recordFrom: true`
changes the VM's recording flags. Every tool carries an MCP `readOnlyHint`
annotation so a client can enforce this itself.
Agents should not call all of them. The recommended flow is
`runtime_health` โ `what_changed` โ a targeted `get_*` โ `diagnose_runtime`;
see [AI Agent Integration](docs/AI-Agent-Integration.md).
## Install
**Requires Node โฅ 20.** Nothing to clone โ `npx` fetches it on first run:
```bash
npx -y flutter-lamp
```
Or install it globally:
```bash
npm install -g flutter-lamp
```
<details>
<summary>From source</summary>
```bash
git clone https://github.com/itsonu/flutter-lamp.git
cd flutter-lamp
npm install
npm run build
node dist/index.js
```
</details>
## Connect your AI client
**Claude Code plugin** (recommended) โ installs the MCP server *and* the
`flutter-runtime-diagnosis` skill in one step:
```
/plugin marketplace add itsonu/flutter-lamp
/plugin install flutter-lamp@flutter-lamp
```
The plugin pins an exact, published server version. Flutter Lamp is also
listed in the [MCP Registry](https://registry.modelcontextprotocol.io) as
`io.github.itsonu/flutter-lamp`.
Or add the server to your MCP client config yourself, then restart the client.
**Claude Code** โ `.mcp.json` in your project:
```json
{
"mcpServers": {
"flutter-lamp": {
"command": "npx",
"args": ["-y", "flutter-lamp"]
}
}
}
```
Or from the CLI:
```bash
claude mcp add --scope user flutter-lamp -- npx -y flutter-lamp
```
`--scope user` makes it available in every project; drop it to register
for the current project only.
**Cursor** (`~/.cursor/mcp.json`) and **Claude Desktop**
(`claude_desktop_config.json`) use the same `mcpServers` shape.
Running from a clone instead? Point `command` at `node` and `args` at
`/absolute/path/to/flutter-lamp/dist/index.js`.
Optional environment variables:
| Var | Default | Meaning |
| --- | --- | --- |
| `DASHBOARD_PORT` | `7373` | Dashboard HTTP/WS port. | read-only |
| `DASHBOARD_HOST` | `127.0.0.1` | Bind address (localhost only by default). | read-only |
| `DASHBOARD_DISABLE` | โ | Set to `1` to disable the dashboard. | read-only |
| `FLUTTER_LAMP_REDACT` | on | Set to `off` to keep raw credential values. | read-only |
| `FLUTTER_LAMP_REDACT_EXTRA` | โ | Comma-separated extra header-name patterns to redact. | read-only |
## Usage
1. Run your Flutter app in debug/profile mode:
```bash
flutter run
```
Copy the line it prints:
```
A Dart VM Service on <device> is available at: http://127.0.0.1:PORT/TOKEN=/
```
> Tip: `flutter run --vm-service-port=8181` gives a stable URI across restarts.
2. Ask your AI to connect and diagnose โ e.g. *"connect to my Flutter app at
`<uri>` and tell me why it's throwing."* With the bundled skill, Claude Code
runs the whole flow (connect โ gather โ `diagnose_runtime`) automatically and
**never asks you to paste logs** when the VM Service is reachable.
3. Open **http://127.0.0.1:7373** in a browser for the live dashboard โ it runs
alongside the AI, not instead of it.
### Debugging without a cable (Android)
A `flutter run` started over USB loses its VM Service tunnel the moment the
cable moves; one started over a TCP transport does not. Ask the server which
transports exist:
```
ensure_tcp_device # read-only: lists transports, recommends one
ensure_tcp_device { promote: true } # puts a USB-only device on TCP (needs the cable once)
```
Then launch against the wireless serial it recommends:
```bash
flutter run -d 192.168.88.3:5555
```
The cable is only needed for the one-time promotion. Reverse it any time with
`adb usb`.
## Live dashboard
A zero-dependency dark UI (native HTTP + WebSocket, no build step) that streams
runtime data as it happens:
**Overview** โ a session report: health, FPS, memory, worst frame, MCP activity,
and findings that link through to the events behind them (the worst-frame
finding opens the timeline filtered to that frame) ยท **Logs** (search / filter /
auto-scroll) ยท **Network** (expandable headers & timing) ยท **Exceptions**
(expandable stack traces) ยท **Timeline** ยท **Performance** (live canvas charts) ยท
**MCP** โ which agent connected, every tool it called, with latency, errors and
response size ยท **Inspector** โ which inspection links exist and which are
pull-only ยท **Diagnostics** โ the measured topology, per-collector health, and
what this target cannot observe at all.
Controls: pause/resume ยท clear view ยท export JSON ยท per-tab search ยท auto-reconnect.
The UI distinguishes states that look alike and are not: `0 MB` is a
measurement, `not sampled yet` is the absence of one, and an empty tab says
whether a filter is hiding rows, the collector is blind on this target, or the
collector is watching and has seen nothing. Readings taken before a disconnect
are labelled as such rather than left looking live.
## How it works
```
Running Flutter app
โ Dart VM Service Protocol (JSON-RPC over WebSocket)
โผ
VmService client โโโถ Collectors (log ยท exception ยท frame ยท network ยท โฆ)
โ
โผ
RuntimeStore (one centralized, capped event stream;
every event: timestamp ยท source ยท severity ยท category)
โ โ
โโโโโโโโโโโโ โโโโโโโโโโโโ
โผ โผ
MCP tools (stdio) Dashboard (HTTP + WebSocket)
โ Claude Code / Cursor / โฆ โ your browser
```
Everything flows through **one** event store. Adding a new runtime source =
implement the `Collector` interface and register it โ no other layer changes.
Design principles: official Flutter/Dart APIs only, structured JSON over text,
never scrape DevTools, and never claim a cause the evidence doesn't support.
## Limitations
- **Debug/profile builds only.** The VM Service, Inspector and `dart:io` HTTP
profiling are not available in release builds.
- **Network is pull-on-demand.** Dart exposes no push stream for `dart:io` HTTP,
so requests are fetched when `get_network` or `diagnose_runtime` runs โ not
streamed continuously.
- **Exceptions are not always observable.** `Debug.PauseException` needs
pause-on-exception enabled in the app to fire. `Flutter.Error` is posted by the
widget inspector only while structured error reporting is on, which the
framework disables in profile mode and on the web โ so on those targets a
thrown framework error leaves no trace at all. The collector asks the app and
reports `degraded` when that is the case, so an empty exception list is never
silently mistaken for a quiet one.
- **Dart-side HTTP only.** Calls made from platform (Kotlin/Swift) code or from a
WebView don't appear in `get_network`.
- **Retention is bounded.** Each category keeps its own fixed window (3,000
logs, 2,000 state, 1,000 exceptions, 1,000 network, 1,000 frames, 1,000
rebuilds, 500 navigation, 500 system). Frames roll over fastest โ about 17
seconds at 60fps, and measured at 4 minutes under a 10fps workload.
`runtime_status` reports what is retained and what was evicted, and once
eviction starts every diagnosis carries a note saying so, so a truncated
window is never presented as a whole session.
- The dashboard binds to `127.0.0.1` by default. Change `DASHBOARD_HOST` only on a
network you trust โ runtime data is served unauthenticated.
## Security
Runtime data is sensitive. HTTP headers carry bearer tokens and cookies, URIs
carry API keys, and developers print credentials into logs โ and everything
captured is handed to an AI model *and* streamed to any browser watching the
dashboard.
**Secrets are redacted at capture**, so they never enter the event store and no
consumer can leak what was never stored. Redacted by default: `Authorization`,
`Proxy-Authorization`, `Cookie`, `Set-Cookie`, `WWW-Authenticate`, any header
whose name contains `token`, `secret`, `password`, `credential`, `api-key` or
`session`, sensitive query-string parameters, and JWT- or `Bearer`-shaped
strings in log lines and error text. Header names that were hit are listed in
`data.redactedHeaders` so you can see that something was withheld rather than
getting a silently partial picture. Add patterns with
`FLUTTER_LAMP_REDACT_EXTRA`, or disable entirely with `FLUTTER_LAMP_REDACT=off`
for a local-only session.
**The VM Service credential is scrubbed unconditionally.** The path segment of a
VM Service URI authorises `evaluate` โ arbitrary Dart execution in the app being
observed โ and an app can print its own URI to its console, which a web target
does on startup. That token is removed from captured text wherever it appears,
and unlike everything above it is **not** governed by `FLUTTER_LAMP_REDACT=off`:
that switch exists so you can read your own app's headers and log text, not so
the server will hand out the key to the app it is attached to.
**The dashboard is not exposed to other pages.** Binding to loopback does not
protect a WebSocket โ browsers exempt WebSocket from the same-origin policy, so
without a check any page you have open could connect to
`ws://127.0.0.1:7373/ws` and read your whole runtime stream. The handshake
requires a per-process token that is inlined into the served page, which
cross-origin script cannot read, and a present `Origin` header must be loopback.
The page is served `X-Frame-Options: DENY`, and `/health` returns liveness only
โ never the VM Service URI, which embeds the VM's own auth token.
Setting `DASHBOARD_HOST` to a non-loopback address logs a warning and puts
runtime evidence on your network. Only do that on a network you trust.
Found a vulnerability? See [SECURITY.md](SECURITY.md).
## Roadmap
Shipped: VM connect, logs, exceptions (with stacks), network, frames, widget
tree, memory, timeline, route awareness, rebuild attribution, state-management
activity, diagnosis, live dashboard, and an evaluation suite that replays
recorded sessions to check the diagnosis against apps that actually misbehaved.
Next: deeper correlation (memory/timeline into `diagnose_runtime`), CPU sampling
& leak heuristics, knowledge graph, auto-fixes. See
[`docs/Phases.md`](docs/Phases.md) and
[`docs/Observability-Roadmap.md`](docs/Observability-Roadmap.md).
## Development
```bash
npm run build # compile TypeScript โ dist/
npm run watch # incremental compile
npm test # node:test suite (engine + collectors + dashboard)
```
`npm test` runs the compiled output, so build first. It relies on glob support in
`node --test`, which needs **Node โฅ 21** โ the server itself runs on Node โฅ 20.
Docs live in [`docs/`](docs/):
| Doc | Contents |
| --- | --- |
| [PRD](docs/PRD.md) | Problem, users, principles, non-goals, constraints |
| [Architecture](docs/Architecture.md) | Data flow, components, event model, how to add a collector |
| [Rules](docs/Rules.md) | Non-negotiable constraints every change is checked against |
| [Phases](docs/Phases.md) | Roadmap and status |
| [AI Agent Integration](docs/AI-Agent-Integration.md) | The investigation protocol agents should follow |
| [Improvement Plan](docs/Improvement-Plan.md) | Current audit and prioritized backlog |
| [Observability Roadmap](docs/Observability-Roadmap.md) | Architecture target, phases, and explicit rejections |
| [Data path](docs/Data-Path.md) | Where runtime events actually flow, and what is observable |
| [Releasing](docs/Releasing.md) | How versions are staged, tagged and published |
| [Design](docs/DESIGN.md) | Dashboard design system โ tokens, status vocabulary, component rules |
| [Implementation Notes](docs/Implementation-Notes.md) | Non-obvious things learned building it |
## Contributing
Issues and PRs welcome. Keep it modular, prefer official APIs over hacks, and
add a `node:test` check for any non-trivial logic.
## License
[MIT](LICENSE) ยฉ Chandrabhushan Prakash
TDQS
Scored across 22 tools
Each tool targets a distinct aspect of runtime debugging (logs, exceptions, frames, network, memory, etc.) with minimal overlap. While runtime_health aggregates several get_* tools, its purpose is clearly differentiated from runtime_status and the raw data collectors. Descriptions are detailed enough to guide correct selection.
All tools use lower_snake_case, but the verb_noun pattern is not fully consistent: many use get_ prefix, while others are noun phrases (runtime_status, runtime_health) or phrases (what_changed). Still, the casing and readability are consistent throughout.
At 22 tools, the set is on the heavy side for the stated scope, though each tool has a clear role. This falls into the borderline '16-25 feels heavy' range, as some raw data tools could potentially be consolidated.
The server covers connection, raw data collection, multi-dimensional diagnostics, export, and capabilities, forming a nearly complete lifecycle for runtime observation. Missing only advanced interactions like hot reload or CPU profiling, which are noted as intentional limitations.