@ahmedshaikh/flight-recorder-mcp
v0.4.2
Published
Execution tracer as an MCP server — record a Python/Node run once (calls, args, returns, exceptions, line probes), then query the recording instead of printf-debugging and rerunning.
Maintainers
Readme
flight-recorder-mcp
Execution tracer as an MCP server — record a run once, then query it like a database.
Agents debug by printf-looping: add a print, rerun, read output, add another print, rerun.
Five to ten full cycles to answer one question like "what was user_id the third time
through that loop?"
flight-recorder replaces the loop with one recording. trace_run executes a Python or
Node command under instrumentation and stores every event in SQLite. After that, every
question is a query — no reruns, no code edits:
| question | tool |
| --- | --- |
| what was this function actually called with, and what did it return? | trace_calls |
| who called whom, in what order, with what values? | trace_tree |
| what was every variable when it crashed? | trace_exception |
| what was total on each loop iteration? | trace_values (via probes) |
| where did the string "abc123" appear during the run? | trace_search |
| what changed between the passing and the failing run? | trace_diff |
What gets recorded
Python (3.8+ — no dependencies, no code changes; on 3.12+ a sys.monitoring
(PEP 669) backend is used, falling back to sys.settrace on older interpreters):
- every call with argument values (
repr, truncated) - every return with the return value and duration
- every exception with its propagation path through frames (including caught ones)
- on an uncaught crash: local variables of every frame in the traceback
- probe lines: all (or selected) locals captured each time a
file:lineexecutes
Node (>= 22, driven over the Chrome DevTools Protocol via --inspect-brk):
- uncaught exceptions / unhandled rejections: per-frame variables at the moment of the throw (local, block, closure, and script scopes)
- every exception thrown in user code (even later caught), with throw-site variables
- probe lines as above
- full call tracing for Node is on the roadmap; crash forensics and probes are the high-value 90%
Only your code is traced by default — stdlib and site-packages are excluded unless
include_deps: true.
Example
trace_run command="python3 app.py" probes=[{"file": "app.py", "line": 20, "vars": ["total"]}]
→ trace #7 — python3 app.py (python, exit 1, 342ms, 1204 events)
UNCAUGHT KeyError: 'quantiy'
path: <module> (app.py:39) → main (app.py:36)
top functions: parse ×50 [12.3ms] · validate ×50 · main ×1
probes: 3 hit(s) — trace_values trace_id=7 to inspect
trace_exception trace_id=7
→ UNCAUGHT KeyError: 'quantiy'
at main (app.py:36)
locals: missing={'sku': 'zzz'} total=9 items=[{'sku': 'abc', 'qty': 2}, …]
trace_values trace_id=7 file=app.py line=20
→ 3 probe hit(s): total=0 · total=2 · total=5
trace_diff trace_a=6 trace_b=7 # passing run vs failing run
→ outcome changes: transform: never raised in A · raised ×2 in B
call-count changes: transform A×3 B×6
exceptions only in B: ValueError at transform (diffy.py:6) ×2 — too big: 4
first value divergence: call position 3, pipeline: A (nums=[0, 1, 2]) · B (nums=[0, 1, 2, 3, 4, 5])Tools
| tool | purpose |
| --- | --- |
| trace_run | run a python/node command under the tracer, get a trace id |
| trace_calls | every invocation of a function: args → return/exception, duration |
| trace_tree | the call tree with args and returns inline; re-rootable |
| trace_exception | crash post-mortem: per-frame locals + propagation chains |
| trace_values | variable values at a probed line, across all hits |
| trace_search | substring search over all recorded events and values |
| trace_diff | compare two traces: outcome flips, call-count changes, new exceptions, first value/return/structural divergence, probe diffs |
| trace_list / trace_delete | manage recordings (auto-prunes to last 20) |
Install
// .mcp.json
{
"mcpServers": {
"flight-recorder": {
"command": "npx",
"args": ["-y", "@ahmedshaikh/flight-recorder-mcp"]
}
}
}Traces are stored in ~/.flight-recorder/traces.db (override with FLIGHT_RECORDER_DB).
Runs are capped at 200k events and 120s by default (max_events, timeout_s).
Tracing test runs
trace_run accepts python -m module commands, and bare pytest … is rewritten to
python3 -m pytest …. Tracing a failing test shows what the code under test was
actually called with — trace_calls func=average might show
average(nums=[2, 4, 6]) → 4.0 next to average(nums=[]) ⚡ ZeroDivisionError.
In module mode, code under the run's cwd is traced; the test runner's own
internals are not (unless include_deps).
Limits
- node entry point must be a script path (
node -eis not supported) - Python values are captured as truncated
reprstrings, not live objects - generators/coroutines record one call segment per resume
- overhead: on Python 3.12+ untraced code (test runners, stdlib, deps) is ~free (~4% measured on a stdlib-heavy workload, vs ~80% for the settrace fallback); fully-traced hot code is serialization-bound at roughly 2–10× either way — cap or exclude accordingly
- interpreter internals (stdlib/site-packages) are excluded even when a venv
lives inside the project directory, unless
include_deps - child processes spawned by the traced program are not traced
(
multiprocessing,subprocess, pytest-xdist workers) - traced values (arguments, returns, locals) are stored as plaintext in the
trace database — if traced code handles secrets, they end up in
~/.flight-recorder/traces.db; usetrace_delete all=trueto purge - on timeout the run gets SIGTERM (flushing events recorded so far) and a
SIGKILL two seconds later; the partial trace is kept with status
timeout
Development
npm install
npm test # node:test; python tests need python3 on PATH
npm run buildMIT
