@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/README.md +257 -0
- package/TOOLS.md +194 -0
- package/bun.lock +28 -0
- package/dist/index.js +536 -9
- package/package.json +4 -2
- package/src/cmd/mcp.ts +8 -0
- package/src/index.ts +10 -1
- package/src/mcp/index.ts +269 -0
- package/src/mcp/tools/albums.ts +13 -0
- package/src/mcp/tools/artists.ts +17 -0
- package/src/mcp/tools/create.ts +27 -0
- package/src/mcp/tools/myscrobbles.ts +42 -0
- package/src/mcp/tools/nowplaying.ts +53 -0
- package/src/mcp/tools/scrobbles.ts +39 -0
- package/src/mcp/tools/search.ts +88 -0
- package/src/mcp/tools/stats.ts +40 -0
- package/src/mcp/tools/tracks.ts +15 -0
- package/src/mcp/tools/whoami.ts +27 -0
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
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
|
-
|
|
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);
|