engine-dj-mcp 0.17.0 → 0.17.1
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 +15 -4
- package/dist/errors.js +6 -5
- package/dist/library-select.d.ts +21 -0
- package/dist/library-select.js +60 -8
- package/dist/server.js +54 -36
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# engine-dj-mcp
|
|
2
2
|
|
|
3
|
-
[](https://github.com/Venut-Technologies/engine-dj-mcp/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/engine-dj-mcp)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
@@ -54,7 +54,7 @@ the configuration you are reading:
|
|
|
54
54
|
```json
|
|
55
55
|
{
|
|
56
56
|
"mcpServers": {
|
|
57
|
-
"engine-dj": { "command": "npx", "args": ["-y", "engine-dj-mcp@0.17.
|
|
57
|
+
"engine-dj": { "command": "npx", "args": ["-y", "engine-dj-mcp@0.17.1", "--allow-writes"] }
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
```
|
|
@@ -407,7 +407,7 @@ own checks.
|
|
|
407
407
|
| --- | --- | --- |
|
|
408
408
|
| `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 |
|
|
409
409
|
| `library_not_found` | `library` names nothing connected — the refusal lists what is — or the library's header could not be read. | yes |
|
|
410
|
-
| `ambiguous_library` | No `library` given, and more than one supported library is connected. Lists them — see [Choosing a library](#choosing-a-library). | yes |
|
|
410
|
+
| `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](#choosing-a-library). | yes |
|
|
411
411
|
| `unsupported_schema` | The library's version is outside what this server supports. | yes |
|
|
412
412
|
| `library_needs_recovery` | Engine DJ left an unrecovered journal. Launch Engine once. | yes |
|
|
413
413
|
| `library_busy` | Something holds a conflicting lock right now. Retry. | yes |
|
|
@@ -448,7 +448,7 @@ scanned first and is often empty, so "the first one found" would hide the
|
|
|
448
448
|
drive you actually work from.
|
|
449
449
|
|
|
450
450
|
That rule is enough for a read, which changes nothing: with two libraries
|
|
451
|
-
connected a read picks one
|
|
451
|
+
connected a read picks one. Pass `library` when it matters which.
|
|
452
452
|
|
|
453
453
|
**A write refuses instead**, as soon as more than one supported library is
|
|
454
454
|
connected — whatever their track counts. `ambiguous_library` lists every
|
|
@@ -464,6 +464,17 @@ work.
|
|
|
464
464
|
|
|
465
465
|
With a single library nothing changes: you never have to name it.
|
|
466
466
|
|
|
467
|
+
**Copies share a uuid.** Copy an `Engine Library` folder onto another drive —
|
|
468
|
+
a spare stick for a gig — and the copy keeps the original's uuid, so with both
|
|
469
|
+
connected one uuid names two libraries. A write naming that uuid is refused the
|
|
470
|
+
same way, with `ambiguous_library` listing both paths, rather than landing on
|
|
471
|
+
whichever drive was scanned first. Pass the path instead — it tells the copies
|
|
472
|
+
apart — and re-read from that path anything the write depends on, since a read
|
|
473
|
+
naming the uuid may have come from the other copy. Reads naming a shared uuid
|
|
474
|
+
are not refused: they answer from one of the copies. Before every write the
|
|
475
|
+
drives are scanned again, so a copy plugged in after the server started is
|
|
476
|
+
counted — as long as its library can be read.
|
|
477
|
+
|
|
467
478
|
The refusal tells the assistant to **ask you** rather than choose. Otherwise
|
|
468
479
|
"pass `library`, here are the two" is an invitation to take the first one,
|
|
469
480
|
which puts the write back on an arbitrary disk and makes the refusal
|
package/dist/errors.js
CHANGED
|
@@ -33,11 +33,12 @@ export const ERROR_CODES = [
|
|
|
33
33
|
"playlist_chain_damaged",
|
|
34
34
|
"playlist_not_found",
|
|
35
35
|
"invalid_position",
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
36
|
+
// A write cannot tell which physical library to change: no `library` was
|
|
37
|
+
// passed and more than one supported library is connected, or the uuid
|
|
38
|
+
// passed is shared by copies on different drives. Its own code rather than
|
|
39
|
+
// invalid_argument because the useful client response is specific: ask
|
|
40
|
+
// which drive, then retry with its path. Only writes raise it; see
|
|
41
|
+
// library-select.ts for why reads still choose.
|
|
41
42
|
"ambiguous_library",
|
|
42
43
|
// update_track_metadata (spec §7.2). stale_value: an `expect` no longer
|
|
43
44
|
// matches what is in the library. track_not_editable: this track, or one
|
package/dist/library-select.d.ts
CHANGED
|
@@ -81,8 +81,29 @@ export declare function ambiguousLibrary(tied: readonly LibraryInfo[]): EngineEr
|
|
|
81
81
|
*
|
|
82
82
|
* A path match is exact on the m.db file, not a prefix: a value that merely
|
|
83
83
|
* *contains* a library path must not select it.
|
|
84
|
+
*
|
|
85
|
+
* A uuid shared by several libraries resolves to the first of them in this
|
|
86
|
+
* server's list of known libraries -- root-scan order at startup, arrival
|
|
87
|
+
* order for a drive plugged in later, but no order a caller can rely on. That
|
|
88
|
+
* is tolerable for a read, which changes no disk, and never for a write: see
|
|
89
|
+
* namedWriteLibrary.
|
|
84
90
|
*/
|
|
85
91
|
export declare function findLibrary(libs: readonly LibraryInfo[], requested: string): LibraryInfo | null;
|
|
92
|
+
/**
|
|
93
|
+
* Every library a `library` value names, in the order of `libs`.
|
|
94
|
+
* More than one only for a uuid: copying an Engine Library folder onto another
|
|
95
|
+
* drive copies its uuid with it, while each library's path is its own.
|
|
96
|
+
*/
|
|
97
|
+
export declare function findLibraries(libs: readonly LibraryInfo[], requested: string): LibraryInfo[];
|
|
98
|
+
/**
|
|
99
|
+
* The one library a write names, or the refusal.
|
|
100
|
+
*
|
|
101
|
+
* findLibrary would hand back the first of two libraries sharing a uuid, and
|
|
102
|
+
* which is first is only discovery order -- the very thing ambiguousLibrary
|
|
103
|
+
* exists so that a write does not rest on. A named uuid is no better an answer
|
|
104
|
+
* than an omitted `library` when it names both a USB drive and its copy.
|
|
105
|
+
*/
|
|
106
|
+
export declare function namedWriteLibrary(libs: readonly LibraryInfo[], requested: string): LibraryInfo | EngineError;
|
|
86
107
|
/**
|
|
87
108
|
* The error for a `library` value that matched nothing. It names what was
|
|
88
109
|
* passed and lists what is actually selectable, because the two ways to get
|
package/dist/library-select.js
CHANGED
|
@@ -17,7 +17,9 @@ export const LIBRARY_ARG_DESCRIPTION = "Which library to use: either the uuid or
|
|
|
17
17
|
"the supported library holding the most tracks. A READ may always omit it. A WRITE " +
|
|
18
18
|
"may omit it only when a single supported library is connected: with two or more, a " +
|
|
19
19
|
"write refuses with ambiguous_library listing them, since the choice decides which " +
|
|
20
|
-
"disk changes; ask the user which, then pass it here."
|
|
20
|
+
"disk changes; ask the user which, then pass it here. A library copied onto another drive " +
|
|
21
|
+
"keeps its uuid, so a uuid can name two connected libraries: a write naming such a uuid " +
|
|
22
|
+
"refuses with ambiguous_library as well. Pass the path to write to one of them.";
|
|
21
23
|
export const LibraryArg = z.string().min(1).optional().describe(LIBRARY_ARG_DESCRIPTION);
|
|
22
24
|
/**
|
|
23
25
|
* The default when no `library` was given: the supported library with the
|
|
@@ -88,9 +90,12 @@ export function writeNeedsLibrary(libs) {
|
|
|
88
90
|
export function ambiguousLibrary(tied) {
|
|
89
91
|
const list = tied.map((l) => `${l.uuid} -- ${redactPath(l.path)} (${l.trackCount} tracks)`).join("; ");
|
|
90
92
|
return err("ambiguous_library", `More than one library is connected, so there is no default to write to: ${list}. ` +
|
|
91
|
-
`Nothing was written. ASK which one to write to, then retry with \`library\` set
|
|
92
|
-
`do not choose for them.
|
|
93
|
-
`
|
|
93
|
+
`Nothing was written. ASK which one to write to, then retry with \`library\` set to that ` +
|
|
94
|
+
`library's path -- do not choose for them. A copy keeps its uuid, so a uuid may name more ` +
|
|
95
|
+
`than one of these. They are usually a USB drive and its copy on the computer, ` +
|
|
96
|
+
`and one of them may be the drive they perform from. A read without \`library\` may have ` +
|
|
97
|
+
`come from a different one of these, so re-read anything the write depends on (track and ` +
|
|
98
|
+
`playlist ids, positions, current values) from the chosen library first.`, { detail: "not_committed" });
|
|
94
99
|
}
|
|
95
100
|
/**
|
|
96
101
|
* Resolves a caller-supplied `library` value: uuid first, then filesystem
|
|
@@ -106,19 +111,66 @@ export function ambiguousLibrary(tied) {
|
|
|
106
111
|
*
|
|
107
112
|
* A path match is exact on the m.db file, not a prefix: a value that merely
|
|
108
113
|
* *contains* a library path must not select it.
|
|
114
|
+
*
|
|
115
|
+
* A uuid shared by several libraries resolves to the first of them in this
|
|
116
|
+
* server's list of known libraries -- root-scan order at startup, arrival
|
|
117
|
+
* order for a drive plugged in later, but no order a caller can rely on. That
|
|
118
|
+
* is tolerable for a read, which changes no disk, and never for a write: see
|
|
119
|
+
* namedWriteLibrary.
|
|
109
120
|
*/
|
|
110
121
|
export function findLibrary(libs, requested) {
|
|
122
|
+
return findLibraries(libs, requested)[0] ?? null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Every library a `library` value names, in the order of `libs`.
|
|
126
|
+
* More than one only for a uuid: copying an Engine Library folder onto another
|
|
127
|
+
* drive copies its uuid with it, while each library's path is its own.
|
|
128
|
+
*/
|
|
129
|
+
export function findLibraries(libs, requested) {
|
|
111
130
|
const wanted = requested.trim();
|
|
112
131
|
if (!wanted)
|
|
113
|
-
return
|
|
114
|
-
const byUuid = libs.
|
|
115
|
-
if (byUuid)
|
|
132
|
+
return [];
|
|
133
|
+
const byUuid = libs.filter((l) => l.uuid && l.uuid.toLowerCase() === wanted.toLowerCase());
|
|
134
|
+
if (byUuid.length > 0)
|
|
116
135
|
return byUuid;
|
|
117
136
|
// resolve() turns a relative value into something rooted at the process
|
|
118
137
|
// cwd, which matches no library path -- exactly the intended outcome for
|
|
119
138
|
// a value that is neither a uuid nor a real path.
|
|
120
139
|
const wantedPath = resolve(expandHome(wanted));
|
|
121
|
-
return libs.
|
|
140
|
+
return libs.filter((l) => resolve(l.path) === wantedPath);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The one library a write names, or the refusal.
|
|
144
|
+
*
|
|
145
|
+
* findLibrary would hand back the first of two libraries sharing a uuid, and
|
|
146
|
+
* which is first is only discovery order -- the very thing ambiguousLibrary
|
|
147
|
+
* exists so that a write does not rest on. A named uuid is no better an answer
|
|
148
|
+
* than an omitted `library` when it names both a USB drive and its copy.
|
|
149
|
+
*/
|
|
150
|
+
export function namedWriteLibrary(libs, requested) {
|
|
151
|
+
const matches = findLibraries(libs, requested);
|
|
152
|
+
if (matches.length > 1)
|
|
153
|
+
return sharedUuid(requested, matches);
|
|
154
|
+
if (matches[0])
|
|
155
|
+
return matches[0];
|
|
156
|
+
// The same refusal a read gets, with its list moved into `message`: on a
|
|
157
|
+
// write `detail` is reserved, for the reason given at ambiguousLibrary.
|
|
158
|
+
const miss = libraryNotFound(requested, libs);
|
|
159
|
+
return { ...miss, message: `${miss.message}. ${miss.detail}`, detail: "not_committed" };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The refusal for a uuid naming more than one library. Lists paths, since the
|
|
163
|
+
* uuid is the one thing the candidates do not differ in; `detail` stays
|
|
164
|
+
* exactly "not_committed" for the reason given at ambiguousLibrary.
|
|
165
|
+
*/
|
|
166
|
+
function sharedUuid(requested, matches) {
|
|
167
|
+
const list = matches.map((l) => `${redactPath(l.path)} (${l.trackCount} tracks)`).join("; ");
|
|
168
|
+
return err("ambiguous_library", `"${requested.trim()}" names more than one connected library -- a library copied onto another ` +
|
|
169
|
+
`drive keeps its uuid: ${list}. Nothing was written. ASK which one to write to, then retry ` +
|
|
170
|
+
`with \`library\` set to that one's path -- do not choose for them. One of them may be the ` +
|
|
171
|
+
`drive they perform from. A read naming this uuid may have come from another of these copies, ` +
|
|
172
|
+
`so re-read anything the write depends on (track and playlist ids, positions, current values) ` +
|
|
173
|
+
`from that path first.`, { detail: "not_committed" });
|
|
122
174
|
}
|
|
123
175
|
/**
|
|
124
176
|
* The error for a `library` value that matched nothing. It names what was
|
package/dist/server.js
CHANGED
|
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
6
|
import { discoverLibraries, defaultRoots, probeLibraries } from "./discovery.js";
|
|
7
7
|
import { libraryCandidates, libraryTag, sidecarDir } from "./paths.js";
|
|
8
|
-
import { LibraryArg, ambiguousLibrary, writeNeedsLibrary, findLibrary, libraryNotFound, pickDefaultLibrary, } from "./library-select.js";
|
|
8
|
+
import { LibraryArg, ambiguousLibrary, writeNeedsLibrary, findLibrary, namedWriteLibrary, libraryNotFound, pickDefaultLibrary, } from "./library-select.js";
|
|
9
9
|
import { hasHotJournal } from "./store/connections.js";
|
|
10
10
|
import { QueryProcess } from "./proc/query-client.js";
|
|
11
11
|
import { IndexManager } from "./store/index-manager.js";
|
|
@@ -92,7 +92,8 @@ const WRITE_LIBRARY_NOTE = " With two or more supported libraries connected, thi
|
|
|
92
92
|
"rather than picking one, and lists them; nothing is written. That is so whatever their track " +
|
|
93
93
|
"counts are -- the count never said which disk should change. Ask the user which one, then " +
|
|
94
94
|
"retry with `library` set; do not pick for them, since one of them may be the drive they " +
|
|
95
|
-
"perform from."
|
|
95
|
+
"perform from. A library copied onto another drive keeps its uuid, so naming a uuid that " +
|
|
96
|
+
"two connected libraries share is refused the same way -- pass the path.";
|
|
96
97
|
/**
|
|
97
98
|
* Not UNDO_SCOPE_NOTE: that one says Engine copies *playlist* changes between
|
|
98
99
|
* libraries, which was measured. For track tags, a fresh Engine launch was
|
|
@@ -246,6 +247,13 @@ export async function createServer(opts = {}) {
|
|
|
246
247
|
rescanLibraries();
|
|
247
248
|
return findLibrary(knownList(), requested) ?? libraryNotFound(requested, knownList());
|
|
248
249
|
};
|
|
250
|
+
/** Resolves `library` the read way -- see selectLibrary -- then prepares it. */
|
|
251
|
+
const acquire = async (requested) => {
|
|
252
|
+
const lib = selectLibrary(requested);
|
|
253
|
+
if (isEngineError(lib))
|
|
254
|
+
return lib;
|
|
255
|
+
return prepare(lib);
|
|
256
|
+
};
|
|
249
257
|
/**
|
|
250
258
|
* `index_stale` is swallowed only when an index is genuinely attached:
|
|
251
259
|
* "the previous index is still in use" is a reason to answer anyway, but
|
|
@@ -256,10 +264,7 @@ export async function createServer(opts = {}) {
|
|
|
256
264
|
* string "no such table: side.track_derived", instead of `index_stale`
|
|
257
265
|
* with a `retry_after_ms` the model can act on.
|
|
258
266
|
*/
|
|
259
|
-
const
|
|
260
|
-
const lib = selectLibrary(requested);
|
|
261
|
-
if (isEngineError(lib))
|
|
262
|
-
return lib;
|
|
267
|
+
const prepare = async (lib) => {
|
|
263
268
|
const state = stateFor(lib);
|
|
264
269
|
const fresh = await state.mgr.ensureFresh();
|
|
265
270
|
if (!isEngineError(fresh))
|
|
@@ -269,8 +274,13 @@ export async function createServer(opts = {}) {
|
|
|
269
274
|
return fresh;
|
|
270
275
|
};
|
|
271
276
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
277
|
+
* The library a write may land in, without touching its search index. A
|
|
278
|
+
* tool that addresses tracks by id needs no index, and building one right
|
|
279
|
+
* before a write only makes it stale the moment the write commits.
|
|
280
|
+
* acquireForWrite adds the index for the tools that resolve playlists.
|
|
281
|
+
*
|
|
282
|
+
* Selection differs from a read's in that it must name exactly one
|
|
283
|
+
* physical library, whether `library` was omitted or given.
|
|
274
284
|
*
|
|
275
285
|
* `pickDefaultLibrary` breaks a tie on root-scan order, which is
|
|
276
286
|
* deterministic and, for a read, fine -- libraries tie because one is a
|
|
@@ -283,43 +293,51 @@ export async function createServer(opts = {}) {
|
|
|
283
293
|
* USB drive both held 257 tracks, tied precisely because one was a copy of
|
|
284
294
|
* the other.
|
|
285
295
|
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
296
|
+
* A named library is taken as named -- unless the name is a uuid two
|
|
297
|
+
* connected libraries share. Copying an Engine Library folder onto another
|
|
298
|
+
* drive copies its uuid, and resolving that uuid to the first match would
|
|
299
|
+
* be the same order-of-discovery pick the omitted case refuses, reached by
|
|
300
|
+
* a caller who had no way to know it was ambiguous. namedWriteLibrary refuses it and asks
|
|
301
|
+
* for the path, which tells the copies apart.
|
|
288
302
|
*
|
|
289
|
-
* Rescans first, because `knownList()` is a cache that
|
|
290
|
-
* library a later scan cannot see -- so a momentarily
|
|
291
|
-
* vanish from list_libraries. For a tie check that is
|
|
292
|
-
* direction that bites: pull the USB drive and one library is
|
|
293
|
-
* cache still holds two, and the write is refused naming a
|
|
294
|
-
* longer there. rescanLibraries() forgets a candidate
|
|
295
|
-
* which is exactly the distinction wanted here, and it
|
|
296
|
-
* plugged in mid-session be seen at all
|
|
303
|
+
* Rescans first, in both cases, because `knownList()` is a cache that
|
|
304
|
+
* deliberately keeps a library a later scan cannot see -- so a momentarily
|
|
305
|
+
* locked drive does not vanish from list_libraries. For a tie check that is
|
|
306
|
+
* wrong in the direction that bites: pull the USB drive and one library is
|
|
307
|
+
* left, but the cache still holds two, and the write is refused naming a
|
|
308
|
+
* drive that is no longer there. rescanLibraries() forgets a candidate
|
|
309
|
+
* whose path is gone, which is exactly the distinction wanted here, and it
|
|
310
|
+
* also lets a drive plugged in mid-session be seen at all -- including a
|
|
311
|
+
* copy that makes a named uuid ambiguous.
|
|
297
312
|
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
313
|
+
* A copy that has never once been readable since startup -- a hot journal,
|
|
314
|
+
* no permission -- is still not counted: its uuid was never read, so there
|
|
315
|
+
* is nothing to match. rescanLibraries keeps only libraries it has read.
|
|
316
|
+
*
|
|
317
|
+
* The cost is one discovery scan per write: every candidate library under
|
|
318
|
+
* the roots is opened read-only and its header and track count read, the
|
|
319
|
+
* same scan list_libraries runs. That is small against a write that is
|
|
320
|
+
* about to copy the entire database for its pre-write snapshot. Reads are
|
|
321
|
+
* left alone: they run far more often and a stale pick between two copies
|
|
322
|
+
* is not worth a scan apiece.
|
|
308
323
|
*/
|
|
309
324
|
const selectForWrite = (requested) => {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return selectLibrary(
|
|
325
|
+
rescanLibraries();
|
|
326
|
+
if (requested !== undefined)
|
|
327
|
+
return namedWriteLibrary(knownList(), requested);
|
|
328
|
+
const choices = writeNeedsLibrary(knownList());
|
|
329
|
+
if (choices.length > 0)
|
|
330
|
+
return ambiguousLibrary(choices);
|
|
331
|
+
return selectLibrary();
|
|
317
332
|
};
|
|
318
333
|
const acquireForWrite = async (requested) => {
|
|
319
334
|
const lib = selectForWrite(requested);
|
|
320
335
|
if (isEngineError(lib))
|
|
321
336
|
return lib;
|
|
322
|
-
|
|
337
|
+
// The library just resolved, not `requested` again by the read rules:
|
|
338
|
+
// today both give the same answer, but only this one was checked for a
|
|
339
|
+
// uuid shared between copies.
|
|
340
|
+
return prepare(lib);
|
|
323
341
|
};
|
|
324
342
|
/**
|
|
325
343
|
* Shared by the engine://libraries resource and the list_libraries tool so
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engine-dj-mcp",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.1",
|
|
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",
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
"author": "Mikhail Chereshnev <venuttv@gmail.com>",
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
|
-
"url": "git+https://github.com/Venut-
|
|
22
|
+
"url": "git+https://github.com/Venut-Technologies/engine-dj-mcp.git"
|
|
23
23
|
},
|
|
24
|
-
"homepage": "https://github.com/Venut-
|
|
24
|
+
"homepage": "https://github.com/Venut-Technologies/engine-dj-mcp#readme",
|
|
25
25
|
"bugs": {
|
|
26
|
-
"url": "https://github.com/Venut-
|
|
26
|
+
"url": "https://github.com/Venut-Technologies/engine-dj-mcp/issues"
|
|
27
27
|
},
|
|
28
28
|
"type": "module",
|
|
29
29
|
"engines": {
|