A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at fix/spotify 269 lines 6.9 kB view raw
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();