engine-dj-mcp 0.11.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -28
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +6 -0
- package/dist/playlists.d.ts +19 -5
- package/dist/playlists.js +32 -14
- package/dist/server.js +115 -2
- package/dist/store/backup.d.ts +24 -0
- package/dist/store/backup.js +54 -15
- package/dist/store/write.d.ts +169 -0
- package/dist/store/write.js +930 -101
- package/dist/tools/write-playlist.d.ts +38 -1
- package/dist/tools/write-playlist.js +103 -1
- package/package.json +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { type CreatePlaylistResult } from "../store/write.js";
|
|
2
|
+
import { type CreatePlaylistResult, type EditResult, type InsertAt } from "../store/write.js";
|
|
3
3
|
import { type EngineError } from "../errors.js";
|
|
4
|
+
import type { QueryProcess } from "../proc/query-client.js";
|
|
4
5
|
export declare const CreatePlaylistInput: z.ZodObject<{
|
|
5
6
|
title: z.ZodString;
|
|
6
7
|
track_ids: z.ZodArray<z.ZodNumber>;
|
|
@@ -9,3 +10,39 @@ export declare function runCreatePlaylist(mdbPath: string, uuid: string, args: {
|
|
|
9
10
|
title: string;
|
|
10
11
|
track_ids: number[];
|
|
11
12
|
}, backupDir: string): Promise<CreatePlaylistResult | EngineError>;
|
|
13
|
+
export declare const AddTracksToPlaylistInput: z.ZodObject<{
|
|
14
|
+
playlist_id: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
playlist_name: z.ZodOptional<z.ZodString>;
|
|
16
|
+
track_ids: z.ZodArray<z.ZodNumber>;
|
|
17
|
+
at: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<"start">, z.ZodLiteral<"end">, z.ZodObject<{
|
|
18
|
+
after_position: z.ZodNumber;
|
|
19
|
+
}, z.core.$strip>]>>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export declare function runAddTracksToPlaylist(qp: QueryProcess, mdbPath: string, uuid: string, args: {
|
|
22
|
+
playlist_id?: number;
|
|
23
|
+
playlist_name?: string;
|
|
24
|
+
track_ids: number[];
|
|
25
|
+
at: InsertAt;
|
|
26
|
+
}, backupDir: string): Promise<EditResult | EngineError>;
|
|
27
|
+
export declare const RemoveTracksFromPlaylistInput: z.ZodObject<{
|
|
28
|
+
playlist_id: z.ZodOptional<z.ZodNumber>;
|
|
29
|
+
playlist_name: z.ZodOptional<z.ZodString>;
|
|
30
|
+
positions: z.ZodArray<z.ZodNumber>;
|
|
31
|
+
expect_track_ids: z.ZodOptional<z.ZodArray<z.ZodNullable<z.ZodNumber>>>;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
export declare function runRemoveTracksFromPlaylist(qp: QueryProcess, mdbPath: string, uuid: string, args: {
|
|
34
|
+
playlist_id?: number;
|
|
35
|
+
playlist_name?: string;
|
|
36
|
+
positions: number[];
|
|
37
|
+
expect_track_ids?: (number | null)[];
|
|
38
|
+
}, backupDir: string): Promise<EditResult | EngineError>;
|
|
39
|
+
export declare const ReorderPlaylistInput: z.ZodObject<{
|
|
40
|
+
playlist_id: z.ZodOptional<z.ZodNumber>;
|
|
41
|
+
playlist_name: z.ZodOptional<z.ZodString>;
|
|
42
|
+
order: z.ZodArray<z.ZodNumber>;
|
|
43
|
+
}, z.core.$strip>;
|
|
44
|
+
export declare function runReorderPlaylist(qp: QueryProcess, mdbPath: string, uuid: string, args: {
|
|
45
|
+
playlist_id?: number;
|
|
46
|
+
playlist_name?: string;
|
|
47
|
+
order: number[];
|
|
48
|
+
}, backupDir: string): Promise<EditResult | EngineError>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// src/tools/write-playlist.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { createPlaylist } from "../store/write.js";
|
|
3
|
+
import { createPlaylist, addTracksToPlaylist, removeTracksFromPlaylist, reorderPlaylist, } from "../store/write.js";
|
|
4
|
+
import { resolvePlaylist } from "../playlists.js";
|
|
5
|
+
import { isEngineError } from "../errors.js";
|
|
4
6
|
export const CreatePlaylistInput = z.object({
|
|
5
7
|
title: z.string().min(1).describe("Name for the new playlist. Must not already exist in this library."),
|
|
6
8
|
track_ids: z
|
|
@@ -11,3 +13,103 @@ export const CreatePlaylistInput = z.object({
|
|
|
11
13
|
export async function runCreatePlaylist(mdbPath, uuid, args, backupDir) {
|
|
12
14
|
return createPlaylist(mdbPath, uuid, { title: args.title, trackIds: args.track_ids }, { backupDir });
|
|
13
15
|
}
|
|
16
|
+
/** playlist_id/playlist_name, shared verbatim by every edit tool's schema below. */
|
|
17
|
+
const PlaylistSelectorShape = {
|
|
18
|
+
playlist_id: z
|
|
19
|
+
.number()
|
|
20
|
+
.int()
|
|
21
|
+
.positive()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("The playlist's id, from get_playlists or get_playlist_tracks. Exactly one of playlist_id/playlist_name is required."),
|
|
24
|
+
playlist_name: z
|
|
25
|
+
.string()
|
|
26
|
+
.min(1)
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("The playlist's name or full path (from get_playlists). Names are unique only within a " +
|
|
29
|
+
"folder; a name matching more than one playlist is refused with every candidate listed, " +
|
|
30
|
+
"not guessed at. Exactly one of playlist_id/playlist_name is required."),
|
|
31
|
+
};
|
|
32
|
+
const InsertAtInput = z.union([
|
|
33
|
+
z.literal("start"),
|
|
34
|
+
z.literal("end"),
|
|
35
|
+
z.object({
|
|
36
|
+
after_position: z
|
|
37
|
+
.number()
|
|
38
|
+
.int()
|
|
39
|
+
.positive()
|
|
40
|
+
.describe("1-based position, from get_playlist_tracks, to insert immediately after."),
|
|
41
|
+
}),
|
|
42
|
+
]);
|
|
43
|
+
export const AddTracksToPlaylistInput = z.object({
|
|
44
|
+
...PlaylistSelectorShape,
|
|
45
|
+
track_ids: z
|
|
46
|
+
.array(z.number().int().positive())
|
|
47
|
+
.max(10_000)
|
|
48
|
+
.describe("Track ids from search_tracks or get_tracks, in the order they should appear. A track " +
|
|
49
|
+
"already in the playlist is refused as duplicate_track."),
|
|
50
|
+
at: InsertAtInput.default("end").describe("Where the new tracks land, against the playlist's current 1-based positions (the same " +
|
|
51
|
+
"numbering get_playlist_tracks reports): \"start\", \"end\" (the default), or " +
|
|
52
|
+
"{ after_position: n }."),
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Turns `playlist_id`/`playlist_name` into a concrete `listId`, the same way
|
|
56
|
+
* `get_playlist_tracks` does (see resolvePlaylist in ../playlists.ts), so
|
|
57
|
+
* every edit tool refuses an ambiguous name identically rather than each
|
|
58
|
+
* growing its own resolution logic.
|
|
59
|
+
*
|
|
60
|
+
* `notFound: "playlist_not_found"` is what makes that code reachable at all.
|
|
61
|
+
* The store functions return it for an id that gets as far as them, and the
|
|
62
|
+
* README documents it as a refusal of all three edit tools -- but resolution
|
|
63
|
+
* runs first and used to answer every miss with `invalid_argument`, so no
|
|
64
|
+
* caller could ever see the documented code. It is also the more accurate of
|
|
65
|
+
* the two: the argument is well-formed, the playlist simply is not there.
|
|
66
|
+
* Ambiguity still comes back as `invalid_argument`, because a name matching
|
|
67
|
+
* three playlists genuinely is a problem with the argument.
|
|
68
|
+
*/
|
|
69
|
+
async function resolveListId(qp, args) {
|
|
70
|
+
const resolved = await resolvePlaylist(qp, { id: args.playlist_id, name: args.playlist_name }, { id: "playlist_id", name: "playlist_name", notFound: "playlist_not_found" });
|
|
71
|
+
if (isEngineError(resolved))
|
|
72
|
+
return resolved;
|
|
73
|
+
return resolved.playlist.id;
|
|
74
|
+
}
|
|
75
|
+
export async function runAddTracksToPlaylist(qp, mdbPath, uuid, args, backupDir) {
|
|
76
|
+
const listId = await resolveListId(qp, args);
|
|
77
|
+
if (isEngineError(listId))
|
|
78
|
+
return listId;
|
|
79
|
+
return addTracksToPlaylist(mdbPath, uuid, { listId, trackIds: args.track_ids, at: args.at }, { backupDir });
|
|
80
|
+
}
|
|
81
|
+
export const RemoveTracksFromPlaylistInput = z.object({
|
|
82
|
+
...PlaylistSelectorShape,
|
|
83
|
+
positions: z
|
|
84
|
+
.array(z.number().int().positive())
|
|
85
|
+
.max(50_000)
|
|
86
|
+
.describe("1-based positions to remove, from get_playlist_tracks against this playlist right now. " +
|
|
87
|
+
"Includes entries whose track is missing from the library (missing: true)."),
|
|
88
|
+
expect_track_ids: z
|
|
89
|
+
.array(z.number().int().positive().nullable())
|
|
90
|
+
.optional()
|
|
91
|
+
.describe("Optional, one entry per position: verifies each named position still holds the track " +
|
|
92
|
+
"expected before anything is removed. null means the position should hold an entry " +
|
|
93
|
+
"whose track is missing from the library, not \"no expectation\"."),
|
|
94
|
+
});
|
|
95
|
+
export async function runRemoveTracksFromPlaylist(qp, mdbPath, uuid, args, backupDir) {
|
|
96
|
+
const listId = await resolveListId(qp, args);
|
|
97
|
+
if (isEngineError(listId))
|
|
98
|
+
return listId;
|
|
99
|
+
return removeTracksFromPlaylist(mdbPath, uuid, { listId, positions: args.positions, expectTrackIds: args.expect_track_ids }, { backupDir });
|
|
100
|
+
}
|
|
101
|
+
export const ReorderPlaylistInput = z.object({
|
|
102
|
+
...PlaylistSelectorShape,
|
|
103
|
+
order: z
|
|
104
|
+
.array(z.number().int().positive())
|
|
105
|
+
.max(50_000)
|
|
106
|
+
.describe("A full permutation of 1..n, n being the playlist's current entry count. order[i] names " +
|
|
107
|
+
"the CURRENT 1-based position (from get_playlist_tracks) of the track that should end " +
|
|
108
|
+
"up at position i + 1. Every position must be named, even ones that do not move."),
|
|
109
|
+
});
|
|
110
|
+
export async function runReorderPlaylist(qp, mdbPath, uuid, args, backupDir) {
|
|
111
|
+
const listId = await resolveListId(qp, args);
|
|
112
|
+
if (isEngineError(listId))
|
|
113
|
+
return listId;
|
|
114
|
+
return reorderPlaylist(mdbPath, uuid, { listId, order: args.order }, { backupDir });
|
|
115
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engine-dj-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
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",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"type": "module",
|
|
29
29
|
"engines": {
|
|
30
|
-
"node": ">=22.
|
|
30
|
+
"node": ">=22.16.0"
|
|
31
31
|
},
|
|
32
32
|
"bin": {
|
|
33
33
|
"engine-dj-mcp": "dist/index.js"
|