deep-roon-mcp 0.4.0 → 0.4.6
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 +4 -2
- package/dist/deep-roon-mcp.js +294 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ server of its own and sends nothing anywhere else. What you ask goes to your MCP
|
|
|
22
22
|
|
|
23
23
|
## Status
|
|
24
24
|
|
|
25
|
-
Version 0.4: fifty-
|
|
25
|
+
Version 0.4: fifty-nine tools. Qobuz, TIDAL or both — whatever is signed in on your Core.
|
|
26
26
|
|
|
27
27
|
**Reading** — changes nothing in Roon:
|
|
28
28
|
|
|
@@ -32,6 +32,7 @@ Version 0.4: fifty-six tools. Qobuz, TIDAL or both — whatever is signed in on
|
|
|
32
32
|
| `roon_new` | Qobuz's shelves (new, qobuzissimes, press awards, top, trending, ideal discography) or TIDAL's (new, recommended, top, rising, masters; albums or tracks) — by genre |
|
|
33
33
|
| `roon_genres` | Qobuz's genres and their ids, or TIDAL's genres and moods |
|
|
34
34
|
| `roon_album` | An album's page: tracks, release dates, label, barcode, credits |
|
|
35
|
+
| `roon_track_files` | Where your own files are: per track of an album, an artist or a list of tracks, the file path as the Core sees it (and duplicates) — Roon's export data |
|
|
35
36
|
| `roon_review` | The review Roon shows, in full, with its author |
|
|
36
37
|
| `roon_bio` | The biography Roon shows, in full, with related artists |
|
|
37
38
|
| `roon_artist` | An artist's discography at Qobuz or TIDAL, oldest first |
|
|
@@ -88,6 +89,7 @@ Version 0.4: fifty-six tools. Qobuz, TIDAL or both — whatever is signed in on
|
|
|
88
89
|
| `roon_library_add` | A Qobuz or TIDAL album into your library |
|
|
89
90
|
| `roon_playlist_create` | A new playlist from tracks |
|
|
90
91
|
| `roon_dsp_set` | MUSE on or off, one part on or off, or a preset |
|
|
92
|
+
| `roon_radio_favorite` | A station into My Live Radio, or out of it |
|
|
91
93
|
|
|
92
94
|
## Install
|
|
93
95
|
|
|
@@ -107,7 +109,7 @@ The settings (Claude Desktop › Settings › Extensions › Deep Roon MCP):
|
|
|
107
109
|
| Setting | What it does |
|
|
108
110
|
|---|---|
|
|
109
111
|
| Roon Core address | Empty: found on your network. Fill in the Core's address for another subnet or a private VPN (Tailscale) |
|
|
110
|
-
| Essentials only | Seventeen tools instead of fifty-
|
|
112
|
+
| Essentials only | Seventeen tools instead of fifty-nine, for small models |
|
|
111
113
|
| Use Roon's internal protocol | Off: only the official API — zones, playback, volume, grouping; no search, reviews, Focus or history |
|
|
112
114
|
|
|
113
115
|
### Other clients
|
package/dist/deep-roon-mcp.js
CHANGED
|
@@ -15525,7 +15525,7 @@ var require_lib2 = __commonJS({
|
|
|
15525
15525
|
var version2;
|
|
15526
15526
|
var init_package = __esm({
|
|
15527
15527
|
"package.json"() {
|
|
15528
|
-
version2 = "0.4.
|
|
15528
|
+
version2 = "0.4.6";
|
|
15529
15529
|
}
|
|
15530
15530
|
});
|
|
15531
15531
|
|
|
@@ -15852,6 +15852,15 @@ function field2(o, name) {
|
|
|
15852
15852
|
for (const [k, v] of Object.entries(f)) if (k.endsWith("::" + name)) return v;
|
|
15853
15853
|
return void 0;
|
|
15854
15854
|
}
|
|
15855
|
+
function imageKeys(c, o, name = "Images", max = 4) {
|
|
15856
|
+
return items(c, field2(o, name)).map((im) => {
|
|
15857
|
+
const m = /^broker:\/\/\/image\/([^.]+)\./.exec(String(field2(im, "Url") ?? ""));
|
|
15858
|
+
return m ? m[1] : "";
|
|
15859
|
+
}).filter(Boolean).slice(0, max);
|
|
15860
|
+
}
|
|
15861
|
+
function imageUrls(c, o, name = "Images", max = 4) {
|
|
15862
|
+
return items(c, field2(o, name)).map((im) => String(field2(im, "Url") ?? "").replace(/^http:\/\//, "https://")).filter((u) => /^https:\/\//.test(u)).slice(0, max);
|
|
15863
|
+
}
|
|
15855
15864
|
function imageKey(c, o, name = "Image") {
|
|
15856
15865
|
const url2 = field2(obj(c, field2(o, name)), "Url");
|
|
15857
15866
|
const m = /^broker:\/\/\/image\/([^.]+)\./.exec(String(url2 ?? ""));
|
|
@@ -15953,6 +15962,8 @@ function albumRow(c, o) {
|
|
|
15953
15962
|
if (typeof st === "number" && st > 0) r.stars = st / 2;
|
|
15954
15963
|
const lb = strings(c, field2(o, "Labels"));
|
|
15955
15964
|
if (lb.length) r.label = lb.join(" / ");
|
|
15965
|
+
const gs = strings(c, field2(o, "Genres"));
|
|
15966
|
+
if (gs.length) r.genres = gs.slice(0, 8);
|
|
15956
15967
|
if (field2(o, "Source") === 1) r.in_library = true;
|
|
15957
15968
|
if (flag(field2(o, "IsFavorite"))) r.favorite = true;
|
|
15958
15969
|
if (flag(field2(o, "IsListenLater"))) r.listen_later = true;
|
|
@@ -16007,6 +16018,8 @@ var init_graph = __esm({
|
|
|
16007
16018
|
return (o?.fields?.$items ?? o?.$items ?? []).filter((x) => typeof x === "string");
|
|
16008
16019
|
}, "strings");
|
|
16009
16020
|
flag = /* @__PURE__ */ __name((v) => v === true || Buffer.isBuffer(v) && v.length > 1 && v[0] >= 1, "flag");
|
|
16021
|
+
__name(imageKeys, "imageKeys");
|
|
16022
|
+
__name(imageUrls, "imageUrls");
|
|
16010
16023
|
__name(imageKey, "imageKey");
|
|
16011
16024
|
__name(dateParts, "dateParts");
|
|
16012
16025
|
yearOf = /* @__PURE__ */ __name((v) => dateParts(v)?.[0], "yearOf");
|
|
@@ -16413,6 +16426,71 @@ async function discography(key, wanted) {
|
|
|
16413
16426
|
return { service: SERVICE_NAME[service2], artist, albums };
|
|
16414
16427
|
});
|
|
16415
16428
|
}
|
|
16429
|
+
async function trackFiles(a) {
|
|
16430
|
+
return withCore(async (c) => {
|
|
16431
|
+
const cb = { type: "ResultCallback<IList<TrackExportInfo>>", name: "cb_result" };
|
|
16432
|
+
let res;
|
|
16433
|
+
if (a.tracks?.length) {
|
|
16434
|
+
if (a.tracks.some((k) => !/^\d+$/.test(k))) throw new RoonError("A track key is a number", "invalid");
|
|
16435
|
+
res = await short(c.call(
|
|
16436
|
+
"Library",
|
|
16437
|
+
"GetTrackExportInfo",
|
|
16438
|
+
[{ type: "IEnumerable<long>", name: "ids" }, cb],
|
|
16439
|
+
buildArgs([Arg.collection(a.tracks.map((k) => buildArgs([Arg.long(BigInt(k))])))]),
|
|
16440
|
+
library(c)
|
|
16441
|
+
), 2e4);
|
|
16442
|
+
} else if (a.album) {
|
|
16443
|
+
const al = await findAlbum(c, a.album);
|
|
16444
|
+
res = await short(c.call(
|
|
16445
|
+
"Library",
|
|
16446
|
+
"GetTrackExportInfoForAlbums",
|
|
16447
|
+
[{ type: "IEnumerable<AlbumBase>", name: "albums" }, cb],
|
|
16448
|
+
buildArgs([Arg.collection([buildArgs([Arg.ref(al.oid)])])]),
|
|
16449
|
+
library(c)
|
|
16450
|
+
), 2e4);
|
|
16451
|
+
} else if (a.artist) {
|
|
16452
|
+
const p = await performer(c, a.artist);
|
|
16453
|
+
res = await short(c.call(
|
|
16454
|
+
"Library",
|
|
16455
|
+
"GetTrackExportInfoForPerformers",
|
|
16456
|
+
[{ type: "IEnumerable<PerformerBase>", name: "performers" }, cb],
|
|
16457
|
+
buildArgs([Arg.collection([buildArgs([Arg.ref(p.$ref)])])]),
|
|
16458
|
+
library(c)
|
|
16459
|
+
), 3e4);
|
|
16460
|
+
} else throw new RoonError("Give an album key, an artist key or track keys", "invalid");
|
|
16461
|
+
if (!res?.success) throw new RoonError(`Roon refused the file list (${res?.status ?? "no answer"})`);
|
|
16462
|
+
if (res.payload.length < 2) return { tracks: [] };
|
|
16463
|
+
const r = new BinaryReader(Uint8Array.from(res.payload));
|
|
16464
|
+
r.integer();
|
|
16465
|
+
const n = r.flexInt();
|
|
16466
|
+
const graph = c.graph;
|
|
16467
|
+
const infos = [];
|
|
16468
|
+
for (let i = 0; i < n && r.remaining > 0; i++) infos.push(graph.readValue(r, 23));
|
|
16469
|
+
await untilLoaded(c, infos.map((x) => field2(x, "Track")).filter(isRef2), 3e3);
|
|
16470
|
+
const paden = /* @__PURE__ */ __name((x) => items(c, field2(x, "DuplicateTracks")).map((d) => field2(d, "FilePath")).filter((p) => typeof p === "string" && !!p), "paden");
|
|
16471
|
+
const tracks2 = infos.map((x) => {
|
|
16472
|
+
const t = obj(c, field2(x, "Track"));
|
|
16473
|
+
const row = { key: String(field2(t, "TrackId") ?? "") };
|
|
16474
|
+
const title = field2(t, "Title");
|
|
16475
|
+
if (title) row.title = title;
|
|
16476
|
+
const al = field2(obj(c, field2(t, "Album")), "Title");
|
|
16477
|
+
if (al) row.album = al;
|
|
16478
|
+
const n2 = field2(t, "TrackNumber");
|
|
16479
|
+
if (n2 != null) row.n = n2;
|
|
16480
|
+
const path2 = field2(x, "FilePath");
|
|
16481
|
+
if (typeof path2 === "string" && path2) row.path = path2;
|
|
16482
|
+
const format = field2(t, "Format");
|
|
16483
|
+
if (format) row.format = format;
|
|
16484
|
+
const dup = paden(x);
|
|
16485
|
+
if (dup.length) row.duplicates = dup;
|
|
16486
|
+
return row;
|
|
16487
|
+
});
|
|
16488
|
+
const gevraagd = a.tracks?.length ?? 0, ontbreekt = gevraagd ? gevraagd - tracks2.length : 0;
|
|
16489
|
+
const zonder = tracks2.filter((t) => !t.path).length + Math.max(0, ontbreekt);
|
|
16490
|
+
const of = gevraagd || tracks2.length;
|
|
16491
|
+
return { tracks: tracks2, ...zonder ? { note: `${zonder} of ${of} ${of === 1 ? "track" : "tracks"} ${zonder === 1 ? "has" : "have"} no file (streaming tracks, or not on this Core's storage)` } : {} };
|
|
16492
|
+
});
|
|
16493
|
+
}
|
|
16416
16494
|
var SERVICE_NAME, SERVICE_TYPE, NONE2, SECTIONS, library, CREDIT_KIND, cut, choose;
|
|
16417
16495
|
var init_library = __esm({
|
|
16418
16496
|
"src/roon/library.ts"() {
|
|
@@ -16469,6 +16547,7 @@ var init_library = __esm({
|
|
|
16469
16547
|
__name(performer, "performer");
|
|
16470
16548
|
__name(bio, "bio");
|
|
16471
16549
|
__name(discography, "discography");
|
|
16550
|
+
__name(trackFiles, "trackFiles");
|
|
16472
16551
|
}
|
|
16473
16552
|
});
|
|
16474
16553
|
|
|
@@ -16478,6 +16557,7 @@ __export(focus_exports, {
|
|
|
16478
16557
|
artists: () => artists,
|
|
16479
16558
|
focus: () => focus,
|
|
16480
16559
|
library: () => library2,
|
|
16560
|
+
toplevelGenres: () => toplevelGenres,
|
|
16481
16561
|
tracks: () => tracks,
|
|
16482
16562
|
work: () => work,
|
|
16483
16563
|
works: () => works
|
|
@@ -16576,6 +16656,28 @@ async function focusMembers(c, f, people = ["artist", "composer", "producer"]) {
|
|
|
16576
16656
|
if (f.not_played_within_days) m.push({ name: "ExcludePlayedInLastNDays", propType: 10, value: maybeInt(f.not_played_within_days) });
|
|
16577
16657
|
if (f.added_within_days) m.push({ name: "RequireImportedInLastNDays", propType: 10, value: maybeInt(f.added_within_days) });
|
|
16578
16658
|
if (f.hires) m.push({ name: "RequireSampleRates", propType: 23, value: intList([88200, 96e3, 176400, 192e3, 352800, 384e3]) });
|
|
16659
|
+
if (f.sample_rate) {
|
|
16660
|
+
const hz = names(f.sample_rate).map((x) => Math.round(parseFloat(x) * (parseFloat(x) < 1e3 ? 1e3 : 1))).filter((x) => x > 0);
|
|
16661
|
+
if (hz.length) m.push({ name: "RequireSampleRates", propType: 23, value: intList(hz) });
|
|
16662
|
+
}
|
|
16663
|
+
if (f.bits) {
|
|
16664
|
+
const b = names(f.bits).map(Number).filter((x) => x > 0);
|
|
16665
|
+
if (b.length) m.push({ name: "RequireBitsPerSample", propType: 23, value: intList(b) });
|
|
16666
|
+
}
|
|
16667
|
+
if (f.mqa === true) m.push(yes("RequireIsMQA"));
|
|
16668
|
+
else if (f.mqa === false) m.push(yes("ExcludeIsMQA"));
|
|
16669
|
+
if (f.storage) {
|
|
16670
|
+
const whole = await albumInfo(c, []);
|
|
16671
|
+
const want = f.storage.toLowerCase();
|
|
16672
|
+
const locs = items(c, field2(whole, "Locations")).map((it) => field2(it, "Value")).filter(isRef2);
|
|
16673
|
+
await untilLoaded(c, locs, 1500);
|
|
16674
|
+
const hit = locs.find((v) => {
|
|
16675
|
+
const n = String(field2(obj(c, v), "Name") ?? "").toLowerCase();
|
|
16676
|
+
return want === "local" ? !/tidal|qobuz/.test(n) : n.includes(want);
|
|
16677
|
+
});
|
|
16678
|
+
if (!hit) throw new RoonError(`No "${f.storage}" in this library's locations`, "not_found");
|
|
16679
|
+
m.push({ name: "RequireLocations", propType: 23, value: refList2([hit.$ref]) });
|
|
16680
|
+
}
|
|
16579
16681
|
if (f.live === true) m.push(yes("RequireIsLive"));
|
|
16580
16682
|
else if (f.live === false) m.push(yes("ExcludeIsLive"));
|
|
16581
16683
|
if (f.compilation === true) m.push(yes("RequireIsCompilation"));
|
|
@@ -16607,10 +16709,20 @@ function ordering(c, kind, table, sort, ascending, descendingByDefault) {
|
|
|
16607
16709
|
if (sort === "random") members2.push({ name: "RandomSeed", propType: 0, value: new BinaryWriter().integer(Math.floor(Math.random() * 2147483647)).toBuffer() });
|
|
16608
16710
|
return [{ name: "Ordering", propType: 23, value: c.structArg(`Sooloos.Broker.Api.${kind}QueryOrdering`, members2) }];
|
|
16609
16711
|
}
|
|
16712
|
+
async function toplevelGenres(c) {
|
|
16713
|
+
if (toplevelNames) return toplevelNames;
|
|
16714
|
+
const { members: members2 } = await focusMembers(c, {});
|
|
16715
|
+
const info = await albumInfo(c, members2);
|
|
16716
|
+
const hist = items(c, field2(info, "ToplevelGenres")).map((it) => field2(it, "Value"));
|
|
16717
|
+
await untilLoaded(c, hist.filter(isRef2), 2e3);
|
|
16718
|
+
const names2 = hist.map((w) => isRef2(w) ? field2(obj(c, w), "Name") : w).filter((n) => typeof n === "string");
|
|
16719
|
+
if (names2.length) toplevelNames = new Set(names2);
|
|
16720
|
+
return new Set(names2);
|
|
16721
|
+
}
|
|
16610
16722
|
async function library2(f, sort, ascending, limit3, offset2) {
|
|
16611
16723
|
return withCore(async (c) => {
|
|
16612
16724
|
const { members: members2, matched } = await focusMembers(c, f);
|
|
16613
|
-
members2.push(...ordering(c, "Album", ALBUM_ORDER, sort, ascending, ["added", "most_played", "popular"]));
|
|
16725
|
+
members2.push(...ordering(c, "Album", ALBUM_ORDER, sort, ascending, ["added", "released", "most_played", "popular"]));
|
|
16614
16726
|
const out = await virtualQuery(c, "Album", members2, limit3, offset2, (o) => albumRow(c, o));
|
|
16615
16727
|
return { ...out, matched };
|
|
16616
16728
|
});
|
|
@@ -16652,7 +16764,13 @@ async function focus(f, more, search2, limit3) {
|
|
|
16652
16764
|
bit_depths: numbers("BitsPerSample").map((x) => ({ bits: x.w, albums: x.n })),
|
|
16653
16765
|
live: field2(info, "IsLiveCount") ?? 0,
|
|
16654
16766
|
compilations: field2(info, "IsCompilationCount") ?? 0,
|
|
16655
|
-
roon_picks: field2(info, "IsPickCount") ?? 0
|
|
16767
|
+
roon_picks: field2(info, "IsPickCount") ?? 0,
|
|
16768
|
+
mqa: field2(info, "IsMQACount") ?? 0,
|
|
16769
|
+
// Raw histograms, to be named once measured (13 Oct 2026): where the albums live, and their file formats.
|
|
16770
|
+
storage: hist("Locations").map((x) => {
|
|
16771
|
+
const n = isRef2(x.w) ? String(field2(obj(c, x.w), "Name") ?? "") : String(x.w);
|
|
16772
|
+
return { name: /tidal/i.test(n) ? "TIDAL" : /qobuz/i.test(n) ? "Qobuz" : "Local", albums: x.n };
|
|
16773
|
+
})
|
|
16656
16774
|
};
|
|
16657
16775
|
});
|
|
16658
16776
|
}
|
|
@@ -16673,7 +16791,7 @@ async function tracks(f, sort, ascending, limit3, offset2) {
|
|
|
16673
16791
|
text: f.text
|
|
16674
16792
|
}, ["artist", "composer"]);
|
|
16675
16793
|
if (f.title) members2.push(text("TrackTitleTextFilter", f.title));
|
|
16676
|
-
members2.push(...ordering(c, "Track", TRACK_ORDER, sort, ascending, ["most_played", "last_played", "added", "popular"]));
|
|
16794
|
+
members2.push(...ordering(c, "Track", TRACK_ORDER, sort, ascending, ["released", "most_played", "last_played", "added", "popular"]));
|
|
16677
16795
|
const out = await virtualQuery(c, "Track", members2, limit3, offset2, (o) => resultRow(c, o));
|
|
16678
16796
|
return { ...out, matched };
|
|
16679
16797
|
});
|
|
@@ -16711,6 +16829,8 @@ function workRow(c, w) {
|
|
|
16711
16829
|
if (typeof l === "number" && l > 0) r.length = duration3(l);
|
|
16712
16830
|
const n = field2(w, "PerformanceCount");
|
|
16713
16831
|
if (typeof n === "number") r.performances = n;
|
|
16832
|
+
const imgs = imageKeys(c, w);
|
|
16833
|
+
if (imgs.length) r.images = imgs;
|
|
16714
16834
|
return r;
|
|
16715
16835
|
}
|
|
16716
16836
|
async function works(f, sort, ascending, limit3, offset2) {
|
|
@@ -16777,7 +16897,7 @@ async function work(key, max, wiki) {
|
|
|
16777
16897
|
};
|
|
16778
16898
|
});
|
|
16779
16899
|
}
|
|
16780
|
-
var intList, textList, refList2, maybeInt, dateValue, yes, text, names, english, PANEL, ALBUM_ORDER, TRACK_ORDER, ARTIST_ORDER, WORK_ORDER;
|
|
16900
|
+
var intList, textList, refList2, maybeInt, dateValue, yes, text, names, english, PANEL, ALBUM_ORDER, TRACK_ORDER, ARTIST_ORDER, WORK_ORDER, toplevelNames;
|
|
16781
16901
|
var init_focus = __esm({
|
|
16782
16902
|
"src/roon/focus.ts"() {
|
|
16783
16903
|
"use strict";
|
|
@@ -16827,6 +16947,8 @@ var init_focus = __esm({
|
|
|
16827
16947
|
TRACK_ORDER = { title: 0, released: 1, most_played: 2, last_played: 3, artist: 4, album: 5, length: 6, added: 7, popular: 14, random: 16 };
|
|
16828
16948
|
ARTIST_ORDER = { name: 0, albums: 2, works: 3, most_played: 4, popular: 6 };
|
|
16829
16949
|
WORK_ORDER = { title: 0, composer: 1, composed: 3, period: 4, performances: 5, popular: 9 };
|
|
16950
|
+
toplevelNames = null;
|
|
16951
|
+
__name(toplevelGenres, "toplevelGenres");
|
|
16830
16952
|
__name(library2, "library");
|
|
16831
16953
|
__name(focus, "focus");
|
|
16832
16954
|
__name(tracks, "tracks");
|
|
@@ -49478,6 +49600,7 @@ __name(nowPlaying, "nowPlaying");
|
|
|
49478
49600
|
init_intern();
|
|
49479
49601
|
init_serializer();
|
|
49480
49602
|
init_writer();
|
|
49603
|
+
init_signature();
|
|
49481
49604
|
init_session();
|
|
49482
49605
|
init_log();
|
|
49483
49606
|
init_graph();
|
|
@@ -49935,6 +50058,8 @@ function stationRow(c, o) {
|
|
|
49935
50058
|
if (field2(o, "IsInLibrary") === true) r.in_library = true;
|
|
49936
50059
|
const img = imageKey(c, o);
|
|
49937
50060
|
if (img) r.image = img;
|
|
50061
|
+
const url2 = field2(obj(c, field2(o, "Image")), "Url");
|
|
50062
|
+
if (typeof url2 === "string" && /^https?:/.test(url2)) r.image_url = url2;
|
|
49938
50063
|
return r;
|
|
49939
50064
|
}
|
|
49940
50065
|
__name(stationRow, "stationRow");
|
|
@@ -50030,6 +50155,30 @@ async function playRadio(key, zoneName) {
|
|
|
50030
50155
|
return { ok: true, ...r, ...await started2(r.zone, 0) };
|
|
50031
50156
|
}
|
|
50032
50157
|
__name(playRadio, "playRadio");
|
|
50158
|
+
async function radioFavorite(key, on) {
|
|
50159
|
+
return withCore(async (c) => {
|
|
50160
|
+
const st = await findStation(c, key);
|
|
50161
|
+
const args = buildArgs([Arg.collection([buildArgs([Arg.ref(st.oid)])])]);
|
|
50162
|
+
if (on) {
|
|
50163
|
+
const res = await c.call("Radio", "AddToLibrary", [{ type: "IList<Channel>", name: "channels" }, { type: "ResultCallback", name: "cb_result" }], args, radioOid(c));
|
|
50164
|
+
if (!res.success) throw new RoonError(`Roon refused (${res.status})`);
|
|
50165
|
+
} else {
|
|
50166
|
+
c.remoting.callMethodNoReply(radioOid(c), formatMethodSignature("Radio", "RemoveFromLibrary", [{ type: "IList<Channel>", name: "channels" }, { type: "ResultCallback", name: "cb_result" }]), args);
|
|
50167
|
+
}
|
|
50168
|
+
log(`radio_favorite: ${field2(st, "Name")} ${on ? "+" : "\u2212"}`);
|
|
50169
|
+
await wait(800);
|
|
50170
|
+
const id = hex3(field2(st, "ChannelId"));
|
|
50171
|
+
const mine = await myStations(c).catch(() => null);
|
|
50172
|
+
const now = mine ? mine.some((x) => hex3(field2(x, "ChannelId")) === id) : void 0;
|
|
50173
|
+
return {
|
|
50174
|
+
ok: now === void 0 || now === on,
|
|
50175
|
+
station: field2(st, "Name"),
|
|
50176
|
+
in_library: now ?? on,
|
|
50177
|
+
note: now === void 0 ? "Roon said ok; it couldn't be read back" : now !== on ? "Roon said ok, but nothing changed" : void 0
|
|
50178
|
+
};
|
|
50179
|
+
});
|
|
50180
|
+
}
|
|
50181
|
+
__name(radioFavorite, "radioFavorite");
|
|
50033
50182
|
|
|
50034
50183
|
// src/core/officialtools.ts
|
|
50035
50184
|
init_errors();
|
|
@@ -50293,6 +50442,16 @@ function registerReadTools(server, internal = true) {
|
|
|
50293
50442
|
inputSchema: { key: external_exports.string().describe("An album key."), credits: external_exports.boolean().optional() },
|
|
50294
50443
|
annotations: READ
|
|
50295
50444
|
}, ({ key, credits }) => run(() => album(key, credits === true)));
|
|
50445
|
+
reg("roon_track_files", {
|
|
50446
|
+
title: "File paths",
|
|
50447
|
+
description: "Where the user's own music files are: for an album (`album` key), an artist (`artist` key, all their tracks in the library) or a list of `tracks` keys, each track with its `key`, title, album, track number, format and `path` \u2014 the file as the Roon Core sees it (on the Core's storage), plus `duplicates` when Roon has the same track twice. The data behind Roon's own Export. Use it to link Roon's tracks to files, e.g. to join with data the user keeps per file. Streaming tracks (Qobuz, TIDAL) have no path.",
|
|
50448
|
+
inputSchema: {
|
|
50449
|
+
album: external_exports.string().optional().describe("An album key."),
|
|
50450
|
+
artist: external_exports.string().optional().describe("An artist key."),
|
|
50451
|
+
tracks: external_exports.array(external_exports.string()).max(500).optional().describe("Track keys.")
|
|
50452
|
+
},
|
|
50453
|
+
annotations: READ
|
|
50454
|
+
}, ({ album: album2, artist, tracks: tracks2 }) => run(() => trackFiles({ album: album2, artist, tracks: tracks2 })));
|
|
50296
50455
|
reg("roon_review", {
|
|
50297
50456
|
title: "Album review",
|
|
50298
50457
|
description: "The review Roon shows on an album's page, in full, with its author \u2014 by album `key`. When the user asks for the review (\"what does Roon say about this album\", \"show me the review\"), they want to read the review itself: show the text complete and exactly as returned, as a quote with the author's name \u2014 no paraphrase or shortening unless they ask for a summary, and don't go to the web for it. `wikipedia: true` adds the album's Wikipedia text; it also comes by itself when Roon has no review of its own. `max_chars` only when a shorter excerpt is asked for.",
|
|
@@ -50325,7 +50484,7 @@ function registerReadTools(server, internal = true) {
|
|
|
50325
50484
|
}, () => run(() => internal ? nowPlaying() : nowPlaying2()));
|
|
50326
50485
|
reg("roon_radio", {
|
|
50327
50486
|
title: "Live Radio",
|
|
50328
|
-
description: 'Roon\'s Live Radio directory. Without arguments: the user\'s own stations (`my_stations`) and the top categories (Music, News & Talk, Sports, Locations), each with a `key`. With `category` (a key): its sub-categories and stations. With `genre` (a category\'s name as written, e.g. "Jazz", "Classical"): stations of that genre. Stations carry a `key` for roon_play_radio, their location and `image
|
|
50487
|
+
description: 'Roon\'s Live Radio directory. Without arguments: the user\'s own stations (`my_stations`) and the top categories (Music, News & Talk, Sports, Locations), each with a `key`. With `category` (a key): its sub-categories and stations. With `genre` (a category\'s name as written, e.g. "Jazz", "Classical"): stations of that genre. Stations carry a `key` for roon_play_radio, their location, and their logo: `image_url` (a plain https picture) for the directory, `image` (a key at the Core) for an own station.',
|
|
50329
50488
|
inputSchema: { category: external_exports.string().optional().describe("A category key from a previous answer."), genre: external_exports.string().optional().describe("A genre, as roon_radio names it (e.g. Jazz)."), limit: limit(100, 40) },
|
|
50330
50489
|
annotations: READ
|
|
50331
50490
|
}, ({ category, genre, limit: n }) => run(() => radio(category, genre, n ?? 40)));
|
|
@@ -50482,6 +50641,7 @@ init_writer();
|
|
|
50482
50641
|
init_session();
|
|
50483
50642
|
init_log();
|
|
50484
50643
|
init_graph();
|
|
50644
|
+
init_focus();
|
|
50485
50645
|
var localNow = /* @__PURE__ */ __name(() => {
|
|
50486
50646
|
const d = /* @__PURE__ */ new Date(), t = /* @__PURE__ */ __name((n) => String(n).padStart(2, "0"), "t");
|
|
50487
50647
|
return `${d.getFullYear()}-${t(d.getMonth() + 1)}-${t(d.getDate())}T${t(d.getHours())}:${t(d.getMinutes())}:${t(d.getSeconds())}`;
|
|
@@ -50526,14 +50686,25 @@ async function dailyPicks(perBox) {
|
|
|
50526
50686
|
});
|
|
50527
50687
|
}
|
|
50528
50688
|
__name(dailyPicks, "dailyPicks");
|
|
50529
|
-
async function newForYou(limit3, offset2) {
|
|
50689
|
+
async function newForYou(limit3, offset2, genre) {
|
|
50530
50690
|
return withCore(async (c) => {
|
|
50531
50691
|
const res = await c.call("Library", "GetNewReleasesForYou", [{ type: "Sooid", name: "profileid" }, { type: "ResultCallback<DataList<AlbumWithExtras>>", name: "cb_result" }], buildArgs([Arg.sooid(c.profile())]), c.serviceOid("Library"));
|
|
50532
50692
|
if (!res.success) throw new RoonError(`Roon refused New releases for you (${res.status})`);
|
|
50533
|
-
|
|
50693
|
+
let all = items(c, decoded2(c, res.payload));
|
|
50694
|
+
await untilLoaded(c, all.map((x) => field2(x, "Album")).filter(isRef2), 4e3);
|
|
50695
|
+
const top2 = await toplevelGenres(c);
|
|
50696
|
+
const counts = /* @__PURE__ */ new Map();
|
|
50697
|
+
for (const x of all) for (const n of new Set(strings(c, field2(obj(c, field2(x, "Album")), "Genres")))) if (top2.has(n)) counts.set(n, (counts.get(n) ?? 0) + 1);
|
|
50698
|
+
const genres2 = [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([name, albums]) => ({ name, albums }));
|
|
50699
|
+
if (genre) {
|
|
50700
|
+
const g = genre.toLowerCase();
|
|
50701
|
+
const names2 = /* @__PURE__ */ __name((x) => strings(c, field2(obj(c, field2(x, "Album")), "Genres")).map((n) => n.toLowerCase()), "names");
|
|
50702
|
+
const exact = all.filter((x) => names2(x).includes(g));
|
|
50703
|
+
all = exact.length ? exact : all.filter((x) => names2(x).some((n) => n.includes(g)));
|
|
50704
|
+
}
|
|
50534
50705
|
const part = all.slice(Math.max(0, offset2), Math.max(0, offset2) + Math.min(Math.max(1, limit3), 100));
|
|
50535
50706
|
await untilLoaded(c, part.map((x) => field2(x, "Album")).filter(isRef2), 3e3);
|
|
50536
|
-
return { total: all.length, offset: Math.max(0, offset2), rows: part.map((x) => {
|
|
50707
|
+
return { total: all.length, offset: Math.max(0, offset2), genres: genres2, rows: part.map((x) => {
|
|
50537
50708
|
const a = obj(c, field2(x, "Album"));
|
|
50538
50709
|
if (!a || field2(a, "Title") === void 0) return null;
|
|
50539
50710
|
const r = albumRow(c, a);
|
|
@@ -50567,10 +50738,43 @@ async function mixes() {
|
|
|
50567
50738
|
const r = { key: mixKey(m), kind: gd && field2(gd, "GenreName") ? "genre" : "artist", name: mixName(c, m) };
|
|
50568
50739
|
const ts = (obj(c, field2(pd ?? gd, "Touchstones"))?.fields?.$items ?? []).filter((x) => typeof x === "string").map(clean).slice(0, 8);
|
|
50569
50740
|
if (ts.length) r.touchstones = ts;
|
|
50741
|
+
const img = pd ? imageKey(c, pd, "Photo") ?? imageKey(c, pd, "Avatar") : void 0;
|
|
50742
|
+
if (img) r.image = img;
|
|
50743
|
+
const imgs = gd ? imageKeys(c, gd) : [];
|
|
50744
|
+
if (imgs.length) r.images = imgs;
|
|
50570
50745
|
return r;
|
|
50571
50746
|
}) }));
|
|
50572
50747
|
}
|
|
50573
50748
|
__name(mixes, "mixes");
|
|
50749
|
+
async function mix(key, limit3) {
|
|
50750
|
+
return withCore(async (c) => {
|
|
50751
|
+
const all = await readMixes(c);
|
|
50752
|
+
const m = all.find((x) => mixKey(x) === key) ?? all.find((x) => String(mixName(c, x)).toLowerCase() === key.toLowerCase());
|
|
50753
|
+
if (!m?.oid) throw new RoonError("Unknown mix \u2014 read roon_mixes for today's keys", "not_found");
|
|
50754
|
+
const res = await c.call("Mix", "GetItems", [{ type: "ResultCallback<DataList<MixItem>>", name: "cb_result" }], Buffer.alloc(0), m.oid);
|
|
50755
|
+
if (!res.success) throw new RoonError(`Roon refused the mix (${res.status})`);
|
|
50756
|
+
const its = items(c, decoded2(c, res.payload));
|
|
50757
|
+
const refs = its.flatMap((it) => obj(c, field2(it, "Tracks"))?.fields?.$items ?? []);
|
|
50758
|
+
await untilLoaded(c, refs, 3e3);
|
|
50759
|
+
const tracks2 = its.flatMap((it) => items(c, field2(it, "Tracks"))).filter((t) => field2(t, "Title") !== void 0);
|
|
50760
|
+
const pd = obj(c, field2(m, "PerformerMixDescription"));
|
|
50761
|
+
const head = { key: mixKey(m), name: mixName(c, m) };
|
|
50762
|
+
const img = pd ? imageKey(c, pd, "Photo") ?? imageKey(c, pd, "Avatar") : void 0;
|
|
50763
|
+
if (img) head.image = img;
|
|
50764
|
+
return { mix: head, total: tracks2.length, rows: tracks2.slice(0, Math.min(Math.max(1, limit3), 300)).map((t, i) => {
|
|
50765
|
+
const al = obj(c, field2(t, "Album"));
|
|
50766
|
+
const r = { n: i + 1, key: String(field2(t, "TrackId") ?? ""), title: field2(t, "Title") ?? "", artist: clean(field2(al, "PerformedBy") ?? field2(t, "PerformedBy") ?? ""), album: field2(al, "Title") ?? void 0 };
|
|
50767
|
+
const ak = field2(al, "AlbumId");
|
|
50768
|
+
if (ak !== void 0 && ak !== null) r.album_key = String(ak);
|
|
50769
|
+
const l = field2(t, "LengthSeconds");
|
|
50770
|
+
if (typeof l === "number" && l > 0) r.length = duration3(l);
|
|
50771
|
+
const im = imageKey(c, al);
|
|
50772
|
+
if (im) r.image = im;
|
|
50773
|
+
return r;
|
|
50774
|
+
}) };
|
|
50775
|
+
});
|
|
50776
|
+
}
|
|
50777
|
+
__name(mix, "mix");
|
|
50574
50778
|
async function playMix(key, zoneName, how) {
|
|
50575
50779
|
const p = PRIORITY[how];
|
|
50576
50780
|
const r = await withCore(async (c) => {
|
|
@@ -50660,6 +50864,10 @@ async function playlists(shelfName, genre, mood, limit3, offset2) {
|
|
|
50660
50864
|
const key = playlistKey(p);
|
|
50661
50865
|
seen.set(key, w);
|
|
50662
50866
|
const r = { key, name: String(field2(p, "Name") ?? "").trim() };
|
|
50867
|
+
const imgs = imageKeys(c, p);
|
|
50868
|
+
if (imgs.length) r.images = imgs;
|
|
50869
|
+
const urls = imageUrls(c, p);
|
|
50870
|
+
if (urls.length) r.image_urls = urls;
|
|
50663
50871
|
const d = field2(p, "Description");
|
|
50664
50872
|
if (d) r.description = clean(d).replace(/\s+/g, " ").trim().slice(0, 240);
|
|
50665
50873
|
const n = field2(p, "TrackCount") ?? field2(p, "ItemCount");
|
|
@@ -50705,13 +50913,24 @@ async function findPlaylist2(c, key) {
|
|
|
50705
50913
|
}
|
|
50706
50914
|
__name(findPlaylist2, "findPlaylist");
|
|
50707
50915
|
async function playlists2() {
|
|
50708
|
-
return withCore(async (c) => ({ playlists: allPlaylists(c).list.map((p) =>
|
|
50709
|
-
|
|
50710
|
-
|
|
50711
|
-
|
|
50712
|
-
|
|
50713
|
-
|
|
50714
|
-
|
|
50916
|
+
return withCore(async (c) => ({ playlists: allPlaylists(c).list.map((p) => {
|
|
50917
|
+
const r = {
|
|
50918
|
+
key: playlistKey(p),
|
|
50919
|
+
name: field2(p, "Name") ?? "",
|
|
50920
|
+
kind: PLAYLIST_KIND[field2(p, "Type")] ?? String(field2(p, "Type") ?? ""),
|
|
50921
|
+
tracks: field2(p, "TrackCount") ?? field2(p, "ItemCount") ?? void 0,
|
|
50922
|
+
read_only: field2(p, "IsReadOnly") === true || void 0
|
|
50923
|
+
};
|
|
50924
|
+
const imgs = imageKeys(c, p);
|
|
50925
|
+
if (imgs.length) r.images = imgs;
|
|
50926
|
+
const l = field2(p, "LengthSeconds");
|
|
50927
|
+
if (typeof l === "number" && l > 0) r.length = duration3(l);
|
|
50928
|
+
const names2 = (obj(c, field2(p, "SampleArtistNames"))?.fields?.$items ?? []).filter((x) => typeof x === "string").slice(0, 4);
|
|
50929
|
+
if (names2.length) r.artists = names2;
|
|
50930
|
+
const d = field2(p, "Description");
|
|
50931
|
+
if (d) r.description = clean(d).replace(/\s+/g, " ").slice(0, 240);
|
|
50932
|
+
return r;
|
|
50933
|
+
}) }));
|
|
50715
50934
|
}
|
|
50716
50935
|
__name(playlists2, "playlists");
|
|
50717
50936
|
async function playlist(key, limit3) {
|
|
@@ -50731,7 +50950,14 @@ async function playlist(key, limit3) {
|
|
|
50731
50950
|
return { name: field2(p, "Name"), total: list.length, rows: list.slice(0, n).map((x, i) => {
|
|
50732
50951
|
const tr = obj(c, field2(obj(c, x), "Track"));
|
|
50733
50952
|
const al = obj(c, field2(tr, "Album"));
|
|
50734
|
-
|
|
50953
|
+
const r = { n: i + 1, key: String(field2(tr, "TrackId") ?? ""), title: field2(tr, "Title") ?? null, artist: clean(field2(al, "PerformedBy") ?? ""), album: field2(al, "Title") ?? void 0 };
|
|
50954
|
+
const ak = field2(al, "AlbumId");
|
|
50955
|
+
if (ak !== void 0 && ak !== null) r.album_key = String(ak);
|
|
50956
|
+
const im = imageKey(c, al);
|
|
50957
|
+
if (im) r.image = im;
|
|
50958
|
+
const l = field2(tr, "LengthSeconds");
|
|
50959
|
+
if (typeof l === "number" && l > 0) r.length = duration3(l);
|
|
50960
|
+
return r;
|
|
50735
50961
|
}) };
|
|
50736
50962
|
});
|
|
50737
50963
|
}
|
|
@@ -51248,6 +51474,10 @@ var FOCUS = {
|
|
|
51248
51474
|
picks: external_exports.boolean().optional().describe("Roon's own picks."),
|
|
51249
51475
|
text: external_exports.string().optional().describe("A word in the title or artist."),
|
|
51250
51476
|
favorites: external_exports.boolean().optional(),
|
|
51477
|
+
sample_rate: external_exports.string().optional().describe('Sample rates in kHz, comma-separated: "44.1, 96".'),
|
|
51478
|
+
bits: external_exports.string().optional().describe('Bit depths, comma-separated: "16, 24".'),
|
|
51479
|
+
mqa: external_exports.boolean().optional(),
|
|
51480
|
+
storage: external_exports.enum(["tidal", "qobuz", "local"]).optional().describe("Where the albums live."),
|
|
51251
51481
|
played_within_days: days,
|
|
51252
51482
|
not_played_within_days: days,
|
|
51253
51483
|
added_within_days: days
|
|
@@ -51336,12 +51566,18 @@ function registerLibraryTools(server) {
|
|
|
51336
51566
|
inputSchema: { limit: limit2(30, 8) },
|
|
51337
51567
|
annotations: READ2
|
|
51338
51568
|
}, ({ limit: n }) => run(() => dailyPicks(n ?? 8)));
|
|
51569
|
+
server.registerTool("roon_mix", {
|
|
51570
|
+
title: "A Daily Mix's tracks",
|
|
51571
|
+
description: "The tracks inside one of Roon's Daily Mixes (by `key` or name from roon_mixes), as Roon's mix page shows them before playing \u2014 each with a `key` for roon_play_tracks. To play the whole mix, roon_play_mix.",
|
|
51572
|
+
inputSchema: { key: external_exports.string().describe("A mix key or its name, from roon_mixes."), limit: limit2(300, 100) },
|
|
51573
|
+
annotations: READ2
|
|
51574
|
+
}, ({ key, limit: n }) => run(() => mix(key, n ?? 100)));
|
|
51339
51575
|
server.registerTool("roon_new_for_you", {
|
|
51340
51576
|
title: "New releases for you",
|
|
51341
|
-
description: '"New releases for you" from Roon\'s Home: new albums Roon picked for this user, from the artists and music they listen to \u2014 personal, unlike roon_new (the streaming services\' editorial shelf, the same for everyone). Use it for "my new releases", "what\'s new for me". Albums carry year, format, sometimes a `review`, and a `key
|
|
51342
|
-
inputSchema: { limit: limit2(100, 30), offset },
|
|
51577
|
+
description: '"New releases for you" from Roon\'s Home: new albums Roon picked for this user, from the artists and music they listen to \u2014 personal, unlike roon_new (the streaming services\' editorial shelf, the same for everyone). Use it for "my new releases", "what\'s new for me". Albums carry year, format, `genres`, sometimes a `review`, and a `key`; `genres` at the top counts the shelf\'s top-level genres (the choices for `genre`).',
|
|
51578
|
+
inputSchema: { limit: limit2(100, 30), offset, genre: external_exports.string().optional().describe("Only albums of this genre (a Roon genre name, e.g. Jazz; sub-genres match too).") },
|
|
51343
51579
|
annotations: READ2
|
|
51344
|
-
}, ({ limit: n, offset: o }) => run(() => newForYou(n ?? 30, o ?? 0)));
|
|
51580
|
+
}, ({ limit: n, offset: o, genre }) => run(() => newForYou(n ?? 30, o ?? 0, genre)));
|
|
51345
51581
|
server.registerTool("roon_mixes", {
|
|
51346
51582
|
title: "Daily Mixes",
|
|
51347
51583
|
description: 'Roon\'s own Daily Mixes, from Roon\'s Home \u2014 NOT TIDAL\'s mixes: for "TIDAL mixes", "My Mix", "My Daily Discovery" or TIDAL\'s daily mixes use roon_tidal_playlists with `shelf: "my_mixes"`. These are today\'s personal Roon mixes, each built around an artist (or genre) the user listens to, with a few `touchstones` and a `key` for roon_play_mix. They change every day.',
|
|
@@ -51396,6 +51632,12 @@ function registerLibraryTools(server) {
|
|
|
51396
51632
|
inputSchema: { source: external_exports.enum(["roon", "qobuz", "tidal"]).optional(), limit: limit2(100, 50) },
|
|
51397
51633
|
annotations: READ2
|
|
51398
51634
|
}, ({ source, limit: n }) => run(() => favorites(source ?? "roon", n ?? 50)));
|
|
51635
|
+
server.registerTool("roon_radio_favorite", {
|
|
51636
|
+
title: "A station in My Live Radio",
|
|
51637
|
+
description: "Put a Live Radio station in the user's own list (My Live Radio), or take it out (`on: false`). The `key` comes from roon_radio. Changes the user's library: only on an explicit request. Reads the result back.",
|
|
51638
|
+
inputSchema: { key: external_exports.string(), on: external_exports.boolean().optional() },
|
|
51639
|
+
annotations: CHANGES
|
|
51640
|
+
}, ({ key, on }) => run(() => radioFavorite(key, on !== false)));
|
|
51399
51641
|
server.registerTool("roon_favorite", {
|
|
51400
51642
|
title: "Heart an album or track",
|
|
51401
51643
|
description: "Put a heart on an album or a track in Roon, or take it off (`on: false`). The `key` comes from a search, shelf, album or playlist result. Changes the user's library: only on an explicit request. Reads the heart back and says if Roon didn't change it.",
|
|
@@ -51499,7 +51741,39 @@ async function runSelfTest() {
|
|
|
51499
51741
|
__name(runSelfTest, "runSelfTest");
|
|
51500
51742
|
|
|
51501
51743
|
// src/tools/status.ts
|
|
51744
|
+
init_session();
|
|
51745
|
+
function registerDebugTool(server) {
|
|
51746
|
+
server.registerTool("roon_debug", {
|
|
51747
|
+
title: "Graph fields (development)",
|
|
51748
|
+
description: "Development only: without `type`, the graph's type names with counts (`match` narrows); with `type`, the first `limit` objects of that type with their fields.",
|
|
51749
|
+
inputSchema: { type: external_exports.string().optional(), match: external_exports.string().optional(), limit: external_exports.number().int().min(1).max(200).optional() },
|
|
51750
|
+
annotations: { readOnlyHint: true }
|
|
51751
|
+
}, ({ type, match, limit: limit3 }) => run(() => withCore(async (c) => {
|
|
51752
|
+
const preview = /* @__PURE__ */ __name((v) => {
|
|
51753
|
+
if (v && typeof v === "object" && "$ref" in v) {
|
|
51754
|
+
const o = c.graph.getObject(v.$ref);
|
|
51755
|
+
return { $ref: String(v.$ref), type: o?.typeName };
|
|
51756
|
+
}
|
|
51757
|
+
if (Array.isArray(v)) return v.slice(0, 3).map(preview);
|
|
51758
|
+
if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).slice(0, 6).map(([k, x]) => [k, preview(x)]));
|
|
51759
|
+
return typeof v === "string" ? v.slice(0, 80) : typeof v === "bigint" ? String(v) : v;
|
|
51760
|
+
}, "preview");
|
|
51761
|
+
if (!type) {
|
|
51762
|
+
const counts = /* @__PURE__ */ new Map();
|
|
51763
|
+
for (const o of c.graph.objects.values()) counts.set(o.typeName, (counts.get(o.typeName) ?? 0) + 1);
|
|
51764
|
+
return { types: [...counts].filter(([n]) => !match || n.toLowerCase().includes(match.toLowerCase())).sort((a, b) => b[1] - a[1]).map(([name, n]) => ({ name, n })) };
|
|
51765
|
+
}
|
|
51766
|
+
const all = c.graph.findByType(type);
|
|
51767
|
+
return { count: all.length, objects: all.slice(0, limit3 ?? 3).map((o) => ({
|
|
51768
|
+
oid: String(o.oid),
|
|
51769
|
+
type: o.typeName,
|
|
51770
|
+
fields: Object.fromEntries(Object.entries(o.fields ?? {}).map(([k, v]) => [k.replace(/^.*::/, ""), preview(v)]))
|
|
51771
|
+
})) };
|
|
51772
|
+
})));
|
|
51773
|
+
}
|
|
51774
|
+
__name(registerDebugTool, "registerDebugTool");
|
|
51502
51775
|
function registerStatusTool(server, internal) {
|
|
51776
|
+
if (process.env.DEEP_ROON_DEBUG === "1") registerDebugTool(server);
|
|
51503
51777
|
server.registerTool("roon_status", {
|
|
51504
51778
|
title: "Deep Roon MCP status",
|
|
51505
51779
|
description: "How Deep Roon MCP stands: its version (and whether a newer one is out), the Roon Core it talks to, whether it is enabled in Roon (Settings \u203A Extensions), whether Roon's internal protocol works, and a self-test of a few reads with the tools each one stands for. Use it when a tool fails, or when the user asks whether everything works.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deep-roon-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "An MCP server for Roon that reaches the parts the official API doesn't: reviews, biographies, Qobuz's and TIDAL's shelves, TIDAL's playlists, your history, search across your library and the streaming services",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "René Bouwmeester <21277119+renebouwmeester@users.noreply.github.com>",
|