engine-dj-mcp 0.11.0 → 0.11.2
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 +6 -4
- package/dist/playlists.d.ts +6 -4
- package/dist/playlists.js +6 -4
- package/dist/server.d.ts +10 -0
- package/dist/server.js +1 -1
- package/dist/store/backup.js +11 -1
- package/dist/store/write.js +33 -33
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/engine-dj-mcp)
|
|
4
4
|
[](./LICENSE)
|
|
5
5
|
|
|
6
|
-
An MCP server that
|
|
7
|
-
|
|
6
|
+
An MCP server that gives an AI assistant your **Engine DJ** libraries — the
|
|
7
|
+
one on your computer and the ones on your USB drives. It searches and audits
|
|
8
|
+
them, reads the cues and beatgrids Engine stored, and builds playlists when
|
|
9
|
+
you ask it to.
|
|
8
10
|
|
|
9
11
|
> **Not affiliated with, endorsed by, or sponsored by inMusic Brands, Denon
|
|
10
12
|
> DJ, or the Engine DJ product.** "Engine DJ" is used here only to name the
|
|
11
|
-
> software whose library this tool reads. No logos or brand
|
|
12
|
-
> inMusic or Denon DJ are used in this project.
|
|
13
|
+
> software whose library this tool reads and writes. No logos or brand
|
|
14
|
+
> artwork from inMusic or Denon DJ are used in this project.
|
|
13
15
|
|
|
14
16
|
Your library is opened **read-only at the operating-system level**. It is
|
|
15
17
|
never written to unless you start the server with `--allow-writes` — see
|
package/dist/playlists.d.ts
CHANGED
|
@@ -22,10 +22,12 @@ import type { QueryProcess } from "./proc/query-client.js";
|
|
|
22
22
|
* never read by this project.
|
|
23
23
|
*
|
|
24
24
|
* Everything here therefore walks the chain — defensively. These are linked
|
|
25
|
-
* lists inside a file this server does not own and
|
|
26
|
-
* half-completed Engine write, a sync conflict or a
|
|
27
|
-
* backup can leave a cycle, a link to a row that is gone,
|
|
28
|
-
* disconnected runs.
|
|
25
|
+
* lists inside a file this server does not own, and which nothing in this
|
|
26
|
+
* module writes: a half-completed Engine write, a sync conflict or a
|
|
27
|
+
* partially restored backup can leave a cycle, a link to a row that is gone,
|
|
28
|
+
* or two disconnected runs. (Since 0.11.0 the server can append a playlist
|
|
29
|
+
* under --allow-writes — see src/store/write.ts — but only ever through
|
|
30
|
+
* Engine's own chain triggers, and never from here.) None of those may hang the walk, and none may come
|
|
29
31
|
* back as a silently short list that reads like a complete one.
|
|
30
32
|
*/
|
|
31
33
|
/**
|
package/dist/playlists.js
CHANGED
|
@@ -22,10 +22,12 @@ import { err, isEngineError } from "./errors.js";
|
|
|
22
22
|
* never read by this project.
|
|
23
23
|
*
|
|
24
24
|
* Everything here therefore walks the chain — defensively. These are linked
|
|
25
|
-
* lists inside a file this server does not own and
|
|
26
|
-
* half-completed Engine write, a sync conflict or a
|
|
27
|
-
* backup can leave a cycle, a link to a row that is gone,
|
|
28
|
-
* disconnected runs.
|
|
25
|
+
* lists inside a file this server does not own, and which nothing in this
|
|
26
|
+
* module writes: a half-completed Engine write, a sync conflict or a
|
|
27
|
+
* partially restored backup can leave a cycle, a link to a row that is gone,
|
|
28
|
+
* or two disconnected runs. (Since 0.11.0 the server can append a playlist
|
|
29
|
+
* under --allow-writes — see src/store/write.ts — but only ever through
|
|
30
|
+
* Engine's own chain triggers, and never from here.) None of those may hang the walk, and none may come
|
|
29
31
|
* back as a silently short list that reads like a complete one.
|
|
30
32
|
*/
|
|
31
33
|
/**
|
package/dist/server.d.ts
CHANGED
|
@@ -32,4 +32,14 @@ export declare function createServer(opts?: {
|
|
|
32
32
|
roots?: string[];
|
|
33
33
|
sidecarBaseDir?: string;
|
|
34
34
|
allowWrites?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Where pre-write snapshots go. Defaults to ~/.engine-dj-mcp/backups.
|
|
37
|
+
*
|
|
38
|
+
* An option rather than a constant because a test that writes through
|
|
39
|
+
* this server would otherwise deposit a full copy of its throwaway
|
|
40
|
+
* fixture in the real home directory -- and under a fresh tag each run,
|
|
41
|
+
* since every fixture gets a new temp path, so rotation could never
|
|
42
|
+
* reclaim them and they accumulated without bound.
|
|
43
|
+
*/
|
|
44
|
+
backupBaseDir?: string;
|
|
35
45
|
}): Promise<EngineDjMcpServer>;
|
package/dist/server.js
CHANGED
|
@@ -444,7 +444,7 @@ export async function createServer(opts = {}) {
|
|
|
444
444
|
const state = await acquire(args.library);
|
|
445
445
|
if (isEngineError(state))
|
|
446
446
|
return reply(state);
|
|
447
|
-
return reply(await runCreatePlaylist(state.lib.path, state.lib.uuid, args, join(homedir(), ".engine-dj-mcp", "backups")));
|
|
447
|
+
return reply(await runCreatePlaylist(state.lib.path, state.lib.uuid, args, opts.backupBaseDir ?? join(homedir(), ".engine-dj-mcp", "backups")));
|
|
448
448
|
});
|
|
449
449
|
}
|
|
450
450
|
/**
|
package/dist/store/backup.js
CHANGED
|
@@ -50,8 +50,18 @@ export async function snapshotLibrary(mdbPath, uuid, baseDir) {
|
|
|
50
50
|
await backup(src, dest);
|
|
51
51
|
src.close();
|
|
52
52
|
src = undefined;
|
|
53
|
+
// Snapshots this library owns: the tagged shape above, plus the untagged
|
|
54
|
+
// `${uuid}-${stamp}.db` an earlier version wrote. Without the second,
|
|
55
|
+
// those sat outside every namespace and were never reclaimed -- up to
|
|
56
|
+
// KEEP full copies of a library, kept forever, on any upgrading user.
|
|
57
|
+
// They predate the tag and therefore predate everything written since,
|
|
58
|
+
// which is why folding them into one window evicts them first. Two
|
|
59
|
+
// libraries sharing a uuid is precisely why the tag exists, and an
|
|
60
|
+
// untagged file cannot say which of them it came from -- ageing them out
|
|
61
|
+
// under whichever library writes next is the only thing left to do.
|
|
62
|
+
const legacy = new RegExp(`^${uuid}-\\d{4}-`);
|
|
53
63
|
const mine = readdirSync(baseDir)
|
|
54
|
-
.filter((f) => f.
|
|
64
|
+
.filter((f) => f.endsWith(".db") && (f.startsWith(prefix) || legacy.test(f)))
|
|
55
65
|
.sort();
|
|
56
66
|
for (const old of mine.slice(0, Math.max(0, mine.length - KEEP))) {
|
|
57
67
|
rmSync(join(baseDir, old), { force: true });
|
package/dist/store/write.js
CHANGED
|
@@ -131,6 +131,24 @@ export function walkFrom(db, listId, headId) {
|
|
|
131
131
|
}
|
|
132
132
|
return out;
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Roll back, swallowing a failure of the rollback itself.
|
|
136
|
+
*
|
|
137
|
+
* Every caller is already returning a specific error -- the chain did not read
|
|
138
|
+
* back, the library is busy -- and a ROLLBACK that throws on the way out would
|
|
139
|
+
* replace that reason with its own, telling the user about a failed rollback
|
|
140
|
+
* instead of what actually went wrong. Nothing is lost by ignoring it:
|
|
141
|
+
* db.close() in the finally block ends any transaction still open, and SQLite
|
|
142
|
+
* discards an uncommitted one on close.
|
|
143
|
+
*/
|
|
144
|
+
function rollback(db) {
|
|
145
|
+
try {
|
|
146
|
+
db.exec("ROLLBACK");
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
/* no transaction in progress, or the connection is already gone */
|
|
150
|
+
}
|
|
151
|
+
}
|
|
134
152
|
export function sameOrder(a, b) {
|
|
135
153
|
return a.length === b.length && a.every((x, i) => x.uuid === b[i].uuid && x.trackId === b[i].trackId);
|
|
136
154
|
}
|
|
@@ -303,32 +321,20 @@ export async function createPlaylist(mdbPath, uuid, input, opts) {
|
|
|
303
321
|
}
|
|
304
322
|
for (let i = 0; i + 1 < ids.length; i++)
|
|
305
323
|
link.run(ids[i + 1], ids[i]);
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
// entries naming a third library's (databaseUuid, trackId), see
|
|
319
|
-
// src/playlists.ts:52-58 -- is not a foreign key at all and no pragma
|
|
320
|
-
// scoping ever protected against it.
|
|
321
|
-
const orphan = db
|
|
322
|
-
.prepare("SELECT 1 FROM PlaylistEntity WHERE listId = ? AND listId NOT IN (SELECT id FROM Playlist)")
|
|
323
|
-
.get(listId);
|
|
324
|
-
if (orphan) {
|
|
325
|
-
db.exec("ROLLBACK");
|
|
326
|
-
return err("library_unreadable", `Writing "${title}" would have broken a foreign key; nothing was changed.`, {
|
|
327
|
-
detail: NOT_COMMITTED,
|
|
328
|
-
});
|
|
329
|
-
}
|
|
324
|
+
// No foreign-key gate here, though spec §6.3 asks for one. PlaylistEntity
|
|
325
|
+
// carries exactly one foreign key -- listId -> Playlist(id) -- it is not
|
|
326
|
+
// DEFERRABLE, and this connection sets PRAGMA foreign_keys = ON, so a bad
|
|
327
|
+
// listId is refused by SQLite at the INSERT above and never reaches a
|
|
328
|
+
// check. The listId asked about would in any case be the one this
|
|
329
|
+
// transaction just inserted, which exists by construction. A gate whose
|
|
330
|
+
// condition cannot become true is not a safety net; it reads as one,
|
|
331
|
+
// which is worse than its absence. (`PRAGMA foreign_key_check(...)` is
|
|
332
|
+
// not the alternative: it reports every orphan in the table, so a
|
|
333
|
+
// PlaylistEntity row left behind by some earlier deleted playlist would
|
|
334
|
+
// fail every create_playlist call on that library forever, blaming this
|
|
335
|
+
// write for damage that predates it.)
|
|
330
336
|
if (ids.length > 0 && !sameOrder(walkFrom(db, listId, ids[0]), refs)) {
|
|
331
|
-
db
|
|
337
|
+
rollback(db);
|
|
332
338
|
return err("library_unreadable", `The entry chain for "${title}" did not read back as written; nothing was changed.`, { detail: NOT_COMMITTED });
|
|
333
339
|
}
|
|
334
340
|
// No check that exactly one Playlist row has nextListId = 0: the schema's
|
|
@@ -365,14 +371,8 @@ export async function createPlaylist(mdbPath, uuid, input, opts) {
|
|
|
365
371
|
// so it is a plain retry, not an unverified write.
|
|
366
372
|
const busyOnCommit = commit === "maybe" && /SQLITE_BUSY|database is locked/i.test(e?.message ?? "");
|
|
367
373
|
if (commit === "not yet" || busyOnCommit) {
|
|
368
|
-
if (open && db)
|
|
369
|
-
|
|
370
|
-
db.exec("ROLLBACK");
|
|
371
|
-
}
|
|
372
|
-
catch {
|
|
373
|
-
/* no transaction in progress */
|
|
374
|
-
}
|
|
375
|
-
}
|
|
374
|
+
if (open && db)
|
|
375
|
+
rollback(db);
|
|
376
376
|
return mapWriteError(e, title, mdbPath);
|
|
377
377
|
}
|
|
378
378
|
// Past the point of no return. No ROLLBACK: after a successful COMMIT
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engine-dj-mcp",
|
|
3
|
-
"version": "0.11.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.11.2",
|
|
4
|
+
"description": "MCP server for an Engine DJ library: search and audit it, read cues and beatgrids, and build playlists when you ask. Not affiliated with inMusic or Denon DJ.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
7
7
|
"model-context-protocol",
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"sqlite",
|
|
12
12
|
"library-audit",
|
|
13
13
|
"claude",
|
|
14
|
-
"ai"
|
|
14
|
+
"ai",
|
|
15
|
+
"playlist",
|
|
16
|
+
"harmonic-mixing"
|
|
15
17
|
],
|
|
16
18
|
"license": "MIT",
|
|
17
19
|
"author": "Mikhail Chereshnev <venuttv@gmail.com>",
|