forked from
rocksky.app/rocksky
A decentralized music tracking and discovery platform built on AT Protocol 馃幍
1import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3import { RockskyClient } from "client";
4import { z } from "zod";
5import { albums } from "./tools/albums";
6import { artists } from "./tools/artists";
7import { createApiKey } from "./tools/create";
8import { myscrobbles } from "./tools/myscrobbles";
9import { nowplaying } from "./tools/nowplaying";
10import { scrobbles } from "./tools/scrobbles";
11import { search } from "./tools/search";
12import { stats } from "./tools/stats";
13import { tracks } from "./tools/tracks";
14import { whoami } from "./tools/whoami";
15
16class RockskyMcpServer {
17 private readonly server: McpServer;
18 private readonly client: RockskyClient;
19
20 constructor() {
21 this.server = new McpServer({
22 name: "rocksky-mcp",
23 version: "0.1.0",
24 });
25 const client = new RockskyClient();
26 this.setupTools();
27 }
28
29 private setupTools() {
30 this.server.tool("whoami", "get the current logged-in user.", async () => {
31 return {
32 content: [
33 {
34 type: "text",
35 text: await whoami(),
36 },
37 ],
38 };
39 });
40
41 this.server.tool(
42 "nowplaying",
43 "get the currently playing track.",
44 {
45 did: z
46 .string()
47 .optional()
48 .describe(
49 "the DID or handle of the user to get the now playing track for."
50 ),
51 },
52 async ({ did }) => {
53 return {
54 content: [
55 {
56 type: "text",
57 text: await nowplaying(did),
58 },
59 ],
60 };
61 }
62 );
63
64 this.server.tool(
65 "scrobbles",
66 "display recently played tracks (recent scrobbles).",
67 {
68 did: z
69 .string()
70 .optional()
71 .describe("the DID or handle of the user to get the scrobbles for."),
72 skip: z.number().optional().describe("number of scrobbles to skip"),
73 limit: z.number().optional().describe("number of scrobbles to limit"),
74 },
75 async ({ did, skip = 0, limit = 10 }) => {
76 return {
77 content: [
78 {
79 type: "text",
80 text: await scrobbles(did, { skip, limit }),
81 },
82 ],
83 };
84 }
85 );
86
87 this.server.tool(
88 "my-scrobbles",
89 "display my recently played tracks (recent scrobbles).",
90 {
91 skip: z.number().optional().describe("number of scrobbles to skip"),
92 limit: z.number().optional().describe("number of scrobbles to limit"),
93 },
94 async ({ skip = 0, limit = 10 }) => {
95 return {
96 content: [
97 {
98 type: "text",
99 text: await myscrobbles({ skip, limit }),
100 },
101 ],
102 };
103 }
104 );
105
106 this.server.tool(
107 "search",
108 "search for tracks, artists, albums or users.",
109 {
110 query: z
111 .string()
112 .describe("the search query, e.g., artist, album, title or account"),
113 limit: z.number().optional().describe("number of results to limit"),
114 albums: z.boolean().optional().describe("search for albums"),
115 tracks: z.boolean().optional().describe("search for tracks"),
116 users: z.boolean().optional().describe("search for users"),
117 artists: z.boolean().optional().describe("search for artists"),
118 },
119 async ({
120 query,
121 limit = 10,
122 albums = false,
123 tracks = false,
124 users = false,
125 artists = false,
126 }) => {
127 return {
128 content: [
129 {
130 type: "text",
131 text: await search(query, {
132 limit,
133 albums,
134 tracks,
135 users,
136 artists,
137 }),
138 },
139 ],
140 };
141 }
142 );
143
144 this.server.tool(
145 "artists",
146 "get the user's top artists or current user's artists if no did is provided.",
147 {
148 did: z
149 .string()
150 .optional()
151 .describe("the DID or handle of the user to get artists for."),
152
153 limit: z.number().optional().describe("number of results to limit"),
154 },
155 async ({ did, limit }) => {
156 return {
157 content: [
158 {
159 type: "text",
160 text: await artists(did, { skip: 0, limit }),
161 },
162 ],
163 };
164 }
165 );
166
167 this.server.tool(
168 "albums",
169 "get the user's top albums or current user's albums if no did is provided.",
170 {
171 did: z
172 .string()
173 .optional()
174 .describe("the DID or handle of the user to get albums for."),
175 limit: z.number().optional().describe("number of results to limit"),
176 },
177 async ({ did, limit }) => {
178 return {
179 content: [
180 {
181 type: "text",
182 text: await albums(did, { skip: 0, limit }),
183 },
184 ],
185 };
186 }
187 );
188
189 this.server.tool(
190 "tracks",
191 "get the user's top tracks or current user's tracks if no did is provided.",
192 {
193 did: z
194 .string()
195 .optional()
196 .describe("the DID or handle of the user to get tracks for."),
197 limit: z.number().optional().describe("number of results to limit"),
198 },
199 async ({ did, limit }) => {
200 return {
201 content: [
202 {
203 type: "text",
204 text: await tracks(did, { skip: 0, limit }),
205 },
206 ],
207 };
208 }
209 );
210
211 this.server.tool(
212 "stats",
213 "get the user's listening stats or current user's stats if no did is provided.",
214 {
215 did: z
216 .string()
217 .optional()
218 .describe("the DID or handle of the user to get stats for."),
219 },
220 async ({ did }) => {
221 return {
222 content: [
223 {
224 type: "text",
225 text: await stats(did),
226 },
227 ],
228 };
229 }
230 );
231
232 this.server.tool(
233 "create-apikey",
234 "create an API key.",
235 {
236 name: z.string().describe("the name of the API key"),
237 description: z
238 .string()
239 .optional()
240 .describe("the description of the API key"),
241 },
242 async ({ name, description }) => {
243 return {
244 content: [
245 {
246 type: "text",
247 text: await createApiKey(name, { description }),
248 },
249 ],
250 };
251 }
252 );
253 }
254
255 async run() {
256 const stdioTransport = new StdioServerTransport();
257 try {
258 await this.server.connect(stdioTransport);
259 } catch (error) {
260 process.exit(1);
261 }
262 }
263
264 public getServer(): McpServer {
265 return this.server;
266 }
267}
268
269export const rockskyMcpServer = new RockskyMcpServer();