Skip to main content
Glama

engine-dj-mcp

CI npm licence

Ask Claude, or any AI assistant, about your Engine DJ library: find tracks by BPM, key, genre or your own comments, check the collection for duplicates, missing files and tracks with no cues, read the cues and beatgrids Engine stored, and have playlists built or track tags fixed when you ask. It works through MCP, the Model Context Protocol, the standard way AI apps such as Claude Desktop, Claude Code, Cursor and VS Code connect to tools on your own computer: the app starts this server, and the assistant calls it when a question needs your library.

It reads the library on your computer and the ones on your USB drives. It opens them read-only at the operating-system level and changes nothing unless you start it with --allow-writes — then it can create and edit playlists and edit genre, comment, label, year and rating, and it copies the whole database to a backup before the first change. See Safety. The server itself sends nothing anywhere; what your AI app does with its answers is covered in PRIVACY.md.

Status: Active · Pre-1.0. In regular use and maintained; before 1.0 a MINOR release may change behaviour, a PATCH never does. Changes are recorded in CHANGELOG.md.

Not affiliated with, endorsed by, or sponsored by inMusic Brands, Denon DJ, or the Engine DJ product. "Engine DJ" is used here only to name the software whose library this tool reads and writes. No logos or brand artwork from inMusic or Denon DJ are used in this project.

What you can ask

Once connected, these are ordinary questions in chat:

  • "Something dark around 124 in a minor key I haven't played in six months."

  • "Find me anything harmonically compatible with 8A between 138 and 142."

  • "What's broken in my collection — missing files, duplicates, tracks with no cues?"

  • "Where are the cue points on this track, and what tempo did Engine analyse?"

  • "Build me a playlist of everything in 5A from 140 BPM up." (needs --allow-writes)

  • "Set the genre of these five tracks to Minimal and rate them four stars." (needs --allow-writes)

Related MCP server: Prompteka MCP Server

Install

You need Node.js 22.16 or newer. There is nothing else to install: every app below starts the server with npx, which downloads it from npm the first time.

npx engine-dj-mcp

That command is what the apps run; you do not need to run it yourself.

Claude Desktop

Settings → Developer → Edit Config, and add to claude_desktop_config.json:

{
  "mcpServers": {
    "engine-dj": { "command": "npx", "args": ["-y", "engine-dj-mcp"] }
  }
}

Restart Claude Desktop.

Claude Code

claude mcp add --scope user engine-dj -- npx -y engine-dj-mcp

Cursor

Add to Cursor — or open this deep link directly, or add the same mcpServers entry as for Claude Desktop to ~/.cursor/mcp.json:

cursor://anysphere.cursor-deeplink/mcp/install?name=engine-dj&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImVuZ2luZS1kai1tY3AiXX0%3D

VS Code

Add to VS Code — or run:

code --add-mcp '{"name":"engine-dj","command":"npx","args":["-y","engine-dj-mcp"]}'

Letting it write

To let the assistant create and edit playlists and edit track tags, add --allow-writes to args — a flag rather than an environment variable precisely so it is visible in the configuration you are reading:

{
  "mcpServers": {
    "engine-dj": { "command": "npx", "args": ["-y", "engine-dj-mcp@0.17.2", "--allow-writes"] }
  }
}

Pin the version in this one. Unpinned, npx fetches whatever is newest at every launch, and this configuration gives that code write access to your library. Pinned, a new release reaches it only when you change the number. Quit Engine DJ before asking for a change; see Writing.

Compatibility

  • macOS: yes. Every check against a real library has been run on macOS, and it finds libraries where Engine DJ keeps them there: ~/Music and the top of each drive under /Volumes.

  • Windows and Linux: not supported yet. The test suite passes on Ubuntu, but no real library has been read on either system. Off macOS the server looks only in ~/Music/Engine Library, so a library on a USB drive is not found, and there is no option to point it somewhere else.

  • Node.js 22.16 or newer, with no native dependencies. node:sqlite stopped needing a flag in 22.13, but the pre-write backup uses its backup(), added in 22.16. CI runs the full suite on Node 22.16 and 24, on macOS and Ubuntu.

  • Engine DJ libraries at schema 3.0.0 through 3.0.2 — Engine DJ 4.5 and 5.x. Everything here has been exercised against a real schema 3.0.2 library; 3.0.0 and 3.0.1 are accepted by the version check and covered by generated fixtures, but no real library at those versions has been read. Anything outside the range is listed with its version and refused, never read on a guess.

Tools

Nine read-only tools, and five that write — create_playlist, add_tracks_to_playlist, remove_tracks_from_playlist, reorder_playlist and update_track_metadata — that appear only when you start the server with --allow-writes. Every tool that reads library data also accepts an optional library argument — see Choosing a library.

search_tracks

The main one. Full-text search with diacritics folded, plus filters for tempo, key, rating, when a track was added and when it was last played.

Argument

What it does

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.

bpm

{ min, max } or { around, tolerance_pct }. Resolved tempo, so an analysed BPM wins over the tag.

key

{ camelot: [...] } for exact keys, { compatible_with: "8A" } for harmonic neighbours, { mode: "minor" } for a whole side of the wheel.

rating

{ min, max }, in stars, 0–5. Engine stores 0, 20, 40, 60, 80, 100; the filter converts, so { min: 4 } means four stars and up. The rating field hands back the stored number, and rating_stars the same thing in stars.

played

{ never: true }, or { before, after } taking an ISO date or a relative form like -6 months.

added

{ before, after }, same date forms.

flags

analyzed, available, has_cues, has_beatgrid. has_cues means a hot cue is genuinely set — see Limitations.

playlist

{ id } or { name } — search inside one playlist. Results still come back by relevance or id; get_playlist_tracks is what preserves playlist order.

fields

Which columns to return. Defaults to id, artist, title, bpm, camelot, rating.

limit, cursor

Page size (default 25, max 200) and an opaque cursor for the next page.

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".

get_tracks

Full metadata for specific track ids, returned in the order you asked for. Unknown ids are omitted rather than failing the call.

ids (required), fields, redact_paths.

get_playlists

Your playlist tree, in the order Engine DJ shows it — folders included.

The list is flat and in the order you would read down the sidebar with every folder expanded: depth and path carry the nesting, parent_id names the folder a list sits in.

Field

Meaning

name, id, path

path is the full Folder/Sub/Name, and is unique — a bare name need not be.

depth, parent_id

The nesting. parent_id is null at the top level.

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.

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.

track_count

Entries in that list alone, never rolled up from its children — the number Engine shows beside it.

missing_count

How many of those entries name a track this library does not have.

limit (default 200, max 1000). warnings appears if a playlist's link chain is damaged; nothing is ever dropped from the list because of one.

get_playlist_tracks

The tracks of one playlist, in playlist order.

Name it with playlist_id or with playlist_name — exactly one of the two. Playlist names are unique only within a folder, so a name matching more than one is refused with every candidate's id and full path rather than guessed at; pass the path from get_playlists to say which you meant.

Every row carries position, its 1-based place in the playlist. Same fields, limit and cursor conventions as search_tracks.

An entry whose track is not in this library keeps its slot and comes back as { position, entry_id, track_id, missing: true }, with missing_count alongside entry_count. That is ordinary rather than corruption — playlist entries outlive their tracks and travel between drives — and they are kept in place so the number of rows still matches the playlist's own length. On one reference library a 43-entry playlist holds exactly one track that library can actually play.

get_track_performance

Decodes the binary PerformanceData Engine stores per track: hot cues, the main cue, saved loops, the beatgrid, and a coarse waveform profile.

Every field carries its own decode status and its own layout marker. layout: "verified" means the byte layout was confirmed against a real library, so status: "ok" is a claim about the values. layout: "unverified" would mean only that the bytes parsed; no field returns it today.

Positions are sample offsets; cue and loop items also carry seconds. items: [] with slots: 8 means an analysed track with no cues set.

id (required).

audit_library

Eleven collection health checks. Returns a count and a small sample of ids per check, never the full result set — a library with thousands of unanalysed tracks should not fill an assistant's context.

Check

Finds

missing_files

Tracks whose file is gone from disk

unavailable

Tracks Engine has marked unavailable

unanalyzed

Tracks Engine has not analysed

no_cues

Tracks with no hot cue set

no_beatgrid

Tracks with no beatgrid data

missing_key

Tracks with no key detected

suspicious_bpm

Analysed and tagged tempo disagree, or tempo is outside 60–200

duplicates

Same artist and title, compared regardless of case in any script

empty_metadata

No artist or no title

orphan_entries

Playlist entries pointing at tracks not in this library — get_playlist_tracks shows where each one sits

path_form_mismatch

The file is on disk, but its name there — or a folder's on the way — is in a different Unicode form from the path Engine stored. macOS finds it anyway; Linux does not (measured on the kernel's exFAT driver), and Engine OS on a player is Linux, so these may fail to load on hardware. Differences in case alone are not counted: exFAT and Windows ignore case

checks — omit it to run all eleven.

run_sql

An escape hatch for questions the tools above do not cover. Read-only is enforced by the kernel, not by this tool. Results are bounded whatever the query says.

Prefer side.track_derived.camelot and side.track_derived.tempo in WHERE clauses over the camelot() and tempo() SQL functions — the functions run per row and defeat the indexes.

sql (required), params, limit.

list_libraries

Every library found, including ones whose schema is unsupported — listed with their version, so you can tell a broken server from a missing library. Re-scans on every call, so a drive plugged in after the server started shows up without a restart. A library that is temporarily unreadable — Engine DJ writing to it, say — stays listed with status: "unreadable" rather than vanishing.

No arguments.

refresh_index

Rebuilds the search index if the library changed. Normally unnecessary; the server checks staleness itself before answering.

create_playlist

The first of the five tools that write, none of which is registered at all unless the server was started with --allow-writes.

Creates one new top-level playlist from track ids — track_ids sets both what is in it and the order it is in, so a list built by search_tracks arrives in Engine DJ in the order the assistant chose. Nothing else changes: no playlist is renamed, reordered, emptied or deleted, and no track, cue or beatgrid is touched. The one existing row that moves is the previous last playlist's link, and Engine's own insert trigger is what moves it.

Argument

What it does

title

Name of the new playlist. Must not already be taken at the top level — Engine allows one name per folder.

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.

Each entry stores the track's origin identity — (originDatabaseUuid, originTrackId), the pair Engine matches on — not the local row id, so a playlist built here reads the same way Engine's own does.

The result carries playlist_id, tracks_added, library and backup_path. To undo it, delete the playlist in Engine DJ; backup_path is a whole-library snapshot for the case where something went wrong at a lower level, not an undo — see Restoring a snapshot.

Its own refusals: playlist_exists for a taken title, unknown_track for an id this library does not have, duplicate_track for the same id twice — plus the ones every write tool shares.

add_tracks_to_playlist

Adds one or more tracks to an existing playlist — this edits that playlist's contents, it does not create a new one (create_playlist does that). If the playlist's entry chain is already damaged, the write is refused outright rather than repaired, and nothing is added.

A playlist that is a folder (is_folder: true — it has child lists) is edited like any other: Engine has no separate folder type, a folder can hold entries of its own, and all three edit tools add to, remove from and reorder those entries without complaint. The lists inside it are untouched either way.

Argument

What it does

playlist_id / playlist_name

Exactly one of the two, resolved the same way get_playlist_tracks does: a name matching more than one playlist is refused with every candidate's id and full path listed, not guessed at.

track_ids

Ids from search_tracks or get_tracks, in the order they should appear. A track already in the playlist is refused as duplicate_track — Engine allows a track in a playlist only once.

at

Where the new tracks land, against the playlist's current 1-based positions (the same numbering get_playlist_tracks reports): "start", "end" (the default), or { after_position: n }.

The result carries playlist_id, tracks_added, positions — where the new tracks landed — undo, undo_complete (always true here), library and backup_path. undo is the exact remove_tracks_from_playlist call that reverses this edit: the positions the tracks landed at, plus expect_track_ids naming the tracks that landed there, so a playlist something else changed in the meantime is refused rather than having the wrong rows removed. Call it to undo rather than restoring backup_path — see Restoring a snapshot, and An undo covers one library for what it does not reach. Its own refusals: playlist_not_found, playlist_chain_damaged, invalid_position, and unknown_track / duplicate_track as for create_playlist — plus the ones every write tool shares.

remove_tracks_from_playlist

Removes one or more tracks from an existing playlist by position — this edits that playlist's contents; it never touches any other playlist. If the entry chain is already damaged, the write is refused outright rather than repaired.

Argument

What it does

playlist_id / playlist_name

Exactly one of the two, resolved the same way get_playlist_tracks does.

positions

1-based positions get_playlist_tracks reports for this playlist right now. Includes entries whose track is missing from the library (missing: true) — removing one is a legitimate way to clean up a hole, and the one removal undo cannot reverse (see below).

expect_track_ids

Optional, one entry per position: verifies each named position still holds the track expected before anything is removed, refusing the whole call otherwise. null means "this position should hold an entry whose track is missing", not "no expectation".

The result carries playlist_id, tracks_removed, removed — each position's track_id, null for a missing one — undo, undo_complete, library and backup_path. undo is a sequence of add_tracks_to_playlist calls, one per removed track that can be restored. Run them in the order given, never in parallel and never reversed — each step's target position is computed against the list as it stands after the previous step has already run, so firing them out of order puts tracks back in the wrong places. Preferred over restoring backup_path for the same reason as above.

undo_complete is false when the removal included an entry whose track is missing from the library: that entry named a track this library does not have, so no add_tracks_to_playlist call can put it back, and an undo_note names those positions. The steps that are returned still run and still restore everything else; the missing entries are recoverable only from backup_path, which reverts the whole library.

Its own refusals: playlist_not_found, playlist_chain_damaged, and invalid_position — for a repeated or out-of-range position, or one that does not hold what expect_track_ids expected — plus the ones every write tool shares.

playlist_chain_damaged always means the same thing for all three edit tools: the playlist's entry chain was already broken before the edit, which is why the edit refused to touch it. If instead the check each edit runs on its own work disagrees — the chain did not read back as it was written — the transaction is rolled back and that comes back as library_unreadable, with detail: "not_committed". Both leave the library exactly as it was; only the second one is this server saying it does not understand what the library just did.

reorder_playlist

Reorders an existing playlist's tracks — this changes the order of that playlist's existing entries; it adds nothing and removes nothing. If the entry chain is already damaged, the write is refused outright rather than repaired.

Argument

What it does

playlist_id / playlist_name

Exactly one of the two, resolved the same way get_playlist_tracks does.

order

A full permutation of 1..n, n being the playlist's current entry count. order[i] names the current 1-based position (from get_playlist_tracks) of the track that should end up at position i + 1. A partial "move x to y" instruction is not accepted — name every position, including ones that do not move.

The result carries playlist_id, undo, undo_complete (always true here), library and backup_path. undo is the exact inverse permutation, as a single reorder_playlist call. Its own refusals: playlist_not_found, playlist_chain_damaged, and invalid_position if order is not a full permutation of the playlist's current positions — plus the ones every write tool shares.

Reordering to the order a playlist is already in is accepted and rewrites no entry: it still stamps the playlist's lastEditTime, and still costs this session's snapshot if nothing had been written yet.

update_track_metadata

Changes genre, comment, label, year or rating on tracks — the values Engine DJ shows in its columns. It writes to Engine's database, not to the audio files' tags. Engine itself writes a comment into the file when you edit it there, but not a genre or a rating, so other software reading the tags will not see these edits either way.

Argument

updates

Up to 200 entries, each { id, genre?, comment?, label?, year?, rating_stars? }. Only the named fields change. "" clears a text field; year: 0 means unknown, as Engine stores it; rating_stars is 0–5.

library

Required when more than one library is connected — see Choosing a library.

A track that already holds the requested values is not written, so repeating a call changes nothing; it is counted in unchanged. The result carries updated, unchanged, changed — which fields changed on which tracks — undo, undo_complete (always true), library, and backup_path whenever the write transaction ran — which can include updated: 0, if the tracks had already changed to the requested values by the time the write lock was taken.

Undo. undo is one update_track_metadata call that restores the previous values of exactly the fields that changed, and names the library. It restores values, not lastEditTime: Engine's own trigger stamps every edit, the undo included. Each entry carries expect set to what this call wrote, so an undo replayed after someone edited the track again is refused as stale_value instead of overwriting that edit. rating_raw and expect exist for this; an ordinary edit needs neither.

Keep the undo from the first response. Repeating a call that already went through finds nothing to change and returns an empty undo. For work spread over several calls, replay the undos in reverse order.

Its own refusals: unknown_track; track_not_editable — a track whose origin is empty (Engine's trigger rewrites an empty origin on any update, which would detach it from playlist entries on other drives), or a field holding a value this tool could not put back, such as a rating outside 0–255; stale_value — the track changed after the values in expect were read (up to 20 mismatches come back in a structured mismatches field, with the total count in the prose message); and invalid_argument. On stale_value, tell the user which tracks and fields changed — do not rebuild expect from a fresh read to force the write without the user's consent, or it silently overwrites the edit the DJ made since. Plus the ones every write tool shares, except index_stale and the query errors: this tool addresses tracks by id and never touches the search index.

Searching right after an edit. Genre, comment and label are in the search index, which is rebuilt on the next read. While Engine DJ holds the library open it cannot be rebuilt, so a search can keep showing the old values, and refresh_index cannot help until Engine lets go. The edit itself is in the database.

Smart playlists. A smart playlist whose rules match on genre changes what it contains when a genre is renamed, though none of its own rows were touched.

Two connected libraries. Do not assume a tag edit propagates the way a playlist edit does (see An undo covers one library): measured once, a tag edit made on the USB library was not copied to the computer's library on a fresh Engine DJ launch. The other direction has not been measured for tags. Edited tracks are marked for sync (isMetadataOfPackedTrackChanged) the same way Engine DJ marks its own tag edits — measured 2026-09-15. That an explicit sync to a drive then carries the edit is what the flag appears to be for, but it has not been measured.

Refusals every write tool shares

These come from what happens before the write itself — choosing the library, bringing its index up to date, resolving the playlist — and from the write's own checks.

Code

Means

Nothing written?

invalid_argument

The arguments do not make sense — both playlist_id and playlist_name, an empty list where one is required, or a playlist_name that matches several playlists (every candidate is listed).

yes

library_not_found

library names nothing connected — the refusal lists what is — or the library's header could not be read.

yes

ambiguous_library

No library given, and more than one supported library is connected — or the uuid given is shared by copies on different drives. Lists them — see Choosing a library.

yes

unsupported_schema

The library's version is outside what this server supports.

yes

library_needs_recovery

Engine DJ left an unrecovered journal. Launch Engine once.

yes

library_busy

Something holds a conflicting lock right now. Retry.

yes

index_stale

The index could not be built yet, typically because Engine holds a lock on a first run. Carries retry_after_ms.

yes

query_timeout, query_process_crashed

The lookup that resolves a playlist failed. Edit tools only.

yes

library_unreadable

The library could not be read; the snapshot taken before the first write could not be made (a full disk, or a Node older than 22.16); or a write's own read-back disagreed with what it wrote, and it was rolled back.

see detail

detail on these errors. Once the write itself has started, detail is exactly one of two strings, and a client can read it to decide whether the library changed: not_committed — the library is what it was — or committed_unverified — the rare one: the write may have landed but could not be confirmed, and only this case hands back a backup_path. Refusals raised before that point — every row above marked "yes" — never opened the library for writing, whatever their detail says: it may be absent, not_committed, or explanatory text such as the candidates an ambiguous playlist_name lists.

Resources

  • engine://schema — the field semantics an assistant needs before writing SQL: how Engine encodes musical key, why tempo is COALESCE(bpmAnalyzed, bpm), that Track.path is relative, where playlist order really lives, and which helper columns are indexed.

  • engine://libraries — what was discovered at startup and whether each library's schema is supported. A snapshot; list_libraries is the live view.

Choosing a library

Engine DJ keeps a library on your computer and another on every drive you export to, so more than one is usually connected. list_libraries reports each with a uuid and a path, and every tool that reads library data takes an optional library argument. Pass either form exactly as printed — the ~/… path is accepted alongside the absolute one. A value matching neither comes back as library_not_found, listing what you can choose from.

Leave it out and the server uses the supported library holding the most tracks. That matters: the local library Engine DJ creates on install is scanned first and is often empty, so "the first one found" would hide the drive you actually work from.

That rule is enough for a read, which changes nothing: with two libraries connected a read picks one. Pass library when it matters which.

A write refuses instead, as soon as more than one supported library is connected — whatever their track counts. ambiguous_library lists every candidate with its track count, and detail: "not_committed".

The count was never the right question. An earlier version refused only an exact tie, reasoning that a USB drive and its copy tie precisely because one is a copy of the other — measured 2026-09-01, both real libraries at 257. But import one track on one side and the tie is gone, and the default quietly takes the larger. A playlist written to the wrong drive is at least visible there; a track's genre is not, and you are left believing the edit did not work.

With a single library nothing changes: you never have to name it.

Copies share a uuid. Copy an Engine Library folder onto another drive — a spare stick for a gig — and the copy keeps the original's uuid, so with both connected one uuid names two libraries. A write naming that uuid is refused the same way, with ambiguous_library listing both paths, rather than landing on whichever drive was scanned first. Pass the path instead — it tells the copies apart — and re-read from that path anything the write depends on, since a read naming the uuid may have come from the other copy. Reads naming a shared uuid are not refused: they answer from one of the copies. Before every write the drives are scanned again, so a copy plugged in after the server started is counted — as long as its library can be read.

The refusal tells the assistant to ask you rather than choose. Otherwise "pass library, here are the two" is an invitation to take the first one, which puts the write back on an arbitrary disk and makes the refusal pointless.

Each library gets its own index and its own connection, opened the first time you ask that library something. Comparing two libraries against each other — "what is on this drive but not that one?" — is not something this server does.

Safety

The guarantees this server makes about your library are listed in PRINCIPLES.md; this section is how they work.

Your library is opened read-only at the operating-system level, not by convention and not by a PRAGMA a query could turn back off. Writes are refused by SQLite itself, and without --allow-writes no file is ever created inside your Engine Library folder. The search index lives in ~/.engine-dj-mcp/.

Writing

Without --allow-writes the server has no tool that can write, and the paragraph above holds exactly as written: SQLite itself refuses.

With the flag, five tools appear. create_playlist adds a new playlist and nothing else. add_tracks_to_playlist, remove_tracks_from_playlist and reorder_playlist go further: with the flag, an existing playlist can now be changed, not only created — its tracks added to, removed from, or put in a different order. What these four playlist tools touch is the named playlist's own entries, plus exactly two rows elsewhere: that playlist's own row, whose lastEditTime every edit stamps so Engine sees the change, and — for create_playlist only — the previous last playlist's link, made by Engine's own insert trigger. No other playlist is renamed, emptied or deleted, and no track, cue or beatgrid is touched by these four. update_track_metadata changes genre, comment, label, year and rating on the tracks named — see its section above — and nothing else: no playlist, cue, beatgrid, title, artist, album, path or file is touched.

Every edit returns undo — the exact tool call that reverses it — and undo_complete; for the playlist tools, undo is expressed against the positions the edit itself produced, and undo_complete says whether replaying it puts the playlist back exactly as it was. Replaying undo is the right way back from an edit; restoring backup_path is not, because it reverts the whole library to before this session's first write, discarding every play count, import, cue and beatgrid change Engine DJ has recorded since, along with the one edit you actually wanted undone. See Restoring a snapshot.

There is exactly one edit undo cannot reverse, and it says so rather than pretending otherwise: removing an entry whose track is missing from the library (missing: true). Such an entry names a track this library does not have, so there is no track id to add back — the result comes back with undo_complete: false and an undo_note naming those positions, and the steps it does return still restore everything else.

Before the first write of a session the database is snapshotted to ~/.engine-dj-mcp/backups/, and every write of that session returns its path. Ten snapshots are kept per library — per library file, so a library and its clone on another drive do not share the ten.

One file is created inside your Engine Library folder while a write is in progress: SQLite's rollback journal, m.db-journal, next to m.db. It is removed when the transaction commits, and it is what makes the write all-or-nothing. If the process is killed mid-transaction the journal is left behind, and both this server and Engine DJ then treat the library as needing recovery — this server reports library_needs_recovery and refuses to touch the library, including for reads, until you have launched Engine DJ once so it can roll the journal back. Nothing else is ever written in that folder, and without --allow-writes not even this.

The write takes SQLite's own write lock for the length of one transaction and does not wait for it: if something else — Engine DJ mid-save, a player — is holding a conflicting lock at that moment, the write is refused with library_busy and nothing is changed.

Quit Engine DJ before writing. Having Engine open is not usually a lock conflict, so the write itself will normally go through — but what Engine then does with a change made underneath it has never been measured here. Every acceptance check of a write was run with Engine closed. What has been measured is that Engine does its own work on the library as it loads: it renumbers playlist entries, and it copies playlist changes to another connected library (see below). Quit, write, relaunch — Engine reads the library on startup and shows the change.

Quit, not close. On macOS, closing Engine's window leaves the application running: observed 2026-09-01 with the main process and seven OfflineAnalyzer workers — which write to the database — still alive afterwards. Use ⌘Q.

An undo covers one library

Every write result carries a library field — the uuid and path of the library the write actually landed in. Two libraries connected at once is the ordinary setup: a USB drive and its copy on the computer. This is where you check which of them a write went to.

undo reverses the edit in that one library, and only there. Each undo step names it — by path, in the step's own library argument — so replaying a step verbatim goes back to the library the edit was made in, not to whatever the default is at replay time. That matters because a USB drive and its copy hold the same playlist ids and the same track ids: a replay that resolved the default could land on the wrong disk, and its expect_track_ids would agree, both sides having been edited the same way.

Engine DJ moves playlist changes between connected libraries by itself, so a copy of your edit can still end up somewhere undo cannot reach.

Measured 2026-09-01. A track was added to a playlist in the library on the computer. Engine DJ was then launched with the USB drive attached, and the same playlist on the USB came back with the same track added — the copy carrying the very lastEditTime this server's INSERT had written. The undo was then run and reversed the edit on the computer. The USB kept it.

Nothing was damaged: both libraries stayed sound. But the two had diverged, and the undo reported success, correctly, because within its own library it did exactly what it promised.

So: if a second library is connected, look at the library field, and undo against each library separately. Undoing before Engine DJ next runs avoids the problem entirely.

Which library a change propagates to, and in which direction, is Engine's own business — this project does not model it and will not guess at it.

Restoring a snapshot

backup_path is not an undo. It is a copy of the whole m.db from before the session's first write, so putting it back reverts the entire library to that moment: every play count, import, cue, beatgrid and rating Engine DJ has written since is discarded along with the one edit you wanted gone. Reach for it only if the library itself is damaged — the case where a write comes back with detail: "committed_unverified".

Snapshots live in ~/.engine-dj-mcp/backups/, ten per library. Only a name ending in .db is a snapshot. A file ending in .partial-<number> — with or without -journal after it — is a copy still being written, or one whose process died before it finished: never restore one of those. A copy is renamed to its .db name only once it is complete, and an abandoned one is cleared the next time that library is snapshotted.

To undo a playlist you created, delete it in Engine DJ. Engine's own delete trigger repairs the playlist chain and cascades the entries away, which is exactly what removing it should do and is not something restoring a snapshot does better. For the playlist tools, to undo an edit to an existing playlist, replay the undo the edit returned instead — it names the precise add_tracks_to_playlist, remove_tracks_from_playlist or reorder_playlist call that puts the playlist back exactly as it was, without touching anything else Engine DJ has recorded since.

run_sql accepts arbitrary SQL, but only the first statement is ever executed, and VACUUM, ATTACH and DETACH are rejected outright, so a chained or exfiltrating statement cannot slip past the read-only connection.

If Engine DJ was closed uncleanly and left an unrecovered journal, this server will not open the library to "fix" it, with or without --allow-writes — rolling a journal forward is a repair on someone else's file, and every write tool refuses such a library outright rather than letting SQLite do it on the way in. It reports library_needs_recovery and asks you to launch Engine DJ once so it can recover its own library.

Limitations

Read this before deciding what to trust.

The PerformanceData layouts are reverse-engineered, so every decoded field says which kind it is. All four are marked layout: "verified" — derived from and checked against a real Engine DJ 3.0.x library of 281 analysed tracks, where cue offsets land inside the track, the beatgrid's implied tempo matches bpmAnalyzed on all 281, and the waveform's declared point spacing multiplies back out to the track's sample count on all 281.

Loops were the last to earn it. The slot grid was pinned down by 2248 sentinels, but no library available had a loop saved in it, so a populated slot stayed untested and loops carried layout: "unverified" through several releases. One deliberately saved loop settled it: its slot spans 1.678321678 s on a track Engine analysed at 143 BPM, which is four beats to within 4e-15 s. Only the right field order, unit and endianness land on a whole beat count.

A layout marker is a claim about the bytes, not about every name put on them. Four labels are inferred rather than measured, and the code says so where each is defined: which of a cue's four colour bytes is which channel (they are returned as stored, one 32-bit value, with no channel claim); that the second beatgrid is the one Engine calls "adjusted" (that it is the one Engine plays is measured — on seven tracks the other runs at exactly half the analysed tempo); that main_cue.is_adjusted is what its flag byte means; and that the waveform's three bytes per point are low, mid and high in that order. None affects a value you get back.

Everything else — titles, artists, tempo, key, ratings, play history, file paths — is read straight from the database and carries no such caveat.

has_cues and no_cues mean "a hot cue is set". Engine writes a quickCues blob to every analysed track whether or not a pad is used, so the cheap SQL test would answer a question about analysis instead: in the reference library all 281 blobs would count as having cues, while two tracks actually do. The blob is therefore decoded while the index is built. That costs roughly 100 ms extra at 50,000 tracks, and only when your library changes. The track's main cue does not count towards it — Engine sets that as a playback marker rather than the DJ placing it. has_beatgrid does still test for the blob: beatData has no "written but empty" state.

It writes playlists and five track fields, and only when you ask for it. Without --allow-writes the library is opened read-only at the OS level and there is no tool that could write. With the flag, five write tools appear: four create playlists and add, remove and reorder their tracks, and update_track_metadata changes genre, comment, label, year and rating in Engine's database — and that is the whole list. Not a cue, a loop or a beatgrid; not a title, artist, album, play count or file path; not the tags inside your audio files; and not even the recovery of a journal Engine DJ left behind.

It does not read play history. Track.timeLastPlayed answers "what have I not played in six months?", but the separate Engine history database — sessions, decks, what followed what — is not opened at all.

Smartlists are not reported. Engine's rule-based lists live in a separate Smartlist table with its own ordering and a JSON rule column, and nothing here reads it. get_playlists reports ordinary playlists and folders only, so a smartlist you can see in Engine will not appear.

An empty folder reads as an empty playlist. Engine's schema has no folder flag — a folder is simply a playlist that other playlists sit under — so is_folder means "has child lists". A folder you have emptied is indistinguishable from a playlist with no tracks.

A playlist's tracks can be edited; the playlist itself cannot. With --allow-writes a new playlist can be created, and an existing one can have tracks added, removed or reordered — but not renamed, deleted, moved between folders, or turned into a folder itself, and there are no set lists or suggested transitions. It answers questions about the collection and writes down the answer if you ask; the mixing is yours.

Schema 3.0.0 through 3.0.2 only. Older and newer libraries are listed with their version and reported as unsupported rather than read on a guess.

Licence

MIT — see LICENSE. What this project guarantees and refuses to do: PRINCIPLES.md.

Available Tools

9 tools
audit_libraryAudit the collectionA
Read-onlyIdempotent

Run collection health checks. Available: missing_files, unavailable, unanalyzed, no_cues, no_beatgrid, missing_key, suspicious_bpm, duplicates, empty_metadata, orphan_entries, path_form_mismatch. missing_files resolves each track against the selected library's own folder. path_form_mismatch finds files that are there, but under a name in a different Unicode normalization form from the stored path -- macOS opens them anyway, Linux does not (measured on its exFAT driver), and Engine OS on a player is Linux, so these may fail to load on hardware while missing_files on a Mac reports nothing. no_cues means "no hot cue is set" -- the quickCues blob is decoded for this, since Engine writes one to every analysed track whether or not a pad is used -- while no_beatgrid means the beatData blob is absent or empty. With more than one library connected, pass library (a uuid or path from list_libraries, either the ~/... form or the absolute one) to choose which one; the default is the supported library with the most tracks.

ParametersJSON Schema
NameRequiredDescriptionDefault
checksNo
libraryNoWhich library to use: either the uuid or the path reported by list_libraries (the reported ~/... form is accepted, as is the absolute path). Omit it to use the supported library holding the most tracks. A READ may always omit it. A WRITE may omit it only when a single supported library is connected: with two or more, a write refuses with ambiguous_library listing them, since the choice decides which disk changes; ask the user which, then pass it here. A library copied onto another drive keeps its uuid, so a uuid can name two connected libraries: a write naming such a uuid refuses with ambiguous_library as well. Pass the path to write to one of them.

TDQS

A3.6/5.0
Behavior1/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description adds rich behavioral detail, such as Unicode normalization semantics for path_form_mismatch and decoding of quickCues/beatData. However, the library parameter description explicitly discusses WRITE operations, disk changes, and refusing ambiguous libraries, while annotations declare readOnlyHint=true. This is a direct contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense and front-loaded with the main action and available checks. Some details could be structured as bullets for readability, but every sentence carries meaningful information and there is no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers check semantics and library disambiguation thoroughly, but it does not explain the return value/output format, and it is unclear what happens when checks is omitted. Without an output schema, these are meaningful gaps for an agent deciding how to invoke the tool and interpret results.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The checks parameter has no schema description, but the tool description lists valid check names and explains several in depth, compensating for the coverage gap. The library parameter receives additional guidance about uuid vs path and ambiguity, going beyond the schema description.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource, 'Run collection health checks', and enumerates the exact health checks supported. It is unambiguous and clearly distinct from sibling tools like get_tracks or search_tracks, even without naming them.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives concrete guidance for selecting a library: with more than one connected, pass library from list_libraries; otherwise the default is the supported library with the most tracks. It does not explicitly contrast against sibling tools, so full alternative-selection guidance is missing.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_playlistsList playlistsA
Read-onlyIdempotent

The library's playlist tree, in the order Engine DJ displays it -- taken from the Playlist.nextListId chain, which is where that order actually lives (the PlaylistPath view's position column runs the other way). Flat and in pre-order, so reading top to bottom is exactly the sidebar: depth and path carry the nesting, parent_id names the folder. is_folder means the list has child lists (Engine has no folder flag; a folder is a playlist other playlists sit under), so an emptied folder reads as an empty playlist. track_count is entries in that list alone, never rolled up from children, and missing_count is how many of them name a track that is not in this library. warnings appears when a link chain is broken or cyclic; nothing is ever dropped from the list because of one. With more than one library connected, pass library (a uuid or path from list_libraries, either the ~/... form or the absolute one) to choose which one; the default is the supported library with the most tracks.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNo
libraryNoWhich library to use: either the uuid or the path reported by list_libraries (the reported ~/... form is accepted, as is the absolute path). Omit it to use the supported library holding the most tracks. A READ may always omit it. A WRITE may omit it only when a single supported library is connected: with two or more, a write refuses with ambiguous_library listing them, since the choice decides which disk changes; ask the user which, then pass it here. A library copied onto another drive keeps its uuid, so a uuid can name two connected libraries: a write naming such a uuid refuses with ambiguous_library as well. Pass the path to write to one of them.

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations (readOnly, idempotent, non-destructive), the description discloses many behavioral details: the flat pre-order representation, field semantics (depth, path, parent_id, is_folder, track_count, missing_count), the meaning of warnings for broken/cyclic chains, and the guarantee that nothing is dropped. It also explains the multiple-library handling and default selection, adding substantial value.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but information-dense, with every sentence contributing new context. It is front-loaded with the core purpose and then elaborates on ordering, fields, and edge cases. While longer than typical, the complexity of the tool justifies the length, and there is no redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description thoroughly covers the output structure (fields like depth, path, parent_id, is_folder, track_count, missing_count), ordering, warnings, and library selection. It doesn't explicitly describe the JSON return format or pagination, but given the field names and the limit parameter, it is largely complete. The absence of an output schema is compensated by this detailed description.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool description adds significant meaning to the library parameter, explaining how to choose between libraries, the ambiguity behavior, and the default. For limit, it adds nothing beyond the schema (which lacks a description), but limit is self-explanatory. With schema coverage at 50%, the description compensates for the missing limit description and enriches the library parameter.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: listing the library's playlist tree in Engine DJ's display order. It specifies the resource (playlists) and the verb (list), and differentiates from siblings by describing the ordering and the source (Playlist.nextListId chain), which is distinct from other tools like get_playlist_tracks or search_tracks.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context on when to use the tool (to retrieve the playlist tree) and explains the library parameter's behavior, including when to omit it and the default selection. It also hints at an alternative view (PlaylistPath) but doesn't explicitly name sibling tools or state exclusions. The guidance is strong but not exhaustive.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_playlist_tracksGet the tracks in a playlistA
Read-onlyIdempotent

The tracks of one playlist, in playlist order -- from the PlaylistEntity.nextEntityId chain, not from row ids, so a track dragged up the list comes back where the DJ put it. Name the playlist with playlist_id, or with playlist_name (exactly one of the two). A name that matches several playlists is refused with every candidate's id and full path rather than picked between -- names are unique only within a folder, so pass the full path from get_playlists to disambiguate. Each row carries position, its 1-based place in the playlist. An entry whose track is not in this library comes back as { position, entry_id, track_id, missing: true } and keeps its slot, so entry_count still matches the playlist's own length; missing_count says how many of those there are. That is ordinary, not corruption -- entries outlive their tracks and arrive from other drives (see audit_library's orphan_entries). Same fields, limit and cursor conventions as search_tracks. With more than one library connected, pass library (a uuid or path from list_libraries, either the ~/... form or the absolute one) to choose which one; the default is the supported library with the most tracks.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNo
cursorNo
fieldsNo
libraryNoWhich library to use: either the uuid or the path reported by list_libraries (the reported ~/... form is accepted, as is the absolute path). Omit it to use the supported library holding the most tracks. A READ may always omit it. A WRITE may omit it only when a single supported library is connected: with two or more, a write refuses with ambiguous_library listing them, since the choice decides which disk changes; ask the user which, then pass it here. A library copied onto another drive keeps its uuid, so a uuid can name two connected libraries: a write naming such a uuid refuses with ambiguous_library as well. Pass the path to write to one of them.
playlist_idNo
redact_pathsNo
playlist_nameNo

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already mark this as read-only and idempotent, and the description adds substantial behavioral detail: missing tracks come back as { position, entry_id, track_id, missing: true }, keep their slot, and are counted by missing_count. It also explicitly declares the missing state ordinary rather than corruption, which is valuable context beyond the structured hints.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense and every clause earns its place, front-loading the core purpose before edge cases. It is one long paragraph rather than structured bullets, but the flow from selection to result shape to library rule is logical.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, yet the description covers the row shape, ordering semantics, missing-entry behavior, count invariants, and library disambiguation well enough for an agent to call this correctly. It even points to audit_library's orphan_entries for broader context about missing tracks.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With only 14% schema coverage, the description does the heavy lifting: it explains playlist_id vs playlist_name mutual exclusion, ambiguity behavior, path-based disambiguation, library selection, and the 1-based position meaning. The only clear gap is redact_paths, whose effect is never described, and fields/limit/cursor are delegated to search_tracks rather than fully explained.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence defines a precise operation: retrieve the tracks of a single playlist in playlist order. It further distinguishes the result from plain row-id ordering and ties it to the PlaylistEntity.nextEntityId chain, so there is no ambiguity about what the tool returns.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly states the two ways to name a playlist, requires exactly one, and explains how ambiguous names are refused. It also gives a library-selection rule with a default and references get_playlists and search_tracks as sources for disambiguation and conventions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_track_performanceGet cues, loops and beatgridA
Read-onlyIdempotent

Decode PerformanceData for one track: hot cues, the main cue, saved loops, the beatgrid and a coarse waveform profile. Each field carries its own decode status and its own layout marker. layout: "verified" (every field) means the binary layout was confirmed against a real Engine DJ library -- cue positions land inside the track, the beatgrid's implied tempo matches the analysed BPM, the waveform's declared point spacing multiplies back out to the track's sample count, and a saved loop spans a whole number of beats at that same analysed BPM -- so status: "ok" is a claim about the values, not just about the parse. layout: "unverified" would mean only that the bytes parsed; no field returns it today. Positions are sample offsets; sample_rate at the top level converts them to seconds, and cue/loop items carry the seconds already. Only hot-cue and loop slots that hold something are listed -- slots is how many the track has in total, so items: [] with slots: 8 means an analysed track with no cues set. Items are capped at 64; total gives the full count and truncated says whether the cap was hit. With more than one library connected, pass library (a uuid or path from list_libraries, either the ~/... form or the absolute one) to choose which one; the default is the supported library with the most tracks.

ParametersJSON Schema
NameRequiredDescriptionDefault
idYes
libraryNoWhich library to use: either the uuid or the path reported by list_libraries (the reported ~/... form is accepted, as is the absolute path). Omit it to use the supported library holding the most tracks. A READ may always omit it. A WRITE may omit it only when a single supported library is connected: with two or more, a write refuses with ambiguous_library listing them, since the choice decides which disk changes; ask the user which, then pass it here. A library copied onto another drive keeps its uuid, so a uuid can name two connected libraries: a write naming such a uuid refuses with ambiguous_library as well. Pass the path to write to one of them.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the safety profile is covered. The description adds substantial behavioral context beyond that: the layout verification semantics ('verified' vs 'unverified'), the meaning of status:'ok', the cap at 64 items with total/truncated fields, the slots vs items distinction, and the library ambiguity behavior for writes. This is rich, non-obvious behavior that an agent needs to interpret results correctly.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but every sentence carries substantive information. It is front-loaded with the core purpose, then moves through layout verification, position semantics, item capping, and library selection. It could arguably be split into clearer sections, but the density is justified given the complexity of the tool's output semantics. No filler or repetition of schema content.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a read-only tool with no output schema, the description is remarkably complete. It explains the return semantics (per-field decode status, layout markers, slots vs items, cap behavior), the coordinate system (sample offsets, seconds), and the library disambiguation rules. An agent has everything needed to call this tool correctly and interpret its results. The only minor gap is that `id` isn't explicitly defined as a track ID, but the tool name and title make that unambiguous.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 50%: the `library` parameter has a detailed schema description, but `id` has only type/constraints. The tool description compensates by explaining that positions are sample offsets, that sample_rate converts them to seconds, and that cue/loop items carry seconds already. It doesn't explain the `id` parameter's meaning (which track), but the tool name and title make that obvious. The description adds meaningful context about how the id relates to the decoded data.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource ('Decode PerformanceData for one track') and enumerates exactly what is decoded: hot cues, main cue, saved loops, beatgrid, and a coarse waveform profile. It clearly distinguishes itself from sibling tools like get_tracks or get_playlist_tracks by focusing on PerformanceData rather than track metadata or playlist membership.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit when-to-use guidance: it explains the library selection behavior, when to pass `library` (with more than one library connected), and what the default is. It also explains the cap behavior and the slots/items distinction, which helps an agent decide whether this tool is appropriate for a given query. The sibling list confirms this is the only tool for PerformanceData, and the description's detail about library disambiguation is actionable.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_tracksGet tracks by idA
Read-onlyIdempotent

Fetch full metadata for specific track ids, in the order requested. With more than one library connected, pass library (a uuid or path from list_libraries, either the ~/... form or the absolute one) to choose which one; the default is the supported library with the most tracks.

ParametersJSON Schema
NameRequiredDescriptionDefault
idsYes
fieldsNo
libraryNoWhich library to use: either the uuid or the path reported by list_libraries (the reported ~/... form is accepted, as is the absolute path). Omit it to use the supported library holding the most tracks. A READ may always omit it. A WRITE may omit it only when a single supported library is connected: with two or more, a write refuses with ambiguous_library listing them, since the choice decides which disk changes; ask the user which, then pass it here. A library copied onto another drive keeps its uuid, so a uuid can name two connected libraries: a write naming such a uuid refuses with ambiguous_library as well. Pass the path to write to one of them.
redact_pathsNo

TDQS

A3.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false. The description adds meaningful behavioral context beyond that: it explains the default library selection rule and the library parameter's ambiguity handling, which is not in the annotations. It does not disclose the redact_paths default behavior, but the schema carries that.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences with no filler. The primary purpose is front-loaded in the first sentence, and the library nuance is confined to the second. Every word earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers the tool's core purpose and the complex library parameter well, but it leaves the 'fields' and 'redact_paths' behavior unexplained, and the return format ('full metadata') is vague. Given the annotations and schema provide partial context, the description is adequate but has notable gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is only 25% (only 'library' has a schema description). The tool description essentially repeats the library guidance already in the schema and adds nothing about 'ids', 'fields', or 'redact_paths'. It does not compensate for the low schema coverage on the other three parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('Fetch') with a clear resource ('full metadata for specific track ids') and adds a scoping detail ('in the order requested'). This distinguishes it from siblings like get_playlist_tracks, which fetches by playlist, and search_tracks, which searches.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage by specifying 'specific track ids', and it gives library-selection guidance, but it does not explicitly contrast this tool with alternatives or state when not to use it. The context is clear for library selection but absent for tool selection.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_librariesList Engine DJ librariesA
Read-onlyIdempotent

List every discovered library, including ones whose schema is unsupported. Re-scans on every call, so a drive plugged in after this server started is visible without a restart (the engine://libraries resource is a start-time snapshot). A library seen before but not readable right now (e.g. Engine DJ is writing to it) stays listed with status: "unreadable" and error set, instead of disappearing. Pass a listed uuid or path as the library argument of any other tool to act on that library; without it they use the supported library holding the most tracks.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the safety profile is covered. The description adds valuable behavioral context beyond annotations: it re-scans on every call (not cached), explains that engine://libraries is a start-time snapshot, and discloses that unreadable libraries remain listed with status 'unreadable' and an error set. This is exactly the kind of behavioral disclosure that helps an agent predict side effects and state.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three sentences, each earning its place: scope, re-scan behavior, and unreadable-library behavior. The most important fact (lists every discovered library) is front-loaded, and the follow-up guidance about passing uuid/path to other tools is a natural, useful extension. No filler or repetition of schema/annotation content.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter, read-only, idempotent list tool, the description is complete. It covers what is listed, the freshness semantics, the failure behavior, and how the results should be used with sibling tools. There is no output schema, but the description's mention of status and error fields gives the agent enough expectation of the return shape. Nothing an agent needs to call this correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, so the schema provides no parameter semantics. The description compensates by explaining how the output (listed uuids/paths) feeds into other tools' `library` argument, which is the only parameter-related context an agent needs. Baseline 4 for zero-param tools is appropriate; no deduction needed since there are no parameters to document.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb ('List') and resource ('every discovered library'), and explicitly distinguishes itself from a start-time snapshot resource (engine://libraries). It also clarifies that unsupported-schema libraries are included, which differentiates it from sibling tools that operate on supported libraries.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly explains when to use this tool: to see libraries discovered since server start, including unsupported or currently unreadable ones. It also provides routing guidance for other tools, telling the agent to pass a listed uuid or path as the `library` argument, and notes the fallback behavior when no library is passed. This is clear usage context with no ambiguity.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

refresh_indexRefresh the search indexA
Read-onlyIdempotent

Rebuild the search index if the library has changed. With more than one library connected, pass library (a uuid or path from list_libraries, either the ~/... form or the absolute one) to choose which one; the default is the supported library with the most tracks.

ParametersJSON Schema
NameRequiredDescriptionDefault
libraryNoWhich library to use: either the uuid or the path reported by list_libraries (the reported ~/... form is accepted, as is the absolute path). Omit it to use the supported library holding the most tracks. A READ may always omit it. A WRITE may omit it only when a single supported library is connected: with two or more, a write refuses with ambiguous_library listing them, since the choice decides which disk changes; ask the user which, then pass it here. A library copied onto another drive keeps its uuid, so a uuid can name two connected libraries: a write naming such a uuid refuses with ambiguous_library as well. Pass the path to write to one of them.

TDQS

A3.5/5.0
Behavior1/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotation Contradiction: annotations declare readOnlyHint=true, yet the description says 'Rebuild' and the schema explicitly describes a WRITE path ('a write refuses... since the choice decides which disk changes'). This is a serious inconsistency, so per rubric the score is 1.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two concise sentences with the action and condition front-loaded. The parenthetical about library forms is lengthy but provides necessary disambiguation, and there is no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The single parameter is fully documented and the typical trigger is stated, but there is no output schema and no description of result or status behavior. More importantly, the read/write contradiction between annotations and description leaves the operation model incomplete and confusing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, and the schema itself is far more detailed, including READ/WRITE behavior, ambiguous_library errors, and uuid reuse across drives. The description only adds the default-library rule, which the schema already covers, so it provides little added semantic value.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: 'Rebuild the search index,' with a clear trigger condition ('if the library has changed'). This is unambiguous against sibling tools like list_libraries, search_tracks, or audit_library.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives clear usage context: when to use it, when to pass `library`, and what the default does ('supported library with the most tracks'). It does not explicitly name exclusions or alternatives, but the conditional trigger provides enough guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

run_sqlRun a read-only SQL queryA
Read-onlyIdempotent

Escape hatch for questions the other tools do not cover. Read-only is enforced by the kernel, not by this check alone. Use side.track_derived.camelot and side.track_derived.tempo in WHERE clauses rather than the camelot()/tempo() SQL functions, which run per row and defeat indexes. With more than one library connected, pass library (a uuid or path from list_libraries, either the ~/... form or the absolute one) to choose which one; the default is the supported library with the most tracks.

ParametersJSON Schema
NameRequiredDescriptionDefault
sqlYes
limitNo
paramsNo
libraryNoWhich library to use: either the uuid or the path reported by list_libraries (the reported ~/... form is accepted, as is the absolute path). Omit it to use the supported library holding the most tracks. A READ may always omit it. A WRITE may omit it only when a single supported library is connected: with two or more, a write refuses with ambiguous_library listing them, since the choice decides which disk changes; ask the user which, then pass it here. A library copied onto another drive keeps its uuid, so a uuid can name two connected libraries: a write naming such a uuid refuses with ambiguous_library as well. Pass the path to write to one of them.

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, idempotentHint, and non-destructive behavior, so the safety profile is covered. The description adds valuable behavior beyond annotations: read-only is enforced by the kernel, not just by the tool's own check, and the SQL functions defeat indexes. It does not detail output shape or error behavior, but the annotations lower the burden here.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but front-loaded: purpose comes first, then the read-only enforcement caveat, then performance guidance, then library disambiguation. Every sentence earns its place. The library-selection sentence is long, but the complexity justifies it. No filler or redundant restating of annotations.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a powerful SQL escape hatch, the description covers the most decision-relevant facts: scope, safety enforcement, performance pitfalls, and library disambiguation. There is no output schema, but the return value of a SQL query is reasonably implicit. It does not mention result limiting or parameter binding syntax, though those are partly visible in the schema.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is only 25% (only `library` has a rich description). The tool description compensates for `sql` by explaining its role and advising on WHERE-clause table usage, and it clarifies `library` selection and defaults. However, `limit` and `params` are left to the schema's minimal type/default info, so the description does not fully compensate for the low coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific action and resource: 'Run a read-only SQL query' and positions itself as an 'escape hatch for questions the other tools do not cover.' This clearly distinguishes it from the sibling query tools and leaves no doubt about what the tool does.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly says when to use the tool (when other tools do not cover the question) and gives concrete routing guidance for performance ('Use side.track_derived... rather than camelot()/tempo()') and library selection when more than one library is connected. This is actionable, not merely contextual.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_tracksSearch tracksA
Read-onlyIdempotent

Search the Engine DJ library by text, tempo, key, rating, play history and analysis flags. Set include_total for a count alongside the page: it is capped at 1000, and a capped result comes back as total: 1000 with total_capped: true -- treat that as 'at least 1000', never as an exact count. flags.has_cues means a hot cue is actually set (the blob is decoded when the index is built), not merely that Engine analysed the track; flags.has_beatgrid means a beatData blob is present. playlist: {id} or {name} narrows the search to one playlist -- results still come back by relevance or id, not in playlist order; use get_playlist_tracks for that. With more than one library connected, pass library (a uuid or path from list_libraries, either the ~/... form or the absolute one) to choose which one; the default is the supported library with the most tracks.

ParametersJSON Schema
NameRequiredDescriptionDefault
qNo
bpmNo
keyNo
addedNo
flagsNo
limitNo
cursorNo
fieldsNo
playedNo
ratingNo
libraryNoWhich library to use: either the uuid or the path reported by list_libraries (the reported ~/... form is accepted, as is the absolute path). Omit it to use the supported library holding the most tracks. A READ may always omit it. A WRITE may omit it only when a single supported library is connected: with two or more, a write refuses with ambiguous_library listing them, since the choice decides which disk changes; ask the user which, then pass it here. A library copied onto another drive keeps its uuid, so a uuid can name two connected libraries: a write naming such a uuid refuses with ambiguous_library as well. Pass the path to write to one of them.
playlistNo
redact_pathsNo
include_totalNo

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the safety profile is covered. The description adds substantial behavioral context beyond that: the total cap semantics (total_capped: true), the meaning of flags.has_cues and flags.has_beatgrid (decoded blob vs. merely analyzed), and the playlist ordering caveat. It also discloses the ambiguous_library failure mode for writes, which is relevant even though this tool is read-only. No contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but every sentence earns its place: it front-loads the core purpose, then covers the count cap, flag semantics, playlist ordering, and library selection. It is longer than a typical description, but the length is justified by the number of subtle behaviors it must disclose. A slight deduction because the library paragraph is long and could be tightened, but it is not padded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a read-only search tool with 14 parameters, no output schema, and several non-obvious behaviors, the description covers the critical gaps: pagination via cursor is implied by the schema, the total cap is explained, flag semantics are clarified, playlist ordering is routed to a sibling, and multi-library selection is fully specified. The only thing not described is the exact shape of returned track objects, but with no output schema and a fields parameter, the agent can infer that from the schema and the tool's purpose. This is complete enough for correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is only 7%, so the description must compensate, and it does for the most subtle parameters: include_total (cap and total_capped semantics), flags.has_cues and flags.has_beatgrid (blob-decoding nuance), playlist (ordering caveat and id/name forms), and library (uuid/path forms, default selection, ambiguous_library behavior). It does not walk through every filter object (bpm, key, rating, played, added), but those are fairly self-explanatory from their property names, and the description's targeted explanations cover the genuinely ambiguous ones.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource ('Search the Engine DJ library') and enumerates the searchable dimensions (text, tempo, key, rating, play history, analysis flags), which clearly distinguishes it from siblings like get_tracks, get_playlist_tracks, and run_sql. It also names the sibling get_playlist_tracks explicitly when discussing playlist ordering, so an agent can tell this tool apart without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit when-to-use guidance: use get_playlist_tracks when playlist order is needed, pass library when multiple libraries are connected, and treat include_total's capped result as 'at least 1000'. It also explains the default library selection behavior. This is strong routing and context that goes well beyond the schema.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 9 tool updatesv0.17.2
    • First observedaudit_library
    • First observedget_playlist_tracks
    • First observedget_playlists
    • First observedget_track_performance
    • First observedget_tracks
    • First observedlist_libraries
    • First observedrefresh_index
    • First observedrun_sql
    • First observedsearch_tracks

TDQS

A4.3/5.0

Scored across 9 tools

Disambiguation5/5

Each tool targets a distinct resource and action: index refresh, playlist tracks, search, track metadata, playlist tree, performance data, health audit, SQL escape hatch, and library listing. No two tools overlap in purpose, and descriptions make boundaries clear.

Naming Consistency5/5

All tools follow a consistent snake_case verb_noun pattern: refresh_index, get_playlist_tracks, search_tracks, get_tracks, get_playlists, get_track_performance, audit_library, run_sql, list_libraries. Verbs are clear and uniform in style.

Tool Count5/5

With 9 tools, the server is well-scoped for a DJ library management domain. It covers discovery, search, retrieval, playlist inspection, performance analysis, health auditing, index maintenance, and a SQL fallback—each tool earns its place without bloat.

Completeness5/5

The tool surface is complete for a read/analysis-oriented server: library listing, search, track and playlist retrieval, performance data decoding, health audits, index refresh, and an escape hatch (run_sql) for uncovered questions. No obvious operations are missing given the server's stated purpose.

Maintenance

ActivityNo data
ResponsivenessUnresponsive

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    F
    maintenance
    Enables AI assistants to control Apple Music on macOS using AppleScript for playback, library management, and playlist operations. It supports features like searching tracks, managing the play queue, and retrieving detailed metadata through a structured interface.
    13 npm
    7
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables AI assistants to directly access and manage your local Prompteka prompt library on macOS. It allows assistants to read, search, create, and organize prompts and folders by connecting directly to the app's SQLite database.
    2 npm
    1
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables MCP-aware AI clients to read, query, and modify a Lexicon DJ library through structured tool calls, including searching tracks, managing tags, and curating playlists or smartlists.
    13
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Connects AI assistants to a local music library for full-text search and filtering of tracks, albums, artists, genres, composers, and lyrics, plus listening analytics and metadata hygiene audits. Also lets users generate and populate playlists and control playback (play, pause, seek, volume) in a running player instance.
    -