Flutter Lamp
Provides tooling for Android device transports, listing available transports, recommending a wireless one, and optionally promoting a USB-connected device to TCP so a Flutter app's VM Service tunnel survives cable disconnection.
Uses the Dart VM Service Protocol (WebSocket or HTTP) as the transport for observing app state: subscribing to VM timeline events (build/paint/layout/GC), collecting VM exceptions and logs, reading Dart heap and external memory, and recording HTTP timeline activity for network capture.
Connects an AI agent to a running Flutter app's Dart VM Service to stream live runtime data — exceptions with reconstructed stack traces, console/dart:developer logs, network requests, frame timings and jank, memory, route/navigation history, widget rebuild hotspots resolved to file and line, and widget-tree/Inspector snapshots — then correlates the evidence into evidence-first root-cause diagnoses for runtime errors and performance problems.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Flutter Lamp my app just crashed on login, what went wrong?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
💡 Flutter Lamp
Give your AI live eyes on a running Flutter app — no more pasting logs.
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.
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: …"Related MCP server: Production Monitoring MCP
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:ioprofiling — 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-diagnosisClaude Code skill
Tools
Tool | Purpose | Safety |
| Connect to a running app's Dart VM Service ( | mutates |
| Android transports; recommend wireless, optionally promote USB. | mutates |
| One-call triage — verdict plus exception/network/frame/log/memory summary. | read-only |
| Evidence from the window before a failure, with a timeline. | read-only |
| Current route and recent transitions, with per-route failures. | read-only |
| Widget rebuild hotspots resolved to widget, file and line. | read-only |
| Riverpod, Provider and Bloc activity over time, and how often it coincides with build-heavy frames. | read-only |
| The whole session as versioned JSON — | read-only |
| Why a diagnosis was reached: resolved evidence, alternatives, gaps. | read-only |
| Active collectors, tool safety classes, what cannot be observed. | read-only |
| Connection, session, reconnect state, event counts, retention window, what the session has cost in responses, and the VM-to-host clock offset. | read-only |
| Console + | read-only |
| Framework & unhandled exceptions with stack traces. | read-only |
| Frame build/raster timings; | read-only |
| HTTP requests (Dio & | read-only |
| Widget-tree snapshot from the Flutter Inspector. | read-only |
| Widget currently selected in the Inspector. | read-only |
| Dart heap + external memory (MB). | read-only |
| Recent VM timeline events (build/paint/layout/GC). | mutates |
| Root cause with a stable | read-only |
| Why the app is janky — percentiles, phase split, rebuild attribution. | read-only |
| 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.
Install
Requires Node ≥ 20. Nothing to clone — npx fetches it on first run:
npx -y flutter-lampOr install it globally:
npm install -g flutter-lampgit clone https://github.com/itsonu/flutter-lamp.git
cd flutter-lamp
npm install
npm run build
node dist/index.jsConnect 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-lampThe plugin pins an exact, published server version. Flutter Lamp is also
listed in the MCP Registry 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:
{
"mcpServers": {
"flutter-lamp": {
"command": "npx",
"args": ["-y", "flutter-lamp"]
}
}
}Or from the CLI:
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 HTTP/WS port. |
|
| Bind address (localhost only by default). |
| — | Set to |
| on | Set to |
| — | Comma-separated extra header-name patterns to redact. |
Usage
Run your Flutter app in debug/profile mode:
flutter runCopy 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=8181gives a stable URI across restarts.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.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:
flutter run -d 192.168.88.3:5555The 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 browserEverything 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:ioHTTP profiling are not available in release builds.Network is pull-on-demand. Dart exposes no push stream for
dart:ioHTTP, so requests are fetched whenget_networkordiagnose_runtimeruns — not streamed continuously.Exceptions are not always observable.
Debug.PauseExceptionneeds pause-on-exception enabled in the app to fire.Flutter.Erroris 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 reportsdegradedwhen 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_statusreports 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.1by default. ChangeDASHBOARD_HOSTonly 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.
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 and
docs/Observability-Roadmap.md.
Development
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/:
Doc | Contents |
Problem, users, principles, non-goals, constraints | |
Data flow, components, event model, how to add a collector | |
Non-negotiable constraints every change is checked against | |
Roadmap and status | |
The investigation protocol agents should follow | |
Current audit and prioritized backlog | |
Architecture target, phases, and explicit rejections | |
Where runtime events actually flow, and what is observable | |
How versions are staged, tagged and published | |
Dashboard design system — tokens, status vocabulary, component rules | |
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 © Chandrabhushan Prakash
Available Tools
22 toolsconnect_vmConnect to Flutter VM ServiceAIdempotent
Connect to a running Flutter app's Dart VM Service and start collecting runtime data (logs, exceptions, frames, network). Pass the ws:// or http:// URI printed by flutter run (line: 'A Dart VM Service ... is available at:'). NOT purely read-only: enables dart:io HTTP timeline logging on the app so network capture works, and adds the GC stream to the VM timeline recorder so garbage-collection pauses can be correlated with jank. Existing recorded streams are preserved, never replaced.
Inputs: uri (required).
diagnose_performanceDiagnose performanceARead-onlyIdempotent
Why the app is janky, not just how much. Returns frame percentiles, the build-vs-raster split, and findings correlating jank against in-flight requests, route transitions and heap growth — each with its own evidence ids, strength and fix. Reports 'healthy' when jank is within normal range and 'unknown' when there are too few frames to tell a pattern from noise. States what it cannot see: no CPU sampling, no widget rebuild counts. GC pauses are captured and correlated, but the strength of that correlation is reported against how much of the window frames actually covered.
Inputs: no named parameters.
diagnose_runtimeDiagnose runtimeARead-onlyIdempotent
Correlate captured runtime evidence into a root-cause diagnosis. Returns status (diagnosed|unknown), summary, rootCause, evidence (each with a citable eventId), a chronological timeline around the root cause, alternativeCauses that also fit, limitations describing what could not be seen, confidence (0-1) with a breakdown of evidence strength / data completeness / alternative strength, and recommended fixes. Status is 'unknown' below 70% rather than a guess.
Inputs: no named parameters.
ensure_tcp_devicePrefer a wireless device transportAIdempotent
Report Android device transports and recommend one, preferring wireless. A flutter run started on a USB transport loses its VM Service tunnel when the cable moves; one started on a TCP transport does not. Read-only by default. With promote:true it runs adb tcpip and adb connect to put a USB-attached device on a TCP transport — that restarts adbd on the device, needs the cable once, and is reversible with adb usb. Android-only: reports adbAvailable:false and changes nothing on iOS, desktop or web targets.
Inputs: port, serial, promote.
explain_diagnosisExplain a diagnosisARead-onlyIdempotent
Why diagnose_runtime reached its conclusion: the claim, every cited event resolved back to its full record, the timeline around the root cause, competing explanations, what evidence is missing, and the confidence breakdown. Use to answer 'why do you think that' without inventing reasoning.
Inputs: no named parameters.
export_sessionExport the debugging sessionARead-onlyIdempotent
The whole session as one versioned JSON artifact: metadata, per-collector health, retention, the captured events, and every diagnosis (runtime, performance, navigation, rebuilds). Use mode 'brief' for the smallest sufficient context — the diagnoses plus only the events their evidence cites — and 'full' to archive everything retained, for a bug report, offline analysis or a regression fixture. Sizes are not close: measured on a ~1,700-event session, 'brief' returned 36kB and 'full' returned 247kB — roughly 62,000 tokens, about a third of a 200k context window, so treat 'full' as something to write to a file rather than read inline. Credentials are already redacted at capture, so nothing here was ever stored raw.
Inputs: mode.
get_capabilitiesWhat this server can observeARead-onlyIdempotent
Machine-readable capability report: active collectors, every tool with its safety class, what can and cannot be observed on this target, and the current redaction, dashboard and retention configuration. Read this before attempting an operation that may not be supported.
Inputs: no named parameters.
get_dashboard_urlGet dashboard URLARead-onlyIdempotent
Return the URL of the live Realtime Runtime Dashboard (a browser UI streaming logs, network, exceptions, frames & memory). Open it in a browser to watch the app alongside the AI.
Inputs: no named parameters.
get_exceptionsGet runtime exceptionsARead-onlyIdempotent
Flutter framework errors and unhandled VM exceptions, most recent first. Each includes the error summary, offending widget, library, and a reconstructed stack trace (data.stackTrace) when available.
Inputs: limit.
get_framesGet frame timingsARead-onlyIdempotent
Frame build/raster timings. Set onlyJanky to focus on frames over the frame budget — 16.67ms (60fps) by default, which is an assumption: the VM Service does not report the display refresh rate. Override with FLUTTER_LAMP_FRAME_BUDGET_MS when the target's rate is known.
Inputs: limit, onlyJanky.
get_logsGet console & structured logsBRead-onlyIdempotent
Console output (Stdout/Stderr) and dart:developer logging, most recent first.
Inputs: limit, source, sinceMs, contains, +1 more.
get_memoryGet memory usageARead-onlyIdempotent
Current Dart heap usage for the main isolate (Dart heap in use, capacity, and external/native memory), in MB. Also records a snapshot into runtime history.
Inputs: no named parameters.
get_networkGet network requestsARead-onlyIdempotent
HTTP requests/responses captured via dart:io profiling (covers Dio & package:http). Fetches the latest profile on demand, then returns completed requests most recent first.
Inputs: limit.
get_rebuildsWidget rebuild hotspotsARead-onlyIdempotent
Which widgets are rebuilding and how often, resolved to widget name, file and line, with your own code ranked above package code. Use for 'why is this screen slow to build' and to find needless rebuilds. Requires a debug build with widget creation tracking; reports why it is empty otherwise.
Inputs: limit.
get_selected_widgetGet selected widgetARead-onlyIdempotent
The widget currently selected in the Flutter Inspector (via 'select widget mode' in the app/DevTools). Returns null if nothing is selected.
Inputs: no named parameters.
get_state_activityState-management activityARead-onlyIdempotent
How much state-management activity the app is doing and when, plus how often build-heavy frames coincide with it — use with get_rebuilds to answer 'is this rebuild storm driven by state churn'. Counts and timing only: Riverpod sends an app-side buffer offset and provider an element id, neither resolvable to a provider name or value. Counts are NOT transition counts — a provider event means dependents were notified, so one state change in a widget-heavy tree produces many events. Stock Bloc posts nothing itself; a flutter_bloc app appears here only as the provider activity its notifications cause.
Inputs: buckets.
get_timelineGet VM timeline eventsAIdempotent
Recent VM timeline trace events (build/paint/layout/GC/etc.), most recent first. Requires timeline recording — enable with recordFrom=true (sets Dart, GC, Compiler & Embedder streams) then reproduce the activity. recordFrom=true is NOT read-only: it changes the VM's recording configuration. Check recorderLagMs/stalled in the result: the VM recorder can stall permanently once its buffer fills while still reporting its streams as recorded, so events may be historical rather than current.
Inputs: limit, recordFrom.
get_widget_treeGet widget treeARead-onlyIdempotent
Snapshot of the running app's widget tree (summary tree from the Flutter Inspector). Use to understand structure, find a widget, or see what is mounted.
Inputs: maxDepth.
runtime_healthRuntime health snapshotARead-onlyIdempotent
One compact answer to 'is this app healthy right now'. Returns a verdict (healthy/degraded/failing/no-data) plus exception, network, frame, log and memory summaries with citable event ids, the retention window, and notes about anything that qualifies the numbers. Call this FIRST instead of calling six get_* tools.
Inputs: no named parameters.
runtime_statusRuntime health checkARead-onlyIdempotent
Report connection health, the current debugging session, reconnection state, how many runtime events have been captured by category, and the retention window (per-category capacity, how many events were evicted, and the oldest event still held). Use to confirm the MCP is receiving live data and to know how far back the evidence goes.
Inputs: no named parameters.
what_changedWhat changed before an incidentARead-onlyIdempotent
Evidence from the window leading up to a failure, plus a baseline comparison: the incident window measured against the equal window before it, per dimension (exceptions, network volume/failures/latency p50/p95, jank ratio, log errors, memory) with directions new/spiked/increased/decreased and citable evidence. Anchors on the given eventId, or the most recent exception, or the current time. Network uses interval matching, so a request that started before the window but failed inside it still counts. When the baseline predates observation, directions are unknown rather than fabricated.
Inputs: eventId, windowMs.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
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.
Maintenance
Related MCP Connectors
Drive real devices from your AI Coding tool. Embed a client SDK (Unity, Godot, Flutter, iOS/macOS, Android, React Native, Web) in your app, then capture screenshots, traverse the UI tree, inject taps and key events, and run automated test tasks on the physical device over a secure relay.
Flutter in-app bug tracking, visual pin annotations, and device logs directly into your AI IDE.
Watches your live app, explains every problem in plain language and writes the fix prompt.
Production runtime data for Android and KMP apps (startup, ANRs, slow screens) for coding agents
Related MCP Servers
- AlicenseCqualityDmaintenanceEnables AI assistants to connect to browser DevTools and backend debuggers for full-stack debugging, including frontend console, network, performance, and backend log analysis.1611 npm7MIT
- AlicenseAqualityBmaintenanceConnects AI agents to live observability stacks including Sentry, GitHub, Vercel, Better Stack, and Cloudflare, enabling end-to-end incident investigation, deployment correlation, root cause analysis, and regression triage.14MIT
- AlicenseNot gradedqualityBmaintenanceExposes 35 stdio JSON-RPC tools that let AI agents snapshot the Flutter Semantics tree, drive gestures, capture screenshots, and observe live app state against running Flutter apps over VM Service.3MIT
- AlicenseNot gradedqualityBmaintenanceExposes a stdio JSON-RPC tool surface that lets AI agents control running Flutter and Dart apps — starting, stopping, reloading, and hot-restarting sessions, reading logs, running doctor/list/tinker checks, and invoking plugin-contributed tools. It gives agents one shared protocol instead of ad hoc shell scripts.2MIT