engine-dj-mcp 0.9.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,168 +1,400 @@
1
1
  # engine-dj-mcp
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/engine-dj-mcp)](https://www.npmjs.com/package/engine-dj-mcp)
4
+ [![licence](https://img.shields.io/npm/l/engine-dj-mcp)](./LICENSE)
5
+
3
6
  An MCP server that lets an AI assistant search and audit your **Engine DJ**
4
- libraries — the one on your Mac and the ones on your USB drives.
7
+ libraries — the one on your computer and the ones on your USB drives.
5
8
 
6
9
  > **Not affiliated with, endorsed by, or sponsored by inMusic Brands, Denon
7
10
  > DJ, or the Engine DJ product.** "Engine DJ" is used here only to name the
8
11
  > software whose library this tool reads. No logos or brand artwork from
9
12
  > inMusic or Denon DJ are used in this project.
10
13
 
11
- ## What it does
12
-
13
- - **Smart search** (`search_tracks`) — full text with diacritics folded,
14
- tempo windows, harmonic key matching, rating, and play history ("what have
15
- I not played in six months?").
16
- - **Track lookup** (`get_tracks`) — full metadata for specific track ids.
17
- - **Cues, loops and beatgrids** (`get_track_performance`) decoded from
18
- Engine's `PerformanceData`, with a per-field decode status. The cue and
19
- beatgrid layouts are reverse-engineered but **validated against a real
20
- Engine library**; the loop layout is not fully validated — see
21
- [Limitations](#limitations).
22
- - **Collection audit** (`audit_library`) missing files, unanalysed tracks,
23
- tracks without cues or beatgrids, duplicates, suspicious tempos, orphaned
24
- playlist entries.
25
- - **Read-only SQL** (`run_sql`) an escape hatch for anything the tools
26
- above do not cover.
27
- - **Library and index introspection** (`list_libraries`, `refresh_index`) —
28
- see what was discovered and force a search-index rebuild.
29
-
30
- It also exposes two MCP resources: `engine://schema` (the field semantics an
31
- assistant needs before writing SQL against the library) and
32
- `engine://libraries` (what was discovered, and whether each library's schema
33
- is supported).
14
+ Your library is opened **read-only at the operating-system level**. It is
15
+ never written to unless you start the server with `--allow-writes` — see
16
+ [Safety](#safety).
17
+
18
+ ## What you can ask
19
+
20
+ Once connected, these are ordinary questions in chat:
21
+
22
+ - *"Something dark around 124 in a minor key I haven't played in six months."*
23
+ - *"Which tracks still have no hot cue set?"*
24
+ - *"Find me anything harmonically compatible with 8A between 138 and 142."*
25
+ - *"What's in my ACID Beach playlist, in order?"*
26
+ - *"Anything around 128 in ACID Beach?"*
27
+ - *"What's broken in my collection — missing files, duplicates, bad tempos?"*
28
+ - *"Where are the cue points on this track, and what tempo did Engine analyse?"*
29
+ - *"Build me a playlist of everything in 5A from 140 BPM up."* (needs `--allow-writes`)
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ npx engine-dj-mcp
35
+ ```
36
+
37
+ Claude Desktop — add to your configuration:
38
+
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "engine-dj": { "command": "npx", "args": ["-y", "engine-dj-mcp"] }
43
+ }
44
+ }
45
+ ```
46
+
47
+ To let the assistant create playlists as well, add `--allow-writes` — a flag
48
+ in `args` rather than an environment variable precisely so it is visible in
49
+ the configuration you are reading:
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "engine-dj": { "command": "npx", "args": ["-y", "engine-dj-mcp", "--allow-writes"] }
55
+ }
56
+ }
57
+ ```
58
+
59
+ **Requirements:** Node.js 22.13 or newer (for the unflagged `node:sqlite`;
60
+ there are no native dependencies), and an Engine DJ library at schema 3.0.0
61
+ through 3.0.2 — Engine DJ 4.5 and 5.x.
62
+
63
+ ## Tools
64
+
65
+ Nine read-only tools, and a tenth — `create_playlist` — that appears only
66
+ when you start the server with `--allow-writes`. Every tool that reads
67
+ library data also accepts an optional `library` argument — see
68
+ [Choosing a library](#choosing-a-library).
69
+
70
+ ### `search_tracks`
71
+
72
+ The main one. Full-text search with diacritics folded, plus filters for
73
+ tempo, key, rating, when a track was added and when it was last played.
74
+
75
+ | Argument | What it does |
76
+ | --- | --- |
77
+ | `q` | Full text over title, artist, album, genre, comment and label. Diacritics are folded, so `bjork` matches `Björk` — Engine's own search does not. |
78
+ | `bpm` | `{ min, max }` or `{ around, tolerance_pct }`. Resolved tempo, so an analysed BPM wins over the tag. |
79
+ | `key` | `{ camelot: [...] }` for exact keys, `{ compatible_with: "8A" }` for harmonic neighbours, `{ mode: "minor" }` for a whole side of the wheel. |
80
+ | `rating` | `{ min, max }`, 0–5. |
81
+ | `played` | `{ never: true }`, or `{ before, after }` taking an ISO date or a relative form like `-6 months`. |
82
+ | `added` | `{ before, after }`, same date forms. |
83
+ | `flags` | `analyzed`, `available`, `has_cues`, `has_beatgrid`. `has_cues` means a hot cue is genuinely set — see [Limitations](#limitations). |
84
+ | `playlist` | `{ id }` or `{ name }` — search inside one playlist. Results still come back by relevance or id; `get_playlist_tracks` is what preserves playlist order. |
85
+ | `fields` | Which columns to return. Defaults to `id, artist, title, bpm, camelot, rating`. |
86
+ | `limit`, `cursor` | Page size (default 25, max 200) and an opaque cursor for the next page. |
87
+ | `include_total` | Off by default because counting costs far more than the page. Capped at 1000 — a capped result carries `total_capped: true` and means "at least 1000". |
88
+
89
+ ### `get_tracks`
90
+
91
+ Full metadata for specific track ids, returned in the order you asked for.
92
+ Unknown ids are omitted rather than failing the call.
93
+
94
+ `ids` (required), `fields`, `redact_paths`.
95
+
96
+ ### `get_playlists`
97
+
98
+ Your playlist tree, in the order Engine DJ shows it — folders included.
99
+
100
+ The list is flat and in the order you would read down the sidebar with every
101
+ folder expanded: `depth` and `path` carry the nesting, `parent_id` names the
102
+ folder a list sits in.
103
+
104
+ | Field | Meaning |
105
+ | --- | --- |
106
+ | `name`, `id`, `path` | `path` is the full `Folder/Sub/Name`, and is unique — a bare `name` need not be. |
107
+ | `depth`, `parent_id` | The nesting. `parent_id` is `null` at the top level. |
108
+ | `is_folder` | The list has child lists. Engine has no folder flag — a folder *is* a playlist that other playlists sit under — so an emptied folder reads as an empty playlist. |
109
+ | `is_persisted` | Engine's own flag for a list saved to the device. Both values appear on lists Engine displays, so nothing is filtered on it. |
110
+ | `track_count` | Entries in that list alone, never rolled up from its children — the number Engine shows beside it. |
111
+ | `missing_count` | How many of those entries name a track this library does not have. |
112
+
113
+ `limit` (default 200, max 1000). `warnings` appears if a playlist's link
114
+ chain is damaged; nothing is ever dropped from the list because of one.
115
+
116
+ ### `get_playlist_tracks`
117
+
118
+ The tracks of one playlist, **in playlist order**.
119
+
120
+ Name it with `playlist_id` or with `playlist_name` — exactly one of the two.
121
+ Playlist names are unique only within a folder, so a name matching more than
122
+ one is refused with every candidate's id and full path rather than guessed
123
+ at; pass the `path` from `get_playlists` to say which you meant.
124
+
125
+ Every row carries `position`, its 1-based place in the playlist. Same
126
+ `fields`, `limit` and `cursor` conventions as `search_tracks`.
127
+
128
+ An entry whose track is not in this library keeps its slot and comes back as
129
+ `{ position, entry_id, track_id, missing: true }`, with `missing_count`
130
+ alongside `entry_count`. That is ordinary rather than corruption — playlist
131
+ entries outlive their tracks and travel between drives — and they are kept in
132
+ place so the number of rows still matches the playlist's own length. On one
133
+ reference library a 43-entry playlist holds exactly one track that library
134
+ can actually play.
135
+
136
+ ### `get_track_performance`
137
+
138
+ Decodes the binary `PerformanceData` Engine stores per track: hot cues, the
139
+ main cue, saved loops, the beatgrid, and a coarse waveform profile.
140
+
141
+ Every field carries its own decode status **and** its own `layout` marker.
142
+ `layout: "verified"` means the byte layout was confirmed against a real
143
+ library, so `status: "ok"` is a claim about the values. `layout: "unverified"`
144
+ would mean only that the bytes parsed; no field returns it today.
145
+
146
+ Positions are sample offsets; cue and loop items also carry seconds.
147
+ `items: []` with `slots: 8` means an analysed track with no cues set.
148
+
149
+ `id` (required).
150
+
151
+ ### `audit_library`
152
+
153
+ Ten collection health checks. Returns a count and a small sample of ids per
154
+ check, never the full result set — a library with thousands of unanalysed
155
+ tracks should not fill an assistant's context.
156
+
157
+ | Check | Finds |
158
+ | --- | --- |
159
+ | `missing_files` | Tracks whose file is gone from disk |
160
+ | `unavailable` | Tracks Engine has marked unavailable |
161
+ | `unanalyzed` | Tracks Engine has not analysed |
162
+ | `no_cues` | Tracks with no hot cue set |
163
+ | `no_beatgrid` | Tracks with no beatgrid data |
164
+ | `missing_key` | Tracks with no key detected |
165
+ | `suspicious_bpm` | Analysed and tagged tempo disagree, or tempo is outside 60–200 |
166
+ | `duplicates` | Same artist and title, or same size and length |
167
+ | `empty_metadata` | No artist or no title |
168
+ | `orphan_entries` | Playlist entries pointing at tracks not in this library — `get_playlist_tracks` shows where each one sits |
169
+
170
+ `checks` — omit it to run all ten.
171
+
172
+ ### `run_sql`
173
+
174
+ An escape hatch for questions the tools above do not cover. Read-only is
175
+ enforced by the kernel, not by this tool. Results are bounded whatever the
176
+ query says.
177
+
178
+ Prefer `side.track_derived.camelot` and `side.track_derived.tempo` in `WHERE`
179
+ clauses over the `camelot()` and `tempo()` SQL functions — the functions run
180
+ per row and defeat the indexes.
181
+
182
+ `sql` (required), `params`, `limit`.
183
+
184
+ ### `list_libraries`
185
+
186
+ Every library found, including ones whose schema is unsupported — listed
187
+ with their version, so you can tell a broken server from a missing library.
188
+ Re-scans on every call, so a drive plugged in after the server started shows
189
+ up without a restart. A library that is temporarily unreadable — Engine DJ
190
+ writing to it, say — stays listed with `status: "unreadable"` rather than
191
+ vanishing.
192
+
193
+ No arguments.
194
+
195
+ ### `refresh_index`
196
+
197
+ Rebuilds the search index if the library changed. Normally unnecessary; the
198
+ server checks staleness itself before answering.
199
+
200
+ ### `create_playlist`
201
+
202
+ The only tool that writes, and the only one that is not registered at all
203
+ unless the server was started with `--allow-writes`.
204
+
205
+ Creates one new top-level playlist from track ids — `track_ids` sets both
206
+ what is in it and the order it is in, so a list built by `search_tracks`
207
+ arrives in Engine DJ in the order the assistant chose. Nothing else changes:
208
+ no playlist is renamed, reordered, emptied or deleted, and no track, cue or
209
+ beatgrid is touched. The one existing row that moves is the previous last
210
+ playlist's link, and Engine's own insert trigger is what moves it.
211
+
212
+ | Argument | What it does |
213
+ | --- | --- |
214
+ | `title` | Name of the new playlist. Must not already be taken at the top level — Engine allows one name per folder. |
215
+ | `track_ids` | Ids from `search_tracks` or `get_tracks`, in playlist order. May be empty, for an empty playlist. A track may appear at most once, which is Engine's own rule. |
216
+
217
+ Each entry stores the track's origin identity — `(originDatabaseUuid,
218
+ originTrackId)`, the pair Engine matches on — not the local row id, so a
219
+ playlist built here reads the same way Engine's own does.
220
+
221
+ The result carries `playlist_id`, `tracks_added` and `backup_path`. **To undo
222
+ it, delete the playlist in Engine DJ**; `backup_path` is a whole-library
223
+ snapshot for the case where something went wrong at a lower level, not an
224
+ undo — see [Restoring a snapshot](#restoring-a-snapshot).
225
+
226
+ Refusals name themselves: `playlist_exists` for a taken title,
227
+ `unknown_track` for an id this library does not have, `duplicate_track` for
228
+ the same id twice, `library_busy` if something else holds a conflicting lock
229
+ right then, `library_needs_recovery` if Engine DJ left an unrecovered
230
+ journal behind. Every error also carries `detail`: `not_committed` means the
231
+ library is exactly what it was, and `committed_unverified` — the rare one —
232
+ means the write may have landed but could not be verified afterwards, and is
233
+ the only case that hands back a `backup_path`.
234
+
235
+ ## Resources
236
+
237
+ - **`engine://schema`** — the field semantics an assistant needs before
238
+ writing SQL: how Engine encodes musical key, why tempo is
239
+ `COALESCE(bpmAnalyzed, bpm)`, that `Track.path` is relative, where playlist
240
+ order really lives, and which helper columns are indexed.
241
+ - **`engine://libraries`** — what was discovered at startup and whether each
242
+ library's schema is supported. A snapshot; `list_libraries` is the live view.
34
243
 
35
244
  ## Choosing a library
36
245
 
37
246
  Engine DJ keeps a library on your computer and another on every drive you
38
- export to, so more than one is usually connected at once. `list_libraries`
39
- reports each of them with a `uuid` and a `path`, and **every tool that reads
40
- library data takes an optional `library` argument**`search_tracks`,
41
- `get_tracks`, `get_track_performance`, `audit_library`, `run_sql` and
42
- `refresh_index`. Pass either the `uuid` or the `path` exactly as
43
- `list_libraries` printed it; the `~/…` form it reports is accepted alongside
44
- the absolute one. A value matching neither comes back as
45
- `library_not_found`, listing the libraries you can actually choose from.
46
-
47
- Leave `library` out and the server uses **the supported library holding the
48
- most tracks**, ties broken by the order the drives were scanned in. That
49
- matters: the local library Engine DJ creates on install is scanned first and
50
- is often empty, so "the first one found" would hide the drive you actually
51
- work from.
52
-
53
- Each library gets its own search index and its own read-only connection,
54
- opened the first time you ask that library a question — mounting four drives
55
- does not cost four connections. Every query, audit and file-path check stays
56
- inside the library selected for that call. Comparing two libraries against
57
- each other — "what is on this drive but not that one?" — is **not** something
58
- this server does.
247
+ export to, so more than one is usually connected. `list_libraries` reports
248
+ each with a `uuid` and a `path`, and every tool that reads library data takes
249
+ an optional `library` argument. Pass either form exactly as printed the
250
+ `~/…` path is accepted alongside the absolute one. A value matching neither
251
+ comes back as `library_not_found`, listing what you can choose from.
252
+
253
+ Leave it out and the server uses **the supported library holding the most
254
+ tracks**. That matters: the local library Engine DJ creates on install is
255
+ scanned first and is often empty, so "the first one found" would hide the
256
+ drive you actually work from.
257
+
258
+ Each library gets its own index and its own connection, opened the first time
259
+ you ask that library something. Comparing two libraries against each other
260
+ *"what is on this drive but not that one?"* — is **not** something this server
261
+ does.
59
262
 
60
263
  ## Safety
61
264
 
62
265
  Your library is opened **read-only at the operating-system level**, not by
63
- convention or by a `PRAGMA` that a query could turn back off. Writes are
64
- refused by SQLite itself, and no file is ever created inside your `Engine
65
- Library` folder. The server keeps its search index in `~/.engine-dj-mcp/`.
266
+ convention and not by a `PRAGMA` a query could turn back off. Writes are
267
+ refused by SQLite itself, and without `--allow-writes` no file is ever
268
+ created inside your `Engine Library` folder. The search index lives in
269
+ `~/.engine-dj-mcp/`.
270
+
271
+ ### Writing
66
272
 
67
- `run_sql` accepts arbitrary SQL, but only ever the first statement of it is
273
+ Without `--allow-writes` the server has no tool that can write, and the
274
+ paragraph above holds exactly as written: SQLite itself refuses.
275
+
276
+ With the flag, one tool appears — `create_playlist`. It adds a new playlist
277
+ and nothing else: no existing playlist is renamed, reordered, emptied or
278
+ deleted, and no track, cue or beatgrid is touched. The single change to an
279
+ existing row is the previous last playlist's link, made by Engine's own
280
+ trigger.
281
+
282
+ Before the first write of a session the database is snapshotted to
283
+ `~/.engine-dj-mcp/backups/`, and every write of that session returns its
284
+ path. Ten snapshots are kept per library — per library *file*, so a library
285
+ and its clone on another drive do not share the ten.
286
+
287
+ One file *is* created inside your `Engine Library` folder while a write is in
288
+ progress: SQLite's rollback journal, `m.db-journal`, next to `m.db`. It is
289
+ removed when the transaction commits, and it is what makes the write
290
+ all-or-nothing. If the process is killed mid-transaction the journal is left
291
+ behind, and both this server and Engine DJ then treat the library as needing
292
+ recovery — this server reports `library_needs_recovery` and refuses to touch
293
+ the library, including for reads, until you have launched Engine DJ once so
294
+ it can roll the journal back. Nothing else is ever written in that folder,
295
+ and without `--allow-writes` not even this.
296
+
297
+ The write takes SQLite's own write lock for the length of one transaction and
298
+ does not wait for it: if something else — Engine DJ mid-save, a player — is
299
+ holding a conflicting lock at that moment, the write is refused with
300
+ `library_busy` and nothing is changed. Merely having Engine DJ *open* is not
301
+ usually a conflict, and the write normally succeeds with Engine running;
302
+ Engine will show the new playlist after it next re-reads the library.
303
+
304
+ ### Restoring a snapshot
305
+
306
+ `backup_path` is not an undo. It is a copy of the **whole** `m.db` from
307
+ before the session's first write, so putting it back reverts the entire
308
+ library to that moment: every play count, import, cue, beatgrid and rating
309
+ Engine DJ has written since is discarded along with the playlist you wanted
310
+ gone. Reach for it only if the library itself is damaged — the case where
311
+ `create_playlist` comes back with `detail: "committed_unverified"`.
312
+
313
+ **To undo a playlist, delete it in Engine DJ.** Engine's own delete trigger
314
+ repairs the playlist chain and cascades the entries away, which is exactly
315
+ what removing it should do and is not something restoring a snapshot does
316
+ better.
317
+
318
+ `run_sql` accepts arbitrary SQL, but only the first statement is ever
68
319
  executed, and `VACUUM`, `ATTACH` and `DETACH` are rejected outright, so a
69
320
  chained or exfiltrating statement cannot slip past the read-only connection.
70
321
 
71
322
  If Engine DJ was closed uncleanly and left an unrecovered journal, this
72
- server will never open the library writably to "fix" it for you — that
73
- would break the one guarantee this project makes. It reports
74
- `library_needs_recovery` instead and asks you to launch Engine DJ once so it
75
- can recover the library itself.
323
+ server will not open the library to "fix" it, with or without
324
+ `--allow-writes` rolling a journal forward is a repair on someone else's
325
+ file, and `create_playlist` refuses such a library outright rather than
326
+ letting SQLite do it on the way in. It reports `library_needs_recovery` and
327
+ asks you to launch Engine DJ once so it can recover its own library.
76
328
 
77
329
  ## Limitations
78
330
 
79
331
  Read this before deciding what to trust.
80
332
 
81
- - **Cue, beatgrid and waveform layouts are validated; the loop layout is
82
- not.** The
83
- binary layouts inside `PerformanceData` are reverse-engineered, so every
84
- decoded field says which kind it is:
85
-
86
- - `layout: "verified"` **cues** (including the main cue), the
87
- **beatgrid**, and the **waveform summary**. These were derived from and
88
- checked against a real Engine DJ 3.0.x library of 281 analysed tracks:
89
- cue offsets land inside the track, the beatgrid's implied tempo matches
90
- `bpmAnalyzed` on all 281, and the waveform's declared point spacing
91
- multiplies back out to the track's sample count on all 281. A
92
- `status: "ok"` here is a claim about the values, not just the parse.
93
- - `layout: "unverified"` — **loops**. The blob's slot structure is known
94
- (eight slots, uncompressed, little-endian) and an empty one decodes
95
- correctly, but not one track in that library had a loop saved, so the
96
- meaning of a *populated* loop slot is untested. Do not report loop
97
- bounds to a user as fact.
98
-
99
- A `layout` marker is a claim about the **bytes** — which offset holds
100
- which field, and what the number there means. It is not a claim about
101
- every name this server puts on them. Three labels are inferred rather than
102
- measured, and the code says so where each is defined: which of a cue's
103
- four colour bytes is which channel (they are reported as stored, as one
104
- 32-bit value, with no channel claim attached); that the second beatgrid is
105
- the one Engine calls "adjusted" (that it is the one Engine *plays* is
106
- measured — on seven tracks the other grid runs at exactly half the
107
- analysed tempo); that `main_cue.is_adjusted` is what its flag byte means;
108
- and that the waveform's three bytes per point are the low, mid and high
109
- bands in that order. None of these affects a value you get back.
110
-
111
- Everything else the server reports — titles, artists, tempo, key, ratings,
112
- play history, file paths — is read straight from the database and carries
113
- no such caveat.
114
- - **`has_cues` and `no_cues` mean "a hot cue is set", and cost a little to
115
- say so.** Engine writes a `quickCues` blob to every analysed track whether
116
- or not any pad is used, so the cheap SQL test (`quickCues` present and
117
- non-empty) answers a question about *analysis*: in the 281-blob reference
118
- library all 281 would count as having cues, while two tracks (three rows,
119
- one track being exported to a second library) actually do. The blob is
120
- therefore decoded when the search index is built, and
121
- `search_tracks(flags: { has_cues: true })` and `audit_library`'s `no_cues`
122
- both read the decoded answer. That costs about 100 ms of extra rebuild
123
- time at 50,000 tracks, and a rebuild only runs when your library changes.
124
-
125
- The track's **main cue** does not count towards it: Engine sets that as a
126
- playback start marker rather than the DJ placing it (it is set on 159 of
127
- the 281 blobs, including every track the library records as played), so
128
- counting it would answer a third question again. `has_beatgrid` and
129
- `no_beatgrid` do still test for the blob — a `beatData` blob has no
130
- "written but empty" state, and on all 281 real blobs presence and a usable
131
- grid have never disagreed.
132
- - **It never writes to your library.** Not to add a cue, not to fix a tag,
133
- not even to recover a journal Engine DJ left behind. The connection is
134
- read-only at the kernel level, so a write is refused by SQLite itself
135
- rather than by a rule this code could get wrong.
136
- - **It does not read play history.** `Track.timeLastPlayed` is used to answer
137
- "what have I not played in six months?", but the separate Engine history
138
- database — individual sessions, decks, what followed what — is not opened
139
- at all.
140
- - **It does not build set lists**, reorder playlists, or suggest transitions.
141
- It answers questions about the collection; the mixing is yours.
142
- - **Schema 3.0.0 through 3.0.2 only** (Engine DJ 4.5 and 5.x). Older and
143
- newer libraries are listed with their version and reported as unsupported
144
- rather than read on a guess.
145
-
146
- ## Requirements
147
-
148
- - Node.js 22 or newer
149
- - Engine DJ with library schema 3.0.0–3.0.2 (Engine DJ 4.5 and 5.x)
333
+ **The `PerformanceData` layouts are reverse-engineered**, so every decoded
334
+ field says which kind it is. All four are marked `layout: "verified"` —
335
+ derived from and checked against a real Engine DJ 3.0.x library of 281
336
+ analysed tracks, where cue offsets land inside the track, the beatgrid's
337
+ implied tempo matches `bpmAnalyzed` on all 281, and the waveform's declared
338
+ point spacing multiplies back out to the track's sample count on all 281.
150
339
 
151
- ## Install
340
+ Loops were the last to earn it. The slot grid was pinned down by 2248
341
+ sentinels, but no library available had a loop saved in it, so a *populated*
342
+ slot stayed untested and loops carried `layout: "unverified"` through several
343
+ releases. One deliberately saved loop settled it: its slot spans 1.678321678 s
344
+ on a track Engine analysed at 143 BPM, which is four beats to within 4e-15 s.
345
+ Only the right field order, unit and endianness land on a whole beat count.
152
346
 
153
- ```bash
154
- npx engine-dj-mcp
155
- ```
347
+ A `layout` marker is a claim about the bytes, not about every name put on
348
+ them. Four labels are inferred rather than measured, and the code says so
349
+ where each is defined: which of a cue's four colour bytes is which channel
350
+ (they are returned as stored, one 32-bit value, with no channel claim); that
351
+ the second beatgrid is the one Engine calls "adjusted" (that it is the one
352
+ Engine *plays* is measured — on seven tracks the other runs at exactly half
353
+ the analysed tempo); that `main_cue.is_adjusted` is what its flag byte means;
354
+ and that the waveform's three bytes per point are low, mid and high in that
355
+ order. None affects a value you get back.
156
356
 
157
- Claude Desktop configuration:
357
+ Everything else — titles, artists, tempo, key, ratings, play history, file
358
+ paths — is read straight from the database and carries no such caveat.
158
359
 
159
- ```json
160
- {
161
- "mcpServers": {
162
- "engine-dj": { "command": "npx", "args": ["-y", "engine-dj-mcp"] }
163
- }
164
- }
165
- ```
360
+ **`has_cues` and `no_cues` mean "a hot cue is set".** Engine writes a
361
+ `quickCues` blob to every analysed track whether or not a pad is used, so the
362
+ cheap SQL test would answer a question about *analysis* instead: in the
363
+ reference library all 281 blobs would count as having cues, while two tracks
364
+ actually do. The blob is therefore decoded while the index is built. That
365
+ costs roughly 100 ms extra at 50,000 tracks, and only when your library
366
+ changes. The track's **main cue** does not count towards it — Engine sets
367
+ that as a playback marker rather than the DJ placing it. `has_beatgrid` does
368
+ still test for the blob: `beatData` has no "written but empty" state.
369
+
370
+ **It writes nothing but playlists, and only when you ask for it.** Without
371
+ `--allow-writes` the library is opened read-only at the OS level and there is
372
+ no tool that could write. With the flag, `create_playlist` adds playlists —
373
+ and that is the whole list. Not a cue, not a tag, not a rating, and not even
374
+ the recovery of a journal Engine DJ left behind.
375
+
376
+ **It does not read play history.** `Track.timeLastPlayed` answers "what have I
377
+ not played in six months?", but the separate Engine history database —
378
+ sessions, decks, what followed what — is not opened at all.
379
+
380
+ **Smartlists are not reported.** Engine's rule-based lists live in a separate
381
+ `Smartlist` table with its own ordering and a JSON rule column, and nothing
382
+ here reads it. `get_playlists` reports ordinary playlists and folders only, so
383
+ a smartlist you can see in Engine will not appear.
384
+
385
+ **An empty folder reads as an empty playlist.** Engine's schema has no folder
386
+ flag — a folder is simply a playlist that other playlists sit under — so
387
+ `is_folder` means "has child lists". A folder you have emptied is
388
+ indistinguishable from a playlist with no tracks.
389
+
390
+ **Playlists can be created, not edited.** With `--allow-writes` a new
391
+ playlist can be added; there is no reordering, renaming, deleting, or adding
392
+ a track to a playlist that already exists, and no set lists or suggested
393
+ transitions. It answers questions about the collection and writes down the
394
+ answer if you ask; the mixing is yours.
395
+
396
+ **Schema 3.0.0 through 3.0.2 only.** Older and newer libraries are listed with
397
+ their version and reported as unsupported rather than read on a guess.
166
398
 
167
399
  ## Licence
168
400
 
@@ -8,10 +8,12 @@
8
8
  * sample count. `status: "ok"` on a verified field is a claim about the
9
9
  * values, not merely about the parse.
10
10
  *
11
- * `"unverified"` still means what it always did: the bytes parsed without
12
- * contradicting the layout, and nothing more. It survives on `loops` because
13
- * no track in the 281 examined has a loop set, so while the slot structure is
14
- * pinned down, the meaning of a *populated* slot is untested.
11
+ * `"unverified"` means the bytes parsed without contradicting the layout, and
12
+ * nothing more. No field carries it today: `loops` was the last one, and it
13
+ * held the marker only for as long as no library available had a loop saved in
14
+ * it. One saved loop supplied the missing prediction see `decodeLoops` and
15
+ * the marker is kept here because it is what a future field decoded from an
16
+ * unpopulated example would deserve.
15
17
  *
16
18
  * The marker is about the *bytes*: which offset holds which field, and what
17
19
  * the numbers there mean. It is not a claim about every English word this
@@ -215,19 +217,23 @@ export declare function hasCueSet(buf: Buffer | null): boolean;
215
217
  * (23 bytes with an empty label)
216
218
  *
217
219
  * Evidence for the framing and the slot grid: 8 + 8×23 = 192, the size of
218
- * all 281 real blobs, every one of which parsed to exactly its last byte;
220
+ * every unpopulated real blob, each of which parsed to exactly its last byte;
219
221
  * the little-endian count reads as 8, and the little-endian -1.0 sentinel
220
222
  * (`000000000000f0bf`) appears at the offsets this layout predicts in all
221
223
  * 2248 slots. Reading the count big-endian gives 134217728, which is what
222
224
  * made every real track decode as `unsupported` before.
223
225
  *
224
- * This layout keeps `layout: "unverified"`. Not one of the 2248 slots is
225
- * populated this library has no saved loops so while the slot grid is
226
- * pinned down by 2248 sentinels, the six bytes after each slot's two doubles
227
- * are zero everywhere, and nothing here distinguishes start/end from
228
- * end/start, or fixes the order of the flag and colour bytes. A populated
229
- * loop is the one thing the available data cannot exercise, so it is not
230
- * claimed as verified.
226
+ * The sentinels pin the slot grid, but an empty slot cannot say which double
227
+ * is the start, what unit the doubles are in, or what the six trailing bytes
228
+ * mean so this layout carried `unverified` for as long as no library had a
229
+ * saved loop in it. The `saved-loop` golden fixture is that loop, and it
230
+ * settles all three at once. Its slot reads label "Loop 1", start 2129416.08
231
+ * and end 2203430.06, which at the 44100 Hz `beatData` declares for the track
232
+ * is 1.678321678 s — exactly four beats at the 143 BPM Engine analysed, to
233
+ * 3.6e-15 s. Nothing but the right field order, the right unit and the right
234
+ * endianness lands on a whole number of beats, so the layout is verified.
235
+ * The trailing bytes read `01 01` (start set, end set) and four colour bytes,
236
+ * which is the same tail `quickCues` slots carry.
231
237
  */
232
238
  export declare function decodeLoops(buf: Buffer | null, sampleRate?: number | null): LoopsResult;
233
239
  /**