deep-roon-mcp 0.3.2 → 0.3.8
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 -3
- package/dist/deep-roon-mcp.js +90 -4
- 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.
|
|
25
|
+
Version 0.3: fifty-three tools. Qobuz, TIDAL or both — whatever is signed in on your Core.
|
|
26
26
|
|
|
27
27
|
**Reading** — changes nothing in Roon:
|
|
28
28
|
|
|
@@ -38,7 +38,7 @@ Version 0.2: fifty-one tools. Qobuz, TIDAL or both — whatever is signed in on
|
|
|
38
38
|
| `roon_status` | How Deep Roon MCP stands: Core, pairing, internal protocol, self-test, newer version |
|
|
39
39
|
| `roon_zones` | Roon's zones and groups |
|
|
40
40
|
| `roon_now_playing` | What plays in each zone, with the volume |
|
|
41
|
-
| `roon_queue` | A zone's queue |
|
|
41
|
+
| `roon_queue` | A zone's queue, numbered (1 is what plays) |
|
|
42
42
|
| `roon_cover` | The cover of what plays, as an image |
|
|
43
43
|
| `roon_history` | Roon's play history, newest first |
|
|
44
44
|
| `roon_top` | Most played albums, artists or tracks over your whole history |
|
|
@@ -68,6 +68,7 @@ Version 0.2: fifty-one tools. Qobuz, TIDAL or both — whatever is signed in on
|
|
|
68
68
|
| `roon_seek` | A position in the playing track |
|
|
69
69
|
| `roon_volume` | A zone's volume, mute |
|
|
70
70
|
| `roon_settings` | Shuffle, repeat, Roon Radio |
|
|
71
|
+
| `roon_queue_jump`, `roon_queue_remove` | Jump to an item of the queue, take items out of it |
|
|
71
72
|
| `roon_queue_clear` | Clear what comes after the playing track (or everything) |
|
|
72
73
|
| `roon_transfer` | Move what plays, with its queue, to another zone |
|
|
73
74
|
| `roon_group`, `roon_ungroup` | Group zones, and undo it |
|
|
@@ -104,7 +105,7 @@ The settings (Claude Desktop › Settings › Extensions › Deep Roon MCP):
|
|
|
104
105
|
| Setting | What it does |
|
|
105
106
|
|---|---|
|
|
106
107
|
| Roon Core address | Empty: found on your network. Fill in the Core's address for another subnet or a private VPN (Tailscale) |
|
|
107
|
-
| Essentials only | Seventeen tools instead of fifty-
|
|
108
|
+
| Essentials only | Seventeen tools instead of fifty-three, for small models |
|
|
108
109
|
| Use Roon's internal protocol | Off: only the official API — zones, playback, volume, grouping; no search, reviews, Focus or history |
|
|
109
110
|
|
|
110
111
|
### 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.3.
|
|
15528
|
+
version2 = "0.3.8";
|
|
15529
15529
|
}
|
|
15530
15530
|
});
|
|
15531
15531
|
|
|
@@ -15852,6 +15852,11 @@ 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 imageKey(c, o, name = "Image") {
|
|
15856
|
+
const url2 = field2(obj(c, field2(o, name)), "Url");
|
|
15857
|
+
const m = /^broker:\/\/\/image\/([^.]+)\./.exec(String(url2 ?? ""));
|
|
15858
|
+
return m ? m[1] : void 0;
|
|
15859
|
+
}
|
|
15855
15860
|
function dateParts(v) {
|
|
15856
15861
|
if (!Buffer.isBuffer(v) || v.length < 2) return void 0;
|
|
15857
15862
|
const n = new BinaryReader(Uint8Array.from(v)).flexInt();
|
|
@@ -15953,6 +15958,8 @@ function albumRow(c, o) {
|
|
|
15953
15958
|
if (flag(field2(o, "IsListenLater"))) r.listen_later = true;
|
|
15954
15959
|
const p = field2(o, "Popularity");
|
|
15955
15960
|
if (typeof p === "number") r.popularity = Math.round(p * 100) / 100;
|
|
15961
|
+
const img = imageKey(c, o);
|
|
15962
|
+
if (img) r.image = img;
|
|
15956
15963
|
return r;
|
|
15957
15964
|
}
|
|
15958
15965
|
function resultRow(c, o) {
|
|
@@ -15962,6 +15969,8 @@ function resultRow(c, o) {
|
|
|
15962
15969
|
const r = { kind: "artist", key: String(field2(o, "PerformerId") ?? ""), name: field2(o, "Name") ?? "" };
|
|
15963
15970
|
const n = field2(o, "MainPerformerAlbumCount");
|
|
15964
15971
|
if (typeof n === "number" && n > 0) r.albums_in_library = n;
|
|
15972
|
+
const img = imageKey(c, o, "Photo") ?? imageKey(c, o, "Avatar");
|
|
15973
|
+
if (img) r.image = img;
|
|
15965
15974
|
return r;
|
|
15966
15975
|
}
|
|
15967
15976
|
if (t === "TrackLite" || t === "Track") {
|
|
@@ -15976,6 +15985,8 @@ function resultRow(c, o) {
|
|
|
15976
15985
|
const l = field2(o, "LengthSeconds");
|
|
15977
15986
|
if (typeof l === "number") r.length = duration3(l);
|
|
15978
15987
|
r.source = sourceOf(o, al) ?? "library";
|
|
15988
|
+
const img = imageKey(c, al) ?? imageKey(c, o);
|
|
15989
|
+
if (img) r.image = img;
|
|
15979
15990
|
return r;
|
|
15980
15991
|
}
|
|
15981
15992
|
return { kind: t, title: field2(o, "Title") ?? field2(o, "Name") ?? "" };
|
|
@@ -15996,6 +16007,7 @@ var init_graph = __esm({
|
|
|
15996
16007
|
return (o?.fields?.$items ?? o?.$items ?? []).filter((x) => typeof x === "string");
|
|
15997
16008
|
}, "strings");
|
|
15998
16009
|
flag = /* @__PURE__ */ __name((v) => v === true || Buffer.isBuffer(v) && v.length > 1 && v[0] >= 1, "flag");
|
|
16010
|
+
__name(imageKey, "imageKey");
|
|
15999
16011
|
__name(dateParts, "dateParts");
|
|
16000
16012
|
yearOf = /* @__PURE__ */ __name((v) => dateParts(v)?.[0], "yearOf");
|
|
16001
16013
|
__name(dateText, "dateText");
|
|
@@ -16341,7 +16353,12 @@ async function bio(key, max, wiki) {
|
|
|
16341
16353
|
for (const n of names2) for (const x of list(n)) {
|
|
16342
16354
|
const o = obj(c, x);
|
|
16343
16355
|
const name = field2(o, "Name");
|
|
16344
|
-
if (name && !out.some((y) => y.name === name))
|
|
16356
|
+
if (name && !out.some((y) => y.name === name)) {
|
|
16357
|
+
const r = { key: String(field2(o, "PerformerId") ?? "") || void 0, name };
|
|
16358
|
+
const img2 = imageKey(c, o, "Photo") ?? imageKey(c, o, "Avatar");
|
|
16359
|
+
if (img2) r.image = img2;
|
|
16360
|
+
out.push(r);
|
|
16361
|
+
}
|
|
16345
16362
|
}
|
|
16346
16363
|
return out.length ? out.slice(0, 12) : void 0;
|
|
16347
16364
|
}, "people");
|
|
@@ -16357,6 +16374,8 @@ async function bio(key, max, wiki) {
|
|
|
16357
16374
|
}
|
|
16358
16375
|
const g = list("Genres").filter((x) => typeof x === "string");
|
|
16359
16376
|
if (g.length) artist.genres = g;
|
|
16377
|
+
const img = imageKey(c, p, "Photo") ?? imageKey(c, p, "Avatar");
|
|
16378
|
+
if (img) artist.image = img;
|
|
16360
16379
|
return {
|
|
16361
16380
|
artist,
|
|
16362
16381
|
texts,
|
|
@@ -16388,6 +16407,8 @@ async function discography(key, wanted) {
|
|
|
16388
16407
|
if (typeof n === "number") artist.albums_in_library = n;
|
|
16389
16408
|
const from = field2(p, "BirthPlace");
|
|
16390
16409
|
if (from) artist.from = from;
|
|
16410
|
+
const img = imageKey(c, p, "Photo") ?? imageKey(c, p, "Avatar");
|
|
16411
|
+
if (img) artist.image = img;
|
|
16391
16412
|
return { service: SERVICE_NAME[service2], artist, albums };
|
|
16392
16413
|
});
|
|
16393
16414
|
}
|
|
@@ -49281,7 +49302,15 @@ async function history(limit3) {
|
|
|
49281
49302
|
closeSoon("after the history");
|
|
49282
49303
|
return {
|
|
49283
49304
|
total: out.total ?? null,
|
|
49284
|
-
plays: out.events.map((e) => ({
|
|
49305
|
+
plays: out.events.map((e) => ({
|
|
49306
|
+
at: localTime(e.playedAt),
|
|
49307
|
+
title: e.title,
|
|
49308
|
+
artist: clean(e.artist),
|
|
49309
|
+
album: e.album ?? void 0,
|
|
49310
|
+
played_pct: e.completionPct ?? void 0,
|
|
49311
|
+
track_key: e.roonTrackId ?? void 0
|
|
49312
|
+
}))
|
|
49313
|
+
// for roon_play_tracks (13 Oct 2026, Rosso's history)
|
|
49285
49314
|
};
|
|
49286
49315
|
});
|
|
49287
49316
|
}
|
|
@@ -49384,6 +49413,8 @@ async function nowPlaying() {
|
|
|
49384
49413
|
r.volume_max = v.max;
|
|
49385
49414
|
if (v.is_muted) r.muted = true;
|
|
49386
49415
|
}
|
|
49416
|
+
const img = off?.now_playing?.image_key;
|
|
49417
|
+
if (img) r.image = img;
|
|
49387
49418
|
const outs = c.graph.findByType("Endpoint").filter((e) => {
|
|
49388
49419
|
const zz = field2(e, "Zone");
|
|
49389
49420
|
return isRef2(zz) && String(zz.$ref) === String(z2.oid);
|
|
@@ -49405,6 +49436,10 @@ async function nowPlaying() {
|
|
|
49405
49436
|
r.title = field2(tr, "Title");
|
|
49406
49437
|
r.artist = clean(field2(al, "PerformedBy"));
|
|
49407
49438
|
r.album = field2(al, "Title");
|
|
49439
|
+
const tk = field2(tr, "TrackId");
|
|
49440
|
+
if (tk !== void 0 && tk !== null) r.track_key = String(tk);
|
|
49441
|
+
const ak = field2(al, "AlbumId");
|
|
49442
|
+
if (ak !== void 0 && ak !== null) r.album_key = String(ak);
|
|
49408
49443
|
const f = field2(tr, "Format");
|
|
49409
49444
|
if (f) r.format = f;
|
|
49410
49445
|
const l = field2(tr, "LengthSeconds");
|
|
@@ -49661,7 +49696,7 @@ function registerReadTools(server, internal = true) {
|
|
|
49661
49696
|
const reg = /* @__PURE__ */ __name(((name, cfg, cb) => R(name) ? server.registerTool.call(server, name, cfg, cb) : void 0), "reg");
|
|
49662
49697
|
reg("roon_search", {
|
|
49663
49698
|
title: "Search Roon",
|
|
49664
|
-
description: 'Search Roon \u2014 the user\'s Roon library and the catalogues of the streaming services signed in (Qobuz, TIDAL) at once; each row says its `source` (library, Qobuz, TIDAL). Returns albums (with format, e.g. FLAC 96kHz 24bit), artists and tracks. `kind` narrows to album, track or artist \u2014 use `kind: "track"` with the song title (and artist) to find one specific track; a broad query without `kind` can fill up with artists. Albums and tracks carry a `key` for `roon_album`; artists carry a `key` for `roon_artist` and `roon_bio`. Albums say their `year`, and `in_library` when the user has them in Roon. Keys stay valid between calls.',
|
|
49699
|
+
description: 'Search Roon \u2014 the user\'s Roon library and the catalogues of the streaming services signed in (Qobuz, TIDAL) at once; each row says its `source` (library, Qobuz, TIDAL). Rows carry `image`, the key of the cover: the Core serves it at http://<core address>:9330/image/<key>.jpg, or scaled at /image/<key>.300.jpg (for clients that draw; a chat shows covers with roon_cover). The `image` of roon_now_playing is an official key, served at /api/image/<key>?scale=fit&width=600&height=600. Returns albums (with format, e.g. FLAC 96kHz 24bit), artists and tracks. `kind` narrows to album, track or artist \u2014 use `kind: "track"` with the song title (and artist) to find one specific track; a broad query without `kind` can fill up with artists. Albums and tracks carry a `key` for `roon_album`; artists carry a `key` for `roon_artist` and `roon_bio`. Albums say their `year`, and `in_library` when the user has them in Roon. Keys stay valid between calls.',
|
|
49665
49700
|
inputSchema: { query: external_exports.string().min(1), kind: external_exports.enum(["album", "track", "artist"]).optional(), limit: limit(100, 30) },
|
|
49666
49701
|
annotations: READ
|
|
49667
49702
|
}, ({ query, kind, limit: n }) => run(() => search(query, n ?? 30, kind)));
|
|
@@ -49958,9 +49993,47 @@ function queueRow(c, oid) {
|
|
|
49958
49993
|
if (album2) r.album = album2;
|
|
49959
49994
|
const l = field2(tr, "LengthSeconds");
|
|
49960
49995
|
if (typeof l === "number") r.length = duration3(l);
|
|
49996
|
+
const k = field2(tr, "TrackId");
|
|
49997
|
+
if (k !== void 0 && k !== null) r.key = String(k);
|
|
49998
|
+
const img = imageKey(c, al) ?? imageKey(c, tr);
|
|
49999
|
+
if (img) r.image = img;
|
|
49961
50000
|
return r;
|
|
49962
50001
|
}
|
|
49963
50002
|
__name(queueRow, "queueRow");
|
|
50003
|
+
async function queueItemAt(c, zoneName, n) {
|
|
50004
|
+
const { z: z2, q } = queueOf(c, zoneName);
|
|
50005
|
+
const refs = await queueItems(c, q);
|
|
50006
|
+
const at = Math.max(0, field2(c.graph.getObject(q.oid), "CurrentItemIndex") ?? 0);
|
|
50007
|
+
const oid = refs[at + n - 1];
|
|
50008
|
+
if (oid === void 0) throw new RoonError(`The queue of ${z2.name} has no item ${n} (${Math.max(0, refs.length - at)} from here)`, "invalid");
|
|
50009
|
+
return { z: z2, q, oid, refs, at };
|
|
50010
|
+
}
|
|
50011
|
+
__name(queueItemAt, "queueItemAt");
|
|
50012
|
+
async function queueJump(zoneName, n) {
|
|
50013
|
+
const r = await withCore(async (c) => {
|
|
50014
|
+
const { z: z2, q, oid } = await queueItemAt(c, zoneName, n);
|
|
50015
|
+
c.remoting.callMethodNoReply(q.oid, formatMethodSignature("Queue", "PlayFromHere", [{ type: "TransportItem", name: "item" }]), buildArgs([Arg.ref(oid)]));
|
|
50016
|
+
log(`queue_jump: ${z2.name} \u2192 item ${n}`);
|
|
50017
|
+
return { zone: z2.name, n };
|
|
50018
|
+
});
|
|
50019
|
+
return { ok: true, ...r, ...await started2(r.zone, PRIORITY["now"]) };
|
|
50020
|
+
}
|
|
50021
|
+
__name(queueJump, "queueJump");
|
|
50022
|
+
async function queueRemove(zoneName, ns) {
|
|
50023
|
+
return withCore(async (c) => {
|
|
50024
|
+
const { z: z2, q } = queueOf(c, zoneName);
|
|
50025
|
+
const refs = await queueItems(c, q);
|
|
50026
|
+
const at = Math.max(0, field2(c.graph.getObject(q.oid), "CurrentItemIndex") ?? 0);
|
|
50027
|
+
const oids = [...new Set(ns)].map((n) => refs[at + n - 1]).filter((o) => o !== void 0);
|
|
50028
|
+
if (!oids.length) throw new RoonError("No such items in the queue", "invalid");
|
|
50029
|
+
c.remoting.callMethodNoReply(q.oid, formatMethodSignature("Queue", "RemoveItems", [{ type: "IEnumerable<TransportItem>", name: "items" }]), buildArgs([Arg.collection(oids.map((o) => buildArgs([Arg.ref(o)])))]));
|
|
50030
|
+
log(`queue_remove: ${z2.name} \u2212 ${oids.length} item(s)`);
|
|
50031
|
+
await wait(300);
|
|
50032
|
+
const after = await queueItems(c, q);
|
|
50033
|
+
return { ok: true, zone: z2.name, removed: oids.length, left: Math.max(0, after.length - at) };
|
|
50034
|
+
});
|
|
50035
|
+
}
|
|
50036
|
+
__name(queueRemove, "queueRemove");
|
|
49964
50037
|
async function queue2(zoneName, limit3) {
|
|
49965
50038
|
return withCore(async (c) => {
|
|
49966
50039
|
const { z: z2, q } = queueOf(c, zoneName);
|
|
@@ -50117,6 +50190,7 @@ __name(cover2, "cover");
|
|
|
50117
50190
|
// src/tools/control.ts
|
|
50118
50191
|
init_official();
|
|
50119
50192
|
init_session();
|
|
50193
|
+
var CHANGES_QUEUE = { readOnlyHint: false, destructiveHint: true, openWorldHint: false };
|
|
50120
50194
|
var PLAYS = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
|
|
50121
50195
|
var REMOVES = { readOnlyHint: false, destructiveHint: true, openWorldHint: false };
|
|
50122
50196
|
var zone = external_exports.string().describe("A Roon zone name (see roon_zones).");
|
|
@@ -50169,6 +50243,18 @@ function registerControlTools(server, internal = true) {
|
|
|
50169
50243
|
inputSchema: { zone, limit: external_exports.number().int().min(1).max(200).optional() },
|
|
50170
50244
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
50171
50245
|
}, ({ zone: zn, limit: limit3 }) => run(() => queue2(zn, limit3 ?? 50)));
|
|
50246
|
+
reg("roon_queue_jump", {
|
|
50247
|
+
title: "Jump in the queue",
|
|
50248
|
+
description: "Play an item of the zone's queue now (`n` as roon_queue numbers them: 1 is what plays, 2 the next \u2026); what came before it is done. Only on an explicit request.",
|
|
50249
|
+
inputSchema: { zone, n: external_exports.number().int().min(1) },
|
|
50250
|
+
annotations: PLAYS
|
|
50251
|
+
}, ({ zone: zn, n }) => run(() => queueJump(zn, n)));
|
|
50252
|
+
reg("roon_queue_remove", {
|
|
50253
|
+
title: "Remove from the queue",
|
|
50254
|
+
description: "Take items out of the zone's queue, by their numbers from roon_queue (`ns`, e.g. [3, 4]). Only on an explicit request.",
|
|
50255
|
+
inputSchema: { zone, ns: external_exports.array(external_exports.number().int().min(2)).min(1).max(100).describe("Positions from roon_queue (2 and up: 1 is what plays).") },
|
|
50256
|
+
annotations: CHANGES_QUEUE
|
|
50257
|
+
}, ({ zone: zn, ns }) => run(() => queueRemove(zn, ns)));
|
|
50172
50258
|
reg("roon_queue_clear", {
|
|
50173
50259
|
title: "Clear the queue",
|
|
50174
50260
|
description: "Clear the queue of a Roon zone. By default the track that plays keeps playing and everything after it is removed; `everything: true` also removes the playing track (the zone falls silent). Only on an explicit request.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deep-roon-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
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>",
|