An append-only, JSONL-backed store that records one durable trace per MQO interaction — the question, how it bound, how well it grounded, and what executing it returned.
You cannot improve a query pipeline you cannot replay. To mine the gaps where MQO mis-binds, to score grounding quality over time, and to export training data, you need every interaction on disk in a form you can scan months later — not metrics aggregated at write time and thrown away. That argues for the simplest durable thing that survives a crash: a flat JSONL file, one record per line, appended and never mutated. This crate is that store, and nothing more. No database, no network, no async — a single file the rest of the toolchain reads.
The constraint that shapes the design is that the store is write-heavy and read-rarely, and writes happen on the live query path. So appends must be cheap and crash-safe, and a single corrupt line — the cost of a process killed mid-write — must never block a later read.
A library, not a binary. Add it as a dependency:
[dependencies]
mcp-trace-store = { git = "https://github.com/joeyen-atscale/mcp-trace-store" }Build and run the test suite:
cargo test # all acceptance criteria
cargo test --release # also enforces the AC7 timing SLO (see below)Open a store, append a record, scan it back through a filter:
use mcp_trace_store::{
BindOutcome, ExecuteOutcome, QualitySignals,
TraceFilter, TraceRecord, TraceStore, TraceStoreConfig,
};
let cfg = TraceStoreConfig::new("~/.local/share/mcp-traces/trace.jsonl");
let store = TraceStore::new(cfg)?; // creates parent dirs if needed
// record_id and timestamp_ms are filled in at write time
let record = TraceRecord::new(
"session-abc",
serde_json::json!({ "entity": "Revenue" }),
BindOutcome::Success,
ExecuteOutcome::Success { row_count: 10, result_empty: false },
QualitySignals {
first_attempt_bind: true,
bind_attempt_count: 1,
total_latency_ms: 42,
tokens_used: None,
},
);
store.append(record)?;
// Read back only the first-attempt successes
let filter = TraceFilter { first_attempt_only: true, ..TraceFilter::default() };
let hits = store.scan(&filter)?;scan returns records oldest-first across every rotation fragment. count returns the total record count with no filter applied.
One record per interaction. A TraceRecord captures the MQO JSON, the bind outcome, an optional grounding score and band, the execute result, and a small set of quality signals (first-attempt bind, attempt count, latency, tokens). Optional fields that weren't recorded deserialize as None; unknown fields in older or newer records are ignored, so the schema can evolve without breaking the corpus.
Appends are atomic. Each record is serialized in full, then written with O_APPEND in a single write_all. POSIX guarantees an O_APPEND write up to PIPE_BUF bytes is atomic against concurrent writers. A process killed mid-write leaves at most one partial line at the tail; every prior record is intact.
Corrupt lines are skipped, not fatal. scan reads line by line; a line that fails to parse is logged to stderr and skipped. One bad write never costs you the rest of the corpus.
Rotation by size. When the active file exceeds rotate_at_bytes (default 50 MB), it shifts to .1, the old .1 to .2, and so on. The next append starts a fresh active file. scan walks all fragments oldest-first — highest-numbered first, then the active file.
<path> ← active file (newest writes)
<path>.1 ← previous rotation
<path>.2 ← two rotations ago (oldest)
| Type | Purpose |
|---|---|
TraceRecord |
One persisted interaction |
BindOutcome |
Success / Ambiguous / NotFound / Error(String) |
GroundingBand |
Grounded / Partial / Ungroundable |
ExecuteOutcome |
Success { row_count, result_empty } / Error(String) / Skipped |
QualitySignals |
first_attempt_bind, bind_attempt_count, total_latency_ms, tokens_used |
TraceFilter |
time range, grounding band, first-attempt, cluster, session, limit |
TraceStore |
append, scan, count, rotate_if_needed |
This is the corpus underneath the MQO toolchain. The store collects what the gap miner, quality scorer, and fine-tuning exporter all read; grounding scores come from mcp-grounding-eval. It is deliberately the least clever piece — a durable file the rest of the chain builds on.
All seven ACs run under cargo test. AC7's timing bound is asserted only in --release (a SHOULD, not a MUST); debug builds run the work but skip the assertion.
| AC | Description |
|---|---|
| AC1 | append creates the file and writes valid JSON lines |
| AC2 | scan filters by grounding_band correctly |
| AC3 | scan with first_attempt_only filters correctly |
| AC4 | A corrupt or partial line does not break scan |
| AC5 | Rotation works; scan reads all fragments in order |
| AC6 | Missing optional fields → None; unknown fields ignored |
| AC7 | 10k append + scan completes under 2s (release build) |
Early — version 0.1.0. The library is complete against the acceptance criteria above and compiles clean; the surface is a flat-file store with no public API beyond the types listed here.
MIT OR Apache-2.0