@rocksky/cli 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,9 +4,12 @@ import fs from 'fs';
4
4
  import os from 'os';
5
5
  import path from 'path';
6
6
  import fs$1 from 'fs/promises';
7
- import md5 from 'md5';
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
+ import { z } from 'zod';
8
10
  import dayjs from 'dayjs';
9
11
  import relative from 'dayjs/plugin/relativeTime.js';
12
+ import md5 from 'md5';
10
13
  import { table, getBorderCharacters } from 'table';
11
14
  import { Command } from 'commander';
12
15
  import axios from 'axios';
@@ -298,7 +301,7 @@ class RockskyClient {
298
301
  }
299
302
  }
300
303
 
301
- async function albums(did, { skip, limit }) {
304
+ async function albums$1(did, { skip, limit }) {
302
305
  const client = new RockskyClient();
303
306
  const albums2 = await client.getAlbums(did, { skip, limit });
304
307
  let rank = 1;
@@ -312,7 +315,7 @@ async function albums(did, { skip, limit }) {
312
315
  }
313
316
  }
314
317
 
315
- async function artists(did, { skip, limit }) {
318
+ async function artists$1(did, { skip, limit }) {
316
319
  const client = new RockskyClient();
317
320
  const artists2 = await client.getArtists(did, { skip, limit });
318
321
  let rank = 1;
@@ -326,7 +329,7 @@ async function artists(did, { skip, limit }) {
326
329
  }
327
330
  }
328
331
 
329
- async function createApiKey(name, { description }) {
332
+ async function createApiKey$1(name, { description }) {
330
333
  const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
331
334
  try {
332
335
  await fs$1.access(tokenPath);
@@ -363,6 +366,527 @@ async function createApiKey(name, { description }) {
363
366
  console.log(`Secret: ${chalk.greenBright(apikey.shared_secret)}`);
364
367
  }
365
368
 
369
+ async function albums(did, { skip, limit = 20 }) {
370
+ const client = new RockskyClient();
371
+ const albums2 = await client.getAlbums(did, { skip, limit });
372
+ let rank = 1;
373
+ let response = `Top ${limit} albums:
374
+ `;
375
+ for (const album of albums2) {
376
+ response += `${rank} ${album.title} - ${album.artist} - ${album.play_count} plays
377
+ `;
378
+ rank++;
379
+ }
380
+ return response;
381
+ }
382
+
383
+ async function artists(did, { skip, limit = 20 }) {
384
+ try {
385
+ const client = new RockskyClient();
386
+ const artists2 = await client.getArtists(did, { skip, limit });
387
+ let rank = 1;
388
+ let response = `Top ${limit} artists:
389
+ `;
390
+ for (const artist of artists2) {
391
+ response += `${rank} ${artist.name} - ${artist.play_count} plays
392
+ `;
393
+ rank++;
394
+ }
395
+ return response;
396
+ } catch (err) {
397
+ return `Failed to fetch artists data. Please check your token and try again, error: ${err.message}`;
398
+ }
399
+ }
400
+
401
+ async function createApiKey(name, { description }) {
402
+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
403
+ try {
404
+ await fs$1.access(tokenPath);
405
+ } catch (err) {
406
+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
407
+ }
408
+ const tokenData = await fs$1.readFile(tokenPath, "utf-8");
409
+ const { token } = JSON.parse(tokenData);
410
+ if (!token) {
411
+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
412
+ }
413
+ const client = new RockskyClient(token);
414
+ const apikey = await client.createApiKey(name, description);
415
+ if (!apikey) {
416
+ return "Failed to create API key. Please try again later.";
417
+ }
418
+ return "API key created successfully!, navigate to your Rocksky account to view it.";
419
+ }
420
+
421
+ dayjs.extend(relative);
422
+ async function myscrobbles({ skip, limit }) {
423
+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
424
+ try {
425
+ await fs$1.access(tokenPath);
426
+ } catch (err) {
427
+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
428
+ }
429
+ const tokenData = await fs$1.readFile(tokenPath, "utf-8");
430
+ const { token } = JSON.parse(tokenData);
431
+ if (!token) {
432
+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
433
+ }
434
+ const client = new RockskyClient(token);
435
+ try {
436
+ const { did } = await client.getCurrentUser();
437
+ const scrobbles = await client.scrobbles(did, { skip, limit });
438
+ return JSON.stringify(
439
+ scrobbles.map((scrobble) => ({
440
+ title: scrobble.title,
441
+ artist: scrobble.artist,
442
+ date: dayjs(scrobble.created_at + "Z").fromNow(),
443
+ isoDate: scrobble.created_at
444
+ })),
445
+ null,
446
+ 2
447
+ );
448
+ } catch (err) {
449
+ return `Failed to fetch scrobbles data. Please check your token and try again, error: ${err.message}`;
450
+ }
451
+ }
452
+
453
+ async function nowplaying$1(did) {
454
+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
455
+ try {
456
+ await fs$1.access(tokenPath);
457
+ } catch (err) {
458
+ if (!did) {
459
+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
460
+ }
461
+ }
462
+ const tokenData = await fs$1.readFile(tokenPath, "utf-8");
463
+ const { token } = JSON.parse(tokenData);
464
+ if (!token && !did) {
465
+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
466
+ }
467
+ const client = new RockskyClient(token);
468
+ try {
469
+ const nowPlaying = await client.getSpotifyNowPlaying(did);
470
+ if (!nowPlaying || Object.keys(nowPlaying).length === 0) {
471
+ const nowPlaying2 = await client.getNowPlaying(did);
472
+ if (!nowPlaying2 || Object.keys(nowPlaying2).length === 0) {
473
+ return "No track is currently playing.";
474
+ }
475
+ return JSON.stringify(
476
+ {
477
+ title: nowPlaying2.title,
478
+ artist: nowPlaying2.artist,
479
+ album: nowPlaying2.album
480
+ },
481
+ null,
482
+ 2
483
+ );
484
+ }
485
+ return JSON.stringify(
486
+ {
487
+ title: nowPlaying.item.name,
488
+ artist: nowPlaying.item.artists.map((a) => a.name).join(", "),
489
+ album: nowPlaying.item.album.name
490
+ },
491
+ null,
492
+ 2
493
+ );
494
+ } catch (err) {
495
+ return `Failed to fetch now playing data. Please check your token and try again, error: ${err.message}`;
496
+ }
497
+ }
498
+
499
+ dayjs.extend(relative);
500
+ async function scrobbles$1(did, { skip, limit }) {
501
+ try {
502
+ const client = new RockskyClient();
503
+ const scrobbles2 = await client.scrobbles(did, { skip, limit });
504
+ if (did) {
505
+ return JSON.stringify(
506
+ scrobbles2.map((scrobble) => ({
507
+ title: scrobble.title,
508
+ artist: scrobble.artist,
509
+ date: dayjs(scrobble.created_at + "Z").fromNow(),
510
+ isoDate: scrobble.created_at
511
+ })),
512
+ null,
513
+ 2
514
+ );
515
+ }
516
+ return JSON.stringify(
517
+ scrobbles2.map((scrobble) => ({
518
+ user: `@${scrobble.user}`,
519
+ title: scrobble.title,
520
+ artist: scrobble.artist,
521
+ date: dayjs(scrobble.date).fromNow(),
522
+ isoDate: scrobble.date
523
+ })),
524
+ null,
525
+ 2
526
+ );
527
+ } catch (err) {
528
+ return `Failed to fetch scrobbles data. Please check your token and try again, error: ${err.message}`;
529
+ }
530
+ }
531
+
532
+ async function search$1(query, { limit = 20, albums = false, artists = false, tracks = false, users = false }) {
533
+ const client = new RockskyClient();
534
+ const results = await client.search(query, { size: limit });
535
+ if (results.records.length === 0) {
536
+ return `No results found for ${query}.`;
537
+ }
538
+ let mergedResults = results.records.map((record) => ({
539
+ ...record,
540
+ type: record.table
541
+ }));
542
+ if (albums) {
543
+ mergedResults = mergedResults.filter((record) => record.table === "albums");
544
+ }
545
+ if (artists) {
546
+ mergedResults = mergedResults.filter(
547
+ (record) => record.table === "artists"
548
+ );
549
+ }
550
+ if (tracks) {
551
+ mergedResults = mergedResults.filter(({ table }) => table === "tracks");
552
+ }
553
+ if (users) {
554
+ mergedResults = mergedResults.filter(({ table }) => table === "users");
555
+ }
556
+ mergedResults.sort((a, b) => b.xata_score - a.xata_score);
557
+ const responses = [];
558
+ for (const { table, record } of mergedResults) {
559
+ if (table === "users") {
560
+ responses.push({
561
+ handle: record.handle,
562
+ display_name: record.display_name,
563
+ did: record.did,
564
+ link: `https://rocksky.app/profile/${record.did}`,
565
+ type: "account"
566
+ });
567
+ }
568
+ if (table === "albums") {
569
+ const link = record.uri ? `https://rocksky.app/${record.uri?.split("at://")[1]}` : "";
570
+ responses.push({
571
+ title: record.title,
572
+ artist: record.artist,
573
+ link,
574
+ type: "album"
575
+ });
576
+ }
577
+ if (table === "tracks") {
578
+ const link = record.uri ? `https://rocksky.app/${record.uri?.split("at://")[1]}` : "";
579
+ responses.push({
580
+ title: record.title,
581
+ artist: record.artist,
582
+ link,
583
+ type: "track"
584
+ });
585
+ }
586
+ if (table === "artists") {
587
+ const link = record.uri ? `https://rocksky.app/${record.uri?.split("at://")[1]}` : "";
588
+ responses.push({
589
+ name: record.name,
590
+ link,
591
+ type: "artist"
592
+ });
593
+ }
594
+ }
595
+ return JSON.stringify(responses, null, 2);
596
+ }
597
+
598
+ async function stats$1(did) {
599
+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
600
+ try {
601
+ await fs$1.access(tokenPath);
602
+ } catch (err) {
603
+ if (!did) {
604
+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
605
+ }
606
+ }
607
+ const tokenData = await fs$1.readFile(tokenPath, "utf-8");
608
+ const { token } = JSON.parse(tokenData);
609
+ if (!token && !did) {
610
+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
611
+ }
612
+ try {
613
+ const client = new RockskyClient(token);
614
+ const stats2 = await client.stats(did);
615
+ return JSON.stringify(
616
+ {
617
+ scrobbles: stats2.scrobbles,
618
+ tracks: stats2.tracks,
619
+ albums: stats2.albums,
620
+ artists: stats2.artists,
621
+ lovedTracks: stats2.lovedTracks
622
+ },
623
+ null,
624
+ 2
625
+ );
626
+ } catch (err) {
627
+ return `Failed to fetch stats data. Please check your token and try again, error: ${err.message}`;
628
+ }
629
+ }
630
+
631
+ async function tracks$1(did, { skip, limit = 20 }) {
632
+ const client = new RockskyClient();
633
+ const tracks2 = await client.getTracks(did, { skip, limit });
634
+ let rank = 1;
635
+ let response = `Top ${limit} tracks:
636
+ `;
637
+ for (const track of tracks2) {
638
+ response += `${rank} ${track.title} - ${track.artist} - ${track.play_count} plays
639
+ `;
640
+ rank++;
641
+ }
642
+ return response;
643
+ }
644
+
645
+ async function whoami$1() {
646
+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
647
+ try {
648
+ await fs$1.access(tokenPath);
649
+ } catch (err) {
650
+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
651
+ }
652
+ const tokenData = await fs$1.readFile(tokenPath, "utf-8");
653
+ const { token } = JSON.parse(tokenData);
654
+ if (!token) {
655
+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
656
+ }
657
+ const client = new RockskyClient(token);
658
+ try {
659
+ const user = await client.getCurrentUser();
660
+ return `You are logged in as ${user.handle} (${user.displayName}).
661
+ View your profile at: https://rocksky.app/profile/${user.handle}`;
662
+ } catch (err) {
663
+ return "Failed to fetch user data. Please check your token and try again.";
664
+ }
665
+ }
666
+
667
+ class RockskyMcpServer {
668
+ server;
669
+ client;
670
+ constructor() {
671
+ this.server = new McpServer({
672
+ name: "rocksky-mcp",
673
+ version: "0.1.0"
674
+ });
675
+ this.setupTools();
676
+ }
677
+ setupTools() {
678
+ this.server.tool("whoami", "get the current logged-in user.", async () => {
679
+ return {
680
+ content: [
681
+ {
682
+ type: "text",
683
+ text: await whoami$1()
684
+ }
685
+ ]
686
+ };
687
+ });
688
+ this.server.tool(
689
+ "nowplaying",
690
+ "get the currently playing track.",
691
+ {
692
+ did: z.string().optional().describe(
693
+ "the DID or handle of the user to get the now playing track for."
694
+ )
695
+ },
696
+ async ({ did }) => {
697
+ return {
698
+ content: [
699
+ {
700
+ type: "text",
701
+ text: await nowplaying$1(did)
702
+ }
703
+ ]
704
+ };
705
+ }
706
+ );
707
+ this.server.tool(
708
+ "scrobbles",
709
+ "display recently played tracks (recent scrobbles).",
710
+ {
711
+ did: z.string().optional().describe("the DID or handle of the user to get the scrobbles for."),
712
+ skip: z.number().optional().describe("number of scrobbles to skip"),
713
+ limit: z.number().optional().describe("number of scrobbles to limit")
714
+ },
715
+ async ({ did, skip = 0, limit = 10 }) => {
716
+ return {
717
+ content: [
718
+ {
719
+ type: "text",
720
+ text: await scrobbles$1(did, { skip, limit })
721
+ }
722
+ ]
723
+ };
724
+ }
725
+ );
726
+ this.server.tool(
727
+ "my-scrobbles",
728
+ "display my recently played tracks (recent scrobbles).",
729
+ {
730
+ skip: z.number().optional().describe("number of scrobbles to skip"),
731
+ limit: z.number().optional().describe("number of scrobbles to limit")
732
+ },
733
+ async ({ skip = 0, limit = 10 }) => {
734
+ return {
735
+ content: [
736
+ {
737
+ type: "text",
738
+ text: await myscrobbles({ skip, limit })
739
+ }
740
+ ]
741
+ };
742
+ }
743
+ );
744
+ this.server.tool(
745
+ "search",
746
+ "search for tracks, artists, albums or users.",
747
+ {
748
+ query: z.string().describe("the search query, e.g., artist, album, title or account"),
749
+ limit: z.number().optional().describe("number of results to limit"),
750
+ albums: z.boolean().optional().describe("search for albums"),
751
+ tracks: z.boolean().optional().describe("search for tracks"),
752
+ users: z.boolean().optional().describe("search for users"),
753
+ artists: z.boolean().optional().describe("search for artists")
754
+ },
755
+ async ({
756
+ query,
757
+ limit = 10,
758
+ albums: albums2 = false,
759
+ tracks: tracks2 = false,
760
+ users = false,
761
+ artists: artists2 = false
762
+ }) => {
763
+ return {
764
+ content: [
765
+ {
766
+ type: "text",
767
+ text: await search$1(query, {
768
+ limit,
769
+ albums: albums2,
770
+ tracks: tracks2,
771
+ users,
772
+ artists: artists2
773
+ })
774
+ }
775
+ ]
776
+ };
777
+ }
778
+ );
779
+ this.server.tool(
780
+ "artists",
781
+ "get the user's top artists or current user's artists if no did is provided.",
782
+ {
783
+ did: z.string().optional().describe("the DID or handle of the user to get artists for."),
784
+ limit: z.number().optional().describe("number of results to limit")
785
+ },
786
+ async ({ did, limit }) => {
787
+ return {
788
+ content: [
789
+ {
790
+ type: "text",
791
+ text: await artists(did, { skip: 0, limit })
792
+ }
793
+ ]
794
+ };
795
+ }
796
+ );
797
+ this.server.tool(
798
+ "albums",
799
+ "get the user's top albums or current user's albums if no did is provided.",
800
+ {
801
+ did: z.string().optional().describe("the DID or handle of the user to get albums for."),
802
+ limit: z.number().optional().describe("number of results to limit")
803
+ },
804
+ async ({ did, limit }) => {
805
+ return {
806
+ content: [
807
+ {
808
+ type: "text",
809
+ text: await albums(did, { skip: 0, limit })
810
+ }
811
+ ]
812
+ };
813
+ }
814
+ );
815
+ this.server.tool(
816
+ "tracks",
817
+ "get the user's top tracks or current user's tracks if no did is provided.",
818
+ {
819
+ did: z.string().optional().describe("the DID or handle of the user to get tracks for."),
820
+ limit: z.number().optional().describe("number of results to limit")
821
+ },
822
+ async ({ did, limit }) => {
823
+ return {
824
+ content: [
825
+ {
826
+ type: "text",
827
+ text: await tracks$1(did, { skip: 0, limit })
828
+ }
829
+ ]
830
+ };
831
+ }
832
+ );
833
+ this.server.tool(
834
+ "stats",
835
+ "get the user's listening stats or current user's stats if no did is provided.",
836
+ {
837
+ did: z.string().optional().describe("the DID or handle of the user to get stats for.")
838
+ },
839
+ async ({ did }) => {
840
+ return {
841
+ content: [
842
+ {
843
+ type: "text",
844
+ text: await stats$1(did)
845
+ }
846
+ ]
847
+ };
848
+ }
849
+ );
850
+ this.server.tool(
851
+ "create-apikey",
852
+ "create an API key.",
853
+ {
854
+ name: z.string().describe("the name of the API key"),
855
+ description: z.string().optional().describe("the description of the API key")
856
+ },
857
+ async ({ name, description }) => {
858
+ return {
859
+ content: [
860
+ {
861
+ type: "text",
862
+ text: await createApiKey(name, { description })
863
+ }
864
+ ]
865
+ };
866
+ }
867
+ );
868
+ }
869
+ async run() {
870
+ const stdioTransport = new StdioServerTransport();
871
+ try {
872
+ await this.server.connect(stdioTransport);
873
+ } catch (error) {
874
+ process.exit(1);
875
+ }
876
+ }
877
+ getServer() {
878
+ return this.server;
879
+ }
880
+ }
881
+ const rockskyMcpServer = new RockskyMcpServer();
882
+
883
+ function mcp() {
884
+ rockskyMcpServer.run().catch((error) => {
885
+ console.error("Failed to run Rocksky MCP server", { error });
886
+ process.exit(1);
887
+ });
888
+ }
889
+
366
890
  async function nowplaying(did) {
367
891
  const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
368
892
  try {
@@ -637,7 +1161,7 @@ async function whoami() {
637
1161
  }
638
1162
  }
639
1163
 
640
- var version = "0.1.0";
1164
+ var version = "0.2.0";
641
1165
  var version$1 = {
642
1166
  version: version};
643
1167
 
@@ -680,7 +1204,9 @@ async function login(handle) {
680
1204
 
681
1205
  const program = new Command();
682
1206
  program.name("rocksky").description(
683
- "Command-line interface for Rocksky \u2013 scrobble tracks, view stats, and manage your listening history."
1207
+ `Command-line interface for Rocksky (${chalk.underline(
1208
+ "https://rocksky.app"
1209
+ )}) \u2013 scrobble tracks, view stats, and manage your listening history.`
684
1210
  ).version(version$1.version);
685
1211
  program.command("login").argument("<handle>", "your BlueSky handle (e.g., <username>.bsky.social)").description("login with your BlueSky account and get a session token.").action(login);
686
1212
  program.command("whoami").description("get the current logged-in user.").action(whoami);
@@ -694,9 +1220,10 @@ program.command("search").option("-a, --albums", "search for albums").option("-t
694
1220
  "the search query, e.g., artist, album, title or account"
695
1221
  ).description("search for tracks, albums, or accounts.").action(search);
696
1222
  program.command("stats").option("-l, --limit <number>", "number of results to limit").argument("[did]", "the DID or handle of the user to get stats for.").description("get the user's listening stats.").action(stats);
697
- program.command("artists").option("-l, --limit <number>", "number of results to limit").argument("[did]", "the DID or handle of the user to get artists for.").description("get the user's top artists.").action(artists);
698
- program.command("albums").option("-l, --limit <number>", "number of results to limit").argument("[did]", "the DID or handle of the user to get albums for.").description("get the user's top albums.").action(albums);
1223
+ program.command("artists").option("-l, --limit <number>", "number of results to limit").argument("[did]", "the DID or handle of the user to get artists for.").description("get the user's top artists.").action(artists$1);
1224
+ program.command("albums").option("-l, --limit <number>", "number of results to limit").argument("[did]", "the DID or handle of the user to get albums for.").description("get the user's top albums.").action(albums$1);
699
1225
  program.command("tracks").option("-l, --limit <number>", "number of results to limit").argument("[did]", "the DID or handle of the user to get tracks for.").description("get the user's top tracks.").action(tracks);
700
1226
  program.command("scrobble").argument("<track>", "the title of the track").argument("<artist>", "the artist of the track").option("-t, --timestamp <timestamp>", "the timestamp of the scrobble").description("scrobble a track to your profile.").action(scrobble);
701
- program.command("create").description("create a new API key.").command("apikey").argument("<name>", "the name of the API key").option("-d, --description <description>", "the description of the API key").description("create a new API key.").action(createApiKey);
1227
+ program.command("create").description("create a new API key.").command("apikey").argument("<name>", "the name of the API key").option("-d, --description <description>", "the description of the API key").description("create a new API key.").action(createApiKey$1);
1228
+ program.command("mcp").description("Starts an MCP server to use with Claude or other LLMs.").action(mcp);
702
1229
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocksky/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Command-line interface for Rocksky – scrobble tracks, view stats, and manage your listening history",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -22,6 +22,7 @@
22
22
  "author": "Tsiry Sandratraina <tsiry.sndr@rocksky.app>",
23
23
  "license": "Apache-2.0",
24
24
  "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.10.2",
25
26
  "axios": "^1.8.4",
26
27
  "chalk": "^5.4.1",
27
28
  "commander": "^13.1.0",
@@ -30,7 +31,8 @@
30
31
  "express": "^5.1.0",
31
32
  "md5": "^2.3.0",
32
33
  "open": "^10.1.0",
33
- "table": "^6.9.0"
34
+ "table": "^6.9.0",
35
+ "zod": "^3.24.3"
34
36
  },
35
37
  "devDependencies": {
36
38
  "@types/express": "^5.0.1",
package/src/cmd/mcp.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { rockskyMcpServer } from "mcp";
2
+
3
+ export function mcp() {
4
+ rockskyMcpServer.run().catch((error) => {
5
+ console.error("Failed to run Rocksky MCP server", { error });
6
+ process.exit(1);
7
+ });
8
+ }
package/src/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import chalk from "chalk";
3
4
  import { albums } from "cmd/albums";
4
5
  import { artists } from "cmd/artists";
5
6
  import { createApiKey } from "cmd/create";
7
+ import { mcp } from "cmd/mcp";
6
8
  import { nowplaying } from "cmd/nowplaying";
7
9
  import { scrobble } from "cmd/scrobble";
8
10
  import { scrobbles } from "cmd/scrobbles";
@@ -19,7 +21,9 @@ const program = new Command();
19
21
  program
20
22
  .name("rocksky")
21
23
  .description(
22
- "Command-line interface for Rocksky – scrobble tracks, view stats, and manage your listening history."
24
+ `Command-line interface for Rocksky (${chalk.underline(
25
+ "https://rocksky.app"
26
+ )}) – scrobble tracks, view stats, and manage your listening history.`
23
27
  )
24
28
  .version(version.version);
25
29
 
@@ -109,4 +113,9 @@ program
109
113
  .description("create a new API key.")
110
114
  .action(createApiKey);
111
115
 
116
+ program
117
+ .command("mcp")
118
+ .description("Starts an MCP server to use with Claude or other LLMs.")
119
+ .action(mcp);
120
+
112
121
  program.parse(process.argv);