deep-roon-mcp 0.2.0 → 0.2.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 CHANGED
@@ -179,6 +179,8 @@ status; it holds no secrets).
179
179
 
180
180
  - Short connections, as Roon Labs asked: opened for a question, closed after two minutes of quiet, one at a time, questions
181
181
  one after another; a PING every 10 s while open, so the Core doesn't drop the session.
182
+ - Several clients at once (Claude Desktop and Claude Code, or ChatGPT next to them) work: Roon keeps one connection
183
+ per extension, and the client that asks a question takes it over for that moment.
182
184
  - Album, track and artist keys stay valid between calls.
183
185
  - The log goes to stderr (your MCP client keeps it).
184
186
  - At the start it asks GitHub once whether a newer release is out (`DEEP_ROON_NO_UPDATE_CHECK=1` turns that off). Nothing
@@ -5340,7 +5340,7 @@ var require_core2 = __commonJS({
5340
5340
  Object.defineProperty(exports2, "__esModule", { value: true });
5341
5341
  var id_1 = require_id();
5342
5342
  var ref_1 = require_ref();
5343
- var core = [
5343
+ var core2 = [
5344
5344
  "$schema",
5345
5345
  "$id",
5346
5346
  "$defs",
@@ -5350,7 +5350,7 @@ var require_core2 = __commonJS({
5350
5350
  id_1.default,
5351
5351
  ref_1.default
5352
5352
  ];
5353
- exports2.default = core;
5353
+ exports2.default = core2;
5354
5354
  }
5355
5355
  });
5356
5356
 
@@ -13239,7 +13239,7 @@ var require_stream = __commonJS({
13239
13239
  };
13240
13240
  duplex._final = function(callback) {
13241
13241
  if (ws.readyState === ws.CONNECTING) {
13242
- ws.once("open", /* @__PURE__ */ __name(function open3() {
13242
+ ws.once("open", /* @__PURE__ */ __name(function open4() {
13243
13243
  duplex._final(callback);
13244
13244
  }, "open"));
13245
13245
  return;
@@ -13260,7 +13260,7 @@ var require_stream = __commonJS({
13260
13260
  };
13261
13261
  duplex._write = function(chunk, encoding, callback) {
13262
13262
  if (ws.readyState === ws.CONNECTING) {
13263
- ws.once("open", /* @__PURE__ */ __name(function open3() {
13263
+ ws.once("open", /* @__PURE__ */ __name(function open4() {
13264
13264
  duplex._write(chunk, encoding, callback);
13265
13265
  }, "open"));
13266
13266
  return;
@@ -14277,7 +14277,7 @@ var require_moomsg = __commonJS({
14277
14277
  var require_core3 = __commonJS({
14278
14278
  "node_modules/node-roon-api/core.js"(exports2, module2) {
14279
14279
  "use strict";
14280
- function Core(moo, roon, registration) {
14280
+ function Core(moo, roon2, registration) {
14281
14281
  this.moo = moo;
14282
14282
  this.core_id = registration.core_id;
14283
14283
  this.display_name = registration.display_name;
@@ -14285,8 +14285,8 @@ var require_core3 = __commonJS({
14285
14285
  this.registration = registration;
14286
14286
  this.services = {};
14287
14287
  var svcs = {};
14288
- roon.services_opts.required_services.forEach((svcobj) => svcobj.services.forEach((svc) => svcs[svc.name] = svcobj));
14289
- roon.services_opts.optional_services.forEach((svcobj) => svcobj.services.forEach((svc) => svcs[svc.name] = svcobj));
14288
+ roon2.services_opts.required_services.forEach((svcobj) => svcobj.services.forEach((svc) => svcs[svc.name] = svcobj));
14289
+ roon2.services_opts.optional_services.forEach((svcobj) => svcobj.services.forEach((svc) => svcs[svc.name] = svcobj));
14290
14290
  registration.provided_services.forEach((e) => {
14291
14291
  this.services[svcs[e].name] = new svcs[e](this);
14292
14292
  });
@@ -14903,24 +14903,24 @@ var require_lib = __commonJS({
14903
14903
  });
14904
14904
  this.pairing_service_1 = {
14905
14905
  services: [svc],
14906
- found_core: /* @__PURE__ */ __name((core) => {
14906
+ found_core: /* @__PURE__ */ __name((core2) => {
14907
14907
  if (!this.paired_core_id) {
14908
14908
  let settings3 = this.get_persisted_state();
14909
- settings3.paired_core_id = core.core_id;
14909
+ settings3.paired_core_id = core2.core_id;
14910
14910
  this.set_persisted_state(settings3);
14911
- this.paired_core_id = core.core_id;
14912
- this.paired_core = core;
14911
+ this.paired_core_id = core2.core_id;
14912
+ this.paired_core = core2;
14913
14913
  this.is_paired = true;
14914
14914
  svc.send_continue_all("subscribe_pairing", "Changed", { paired_core_id: this.paired_core_id });
14915
14915
  }
14916
- if (core.core_id == this.paired_core_id) {
14917
- if (this.extension_opts.core_paired) this.extension_opts.core_paired(core);
14916
+ if (core2.core_id == this.paired_core_id) {
14917
+ if (this.extension_opts.core_paired) this.extension_opts.core_paired(core2);
14918
14918
  }
14919
14919
  }, "found_core"),
14920
- lost_core: /* @__PURE__ */ __name((core) => {
14921
- if (core.core_id == this.paired_core_id)
14920
+ lost_core: /* @__PURE__ */ __name((core2) => {
14921
+ if (core2.core_id == this.paired_core_id)
14922
14922
  this.is_paired = false;
14923
- if (this.extension_opts.core_unpaired) this.extension_opts.core_unpaired(core);
14923
+ if (this.extension_opts.core_unpaired) this.extension_opts.core_unpaired(core2);
14924
14924
  }, "lost_core")
14925
14925
  };
14926
14926
  o.provided_services.push(this.pairing_service_1);
@@ -15113,8 +15113,8 @@ var require_lib2 = __commonJS({
15113
15113
  return zo.zone_id;
15114
15114
  }
15115
15115
  __name(zoid, "zoid");
15116
- function RoonApiTransport2(core) {
15117
- this.core = core;
15116
+ function RoonApiTransport2(core2) {
15117
+ this.core = core2;
15118
15118
  this._queues = {};
15119
15119
  }
15120
15120
  __name(RoonApiTransport2, "RoonApiTransport");
@@ -15416,7 +15416,7 @@ var require_lib2 = __commonJS({
15416
15416
  var version2;
15417
15417
  var init_package = __esm({
15418
15418
  "package.json"() {
15419
- version2 = "0.2.0";
15419
+ version2 = "0.2.2";
15420
15420
  }
15421
15421
  });
15422
15422
 
@@ -15443,7 +15443,8 @@ function configDir() {
15443
15443
  function startOfficial(target2) {
15444
15444
  if (started) return;
15445
15445
  started = true;
15446
- const roon = new import_node_roon_api.default({
15446
+ core = target2;
15447
+ roon = new import_node_roon_api.default({
15447
15448
  extension_id: "nl.renebouwmeester.deep-roon-mcp",
15448
15449
  display_name: "Deep Roon MCP",
15449
15450
  display_version: version2,
@@ -15453,13 +15454,13 @@ function startOfficial(target2) {
15453
15454
  log_level: "none",
15454
15455
  get_persisted_state: readState,
15455
15456
  set_persisted_state: writeState,
15456
- core_paired(core) {
15457
- transport = core.services.RoonApiTransport ?? Object.entries(core.services ?? {}).find(([k]) => k.startsWith("RoonApiTransport"))?.[1];
15457
+ core_paired(core2) {
15458
+ transport = core2.services.RoonApiTransport ?? Object.entries(core2.services ?? {}).find(([k]) => k.startsWith("RoonApiTransport"))?.[1];
15458
15459
  if (!transport) {
15459
15460
  log("paired, but the Core offers no transport service");
15460
15461
  return;
15461
15462
  }
15462
- log(`paired with ${core.display_name} (Roon \u203A Settings \u203A Extensions)`);
15463
+ log(`paired with ${core2.display_name} (Roon \u203A Settings \u203A Extensions)`);
15463
15464
  transport.subscribe_zones((cmd, data) => {
15464
15465
  if (cmd === "Subscribed") {
15465
15466
  zones.clear();
@@ -15490,25 +15491,26 @@ function startOfficial(target2) {
15490
15491
  }
15491
15492
  });
15492
15493
  roon.init_services({ required_services: [import_node_roon_api_transport.default] });
15493
- let tries = 0;
15494
- const connect = /* @__PURE__ */ __name(() => {
15495
- roon.ws_connect({
15496
- host: target2.host,
15497
- port: target2.port,
15498
- onclose: /* @__PURE__ */ __name(() => {
15499
- transport = null;
15500
- zones.clear();
15501
- zonesIn = false;
15502
- const delay = [5, 15, 60][tries] ?? 300;
15503
- tries += 1;
15504
- setTimeout(connect, delay * 1e3).unref();
15505
- }, "onclose")
15506
- });
15507
- }, "connect");
15508
15494
  connect();
15509
15495
  }
15496
+ function connect() {
15497
+ if (open2 || !roon || !core) return;
15498
+ open2 = true;
15499
+ roon.ws_connect({
15500
+ host: core.host,
15501
+ port: core.port,
15502
+ onclose: /* @__PURE__ */ __name(() => {
15503
+ open2 = false;
15504
+ transport = null;
15505
+ zones.clear();
15506
+ zonesIn = false;
15507
+ log("the extension connection closed (another copy of Deep Roon MCP may have taken it over) \u2014 it opens again when a tool needs it");
15508
+ }, "onclose")
15509
+ });
15510
+ }
15510
15511
  async function paired(ms = 8e3) {
15511
15512
  if (transport) return transport;
15513
+ connect();
15512
15514
  await new Promise((resolve) => {
15513
15515
  waiting.push(resolve);
15514
15516
  setTimeout(resolve, ms).unref();
@@ -15547,7 +15549,7 @@ async function setVolume(name, change) {
15547
15549
  }
15548
15550
  return { zone: z2.display_name, outputs: outs.map((o) => ({ output: o.display_name, scale: `${o.volume.min}\u2013${o.volume.max}` })), volume: change.value, muted: change.mute };
15549
15551
  }
15550
- var import_fs, import_os, import_path, import_node_roon_api, import_node_roon_api_transport, STATE_FILE, readState, writeState, transport, zones, started, waiting, zonesIn, zoneWaiting, officialZones, isPaired;
15552
+ var import_fs, import_os, import_path, import_node_roon_api, import_node_roon_api_transport, STATE_FILE, readState, writeState, transport, zones, started, waiting, zonesIn, zoneWaiting, roon, core, open2, officialZones, isPaired;
15551
15553
  var init_official = __esm({
15552
15554
  "src/core/official.ts"() {
15553
15555
  "use strict";
@@ -15578,7 +15580,11 @@ var init_official = __esm({
15578
15580
  waiting = [];
15579
15581
  zonesIn = false;
15580
15582
  zoneWaiting = [];
15583
+ roon = null;
15584
+ core = null;
15585
+ open2 = false;
15581
15586
  __name(startOfficial, "startOfficial");
15587
+ __name(connect, "connect");
15582
15588
  __name(paired, "paired");
15583
15589
  officialZones = /* @__PURE__ */ __name(() => [...zones.values()], "officialZones");
15584
15590
  isPaired = /* @__PURE__ */ __name(() => !!transport, "isPaired");
@@ -15637,7 +15643,7 @@ function close(reason) {
15637
15643
  log(`connection closed (${reason})`);
15638
15644
  }
15639
15645
  }
15640
- async function open2() {
15646
+ async function open3() {
15641
15647
  if (client) return client;
15642
15648
  const t = await findTarget();
15643
15649
  await paired();
@@ -15678,7 +15684,7 @@ function withCore(work2) {
15678
15684
  idleTimer = null;
15679
15685
  }
15680
15686
  try {
15681
- return await work2(await open2());
15687
+ return await work2(await open3());
15682
15688
  } catch (e) {
15683
15689
  if (!(e instanceof RoonError && (e.kind === "not_found" || e.kind === "invalid"))) close("after an error");
15684
15690
  throw e;
@@ -15692,7 +15698,7 @@ function withCore(work2) {
15692
15698
  }
15693
15699
  async function fresh() {
15694
15700
  close("a fresh graph");
15695
- return open2();
15701
+ return open3();
15696
15702
  }
15697
15703
  function closeSoon(reason) {
15698
15704
  close(reason);
@@ -15722,7 +15728,7 @@ var init_session = __esm({
15722
15728
  __name(findTarget, "findTarget");
15723
15729
  __name(locate, "locate");
15724
15730
  __name(close, "close");
15725
- __name(open2, "open");
15731
+ __name(open3, "open");
15726
15732
  __name(withCore, "withCore");
15727
15733
  __name(fresh, "fresh");
15728
15734
  __name(closeSoon, "closeSoon");
@@ -48809,7 +48815,7 @@ function registerLibraryTools(server) {
48809
48815
  }, ({ limit: n, offset: o }) => run(() => newForYou(n ?? 30, o ?? 0)));
48810
48816
  server.registerTool("roon_mixes", {
48811
48817
  title: "Daily Mixes",
48812
- description: "The Daily Mixes from Roon's Home: today's personal 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.",
48818
+ 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.',
48813
48819
  inputSchema: {},
48814
48820
  annotations: READ2
48815
48821
  }, () => run(() => mixes()));
@@ -48827,7 +48833,7 @@ function registerLibraryTools(server) {
48827
48833
  }, () => run(() => playlists2()));
48828
48834
  server.registerTool("roon_tidal_playlists", {
48829
48835
  title: "TIDAL playlists",
48830
- description: "TIDAL's playlists as Roon shows them, when TIDAL is signed in: the editorial shelves `new`, `recommended`, `exclusive`, `masters`, `popular`, `essential`, `hires_classics`, `recently_updated`, `forgotten_favorites`; the user's own TIDAL mixes `my_mixes` (My Daily Discovery, My Mix 1 \u2026) and `history_mixes` (My Most Listened, the yearly ones \u2026); or the playlists of one TIDAL `genre` or `mood` (ids from roon_genres with `service: \"tidal\"`). Each playlist has a `key` for roon_playlist (its tracks) and roon_play_playlist, a description, the number of tracks and a few of its artists.",
48836
+ description: "TIDAL's playlists as Roon shows them, when TIDAL is signed in: the editorial shelves `new`, `recommended`, `exclusive`, `masters`, `popular`, `essential`, `hires_classics`, `recently_updated`, `forgotten_favorites`; the user's own TIDAL mixes `my_mixes` (My Daily Discovery, My Mix 1 \u2026, My New Arrivals \u2014 what the user means by their TIDAL (daily) mixes; not roon_mixes, which are Roon's) and `history_mixes` (My Most Listened, the yearly ones \u2026); or the playlists of one TIDAL `genre` or `mood` (ids from roon_genres with `service: \"tidal\"`). Each playlist has a `key` for roon_playlist (its tracks \u2014 readable without playing) and roon_play_playlist, a description, the number of tracks and a few of its artists.",
48831
48837
  inputSchema: {
48832
48838
  shelf: external_exports.enum(Object.keys(PLAYLIST_SHELVES)).optional().describe("Default new."),
48833
48839
  genre: external_exports.string().optional().describe("A TIDAL genre (roon_genres, service tidal)."),
@@ -48972,7 +48978,7 @@ function registerStatusTool(server, internal) {
48972
48978
  annotations: { readOnlyHint: true, openWorldHint: false }
48973
48979
  }, () => run(async () => {
48974
48980
  const t = await findTarget().catch((e) => ({ error: e.message }));
48975
- const core = "error" in t ? { error: t.error } : { name: t.info.name, version: t.info.version, address: t.host };
48981
+ const core2 = "error" in t ? { error: t.error } : { name: t.info.name, version: t.info.version, address: t.host };
48976
48982
  if (!("error" in t)) await paired(5e3).catch(() => null);
48977
48983
  const paired2 = isPaired();
48978
48984
  if (internal && paired2 && !selfTestResults()) await selfTest(runSelfTest);
@@ -48980,7 +48986,7 @@ function registerStatusTool(server, internal) {
48980
48986
  return {
48981
48987
  deep_roon_mcp: version2,
48982
48988
  newer_version: updateInfo() ?? void 0,
48983
- core,
48989
+ core: core2,
48984
48990
  enabled_in_roon: paired2 || 'not yet: Roon \u203A Settings \u203A Extensions \u203A Enable next to "Deep Roon MCP"',
48985
48991
  internal_protocol: internal ? checks2 ? checks2.every((c) => c.ok) ? "works" : "partly broken \u2014 see self_test" : "not tested yet" : "switched off (only the official API)",
48986
48992
  self_test: checks2 ?? void 0
@@ -48998,6 +49004,7 @@ var INSTRUCTIONS = `Deep Roon MCP connects to the user's own Roon Core (music pl
48998
49004
  - Albums, tracks and artists come with a \`key\`; keys stay valid between calls \u2014 no need to search again.
48999
49005
  - Reviews and biographies are Roon's own texts: when asked for them, show them in full with the author's name.
49000
49006
  - What plays changes all the time: read roon_now_playing afresh before describing it.
49007
+ - "Daily Mixes" are Roon's own (roon_mixes); the user's TIDAL mixes (My Mix 1 \u2026, My Daily Discovery) are roon_tidal_playlists with shelf my_mixes, and their tracks can be read with roon_playlist.
49001
49008
  - Unofficial: this uses Roon's internal protocol, which a Roon update can change; if a tool says so, tell the user to check for an update.`;
49002
49009
  async function main() {
49003
49010
  const server = new McpServer({ name: "deep-roon-mcp", title: "Deep Roon MCP", version: version2, websiteUrl: "https://github.com/renebouwmeester/deep-roon-mcp" }, { instructions: INSTRUCTIONS });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deep-roon-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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>",