Skip to main content
Glama
igorgarbuz
by igorgarbuz
read.ts14.4 kB
import type { MaxInt } from '@spotify/web-api-ts-sdk'; import { z } from 'zod'; import type { SpotifyHandlerExtra, SpotifyTrack, tool } from './types.js'; import { formatDuration, handleSpotifyRequest } from './utils.js'; function isTrack(item: any): item is SpotifyTrack { return ( item && item.type === 'track' && Array.isArray(item.artists) && item.album && typeof item.album.name === 'string' ); } const searchSpotify: tool<{ query: z.ZodString; type: z.ZodEnum<['track', 'album', 'playlist']>; limit: z.ZodOptional<z.ZodNumber>; offset: z.ZodOptional<z.ZodNumber>; }> = { name: 'searchSpotify', description: 'Search Spotify by keyword and field filters (e.g. artist, track, playlist, tag:new, tag:hipster) and return items of the given type', schema: { query: z .string() .describe( [ 'Full Spotify search string combining:', '- free-text keywords (e.g. “remaster”),', '- field filters: artist:<name>, track:<name>, album:<name>,', ' year:<YYYY> or <YYYY-YYYY>, genre:<name>.', 'The album, artist and year filters can be used for album and track types.', 'The genre filter can only be used for track type.', 'Special filter tag:hipster (bottom 10% popularity) can be used with track and album types.', 'All separated by spaces. Example: "tag:hipster artist:Queen remaster".', ].join(' '), ), type: z .enum(['track', 'album', 'playlist']) .describe('Which item type to return: track, album or playlist'), limit: z .number() .min(1) .max(50) .optional() .describe('Max number of results to return (1-50)'), offset: z .number() .min(0) .max(1000) .optional() .describe('The index of the first item to return. Defaults to 0'), }, handler: async (args, extra: SpotifyHandlerExtra) => { const { query, type, limit = 20, offset = 0 } = args; try { const results = await handleSpotifyRequest(async (spotifyApi) => { return await spotifyApi.search( query, [type], undefined, limit as MaxInt<50>, offset, ); }); let formattedResults = ''; if (type === 'track' && results.tracks) { formattedResults = results.tracks.items .map((track, i) => { const artists = track.artists.map((a) => a.name).join(', '); const duration = formatDuration(track.duration_ms); return `${i + 1}. "${ track.name }" by ${artists} (${duration}) - ID: ${track.id}`; }) .join('\n'); } else if (type === 'album' && results.albums) { formattedResults = results.albums.items .map((album, i) => { const artists = album.artists.map((a) => a.name).join(', '); return `${i + 1}. "${album.name}" by ${artists} - ID: ${album.id}`; }) .join('\n'); } else if (type === 'playlist' && results.playlists) { formattedResults = results.playlists.items .map((playlist, i) => { return `${i + 1}. "${playlist?.name ?? 'Unknown Playlist'} (${ playlist?.description ?? 'No description' } tracks)" by ${playlist?.owner?.display_name} - ID: ${ playlist?.id }`; }) .join('\n'); } return { content: [ { type: 'text', text: formattedResults.length > 0 ? `# Search results for "${query}" (type: ${type})\n\n${formattedResults}` : `No ${type} results found for "${query}"`, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error searching for ${type}s: ${ error instanceof Error ? error.message : String(error) }`, }, ], }; } }, }; const getNowPlaying: tool<Record<string, never>> = { name: 'getNowPlaying', description: 'Get information about the currently playing track on Spotify', schema: {}, handler: async (args, extra: SpotifyHandlerExtra) => { try { const currentTrack = await handleSpotifyRequest(async (spotifyApi) => { return await spotifyApi.player.getCurrentlyPlayingTrack(); }); if (!currentTrack || !currentTrack.item) { return { content: [ { type: 'text', text: 'Nothing is currently playing on Spotify', }, ], }; } const item = currentTrack.item; if (!isTrack(item)) { return { content: [ { type: 'text', text: 'Currently playing item is not a track (might be a podcast episode)', }, ], }; } const artists = item.artists.map((a) => a.name).join(', '); const album = item.album.name; const duration = formatDuration(item.duration_ms); const progress = formatDuration(currentTrack.progress_ms || 0); const isPlaying = currentTrack.is_playing; return { content: [ { type: 'text', text: `# Currently ${isPlaying ? 'Playing' : 'Paused'}\n\n` + `**Track**: "${item.name}"\n` + `**Artist**: ${artists}\n` + `**Album**: ${album}\n` + `**Progress**: ${progress} / ${duration}\n` + `**ID**: ${item.id}`, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error getting current track: ${ error instanceof Error ? error.message : String(error) }`, }, ], }; } }, }; const getUserPlaylists: tool<{ limit: z.ZodOptional<z.ZodNumber>; }> = { name: 'getUserPlaylists', description: "Get a list of the current user's playlists on Spotify", schema: { limit: z .number() .min(1) .max(50) .optional() .describe('Maximum number of playlists to return (1-50)'), }, handler: async (args, extra: SpotifyHandlerExtra) => { const { limit = 50 } = args; const playlists = await handleSpotifyRequest(async (spotifyApi) => { return await spotifyApi.currentUser.playlists.playlists( limit as MaxInt<50>, ); }); if (playlists.items.length === 0) { return { content: [ { type: 'text', text: "You don't have any playlists on Spotify", }, ], }; } const formattedPlaylists = playlists.items .map((playlist, i) => { const tracksTotal = playlist.tracks?.total ? playlist.tracks.total : 0; return `${i + 1}. "${playlist.name}" (${tracksTotal} tracks) - ID: ${ playlist.id }`; }) .join('\n'); return { content: [ { type: 'text', text: `# Your Spotify Playlists\n\n${formattedPlaylists}`, }, ], }; }, }; const getPlaylistTracks: tool<{ playlistId: z.ZodString; limit: z.ZodOptional<z.ZodNumber>; offset: z.ZodOptional<z.ZodNumber>; }> = { name: 'getPlaylistTracks', description: 'Get a list of tracks in a Spotify playlist', schema: { playlistId: z.string().describe('The Spotify ID of the playlist'), limit: z .number() .min(1) .max(50) .optional() .describe('Maximum number of tracks to return (1-50)'), offset: z .number() .min(0) .max(1000) .optional() .describe('The index of the first item to return. Defaults to 0'), }, handler: async (args, extra: SpotifyHandlerExtra) => { const { playlistId, limit = 50, offset = 0 } = args; const playlistTracks = await handleSpotifyRequest(async (spotifyApi) => { return await spotifyApi.playlists.getPlaylistItems( playlistId, undefined, undefined, limit as MaxInt<50>, offset, ); }); if ((playlistTracks.items?.length ?? 0) === 0) { return { content: [ { type: 'text', text: "This playlist doesn't have any tracks", }, ], }; } const formattedTracks = playlistTracks.items .map((item, i) => { const { track } = item; if (!track) return `${i + 1}. [Removed track]`; if (isTrack(track)) { const artists = track.artists.map((a) => a.name).join(', '); const duration = formatDuration(track.duration_ms); return `${i + 1}. "${track.name}" by ${artists} (${duration}) - ID: ${track.id}`; } return `${i + 1}. Unknown item`; }) .join('\n'); return { content: [ { type: 'text', text: `# Tracks in Playlist\n\n${formattedTracks}`, }, ], }; }, }; const getRecentlyPlayed: tool<{ limit: z.ZodOptional<z.ZodNumber>; }> = { name: 'getRecentlyPlayed', description: 'Get a list of recently played tracks on Spotify', schema: { limit: z .number() .min(1) .max(50) .optional() .describe('Maximum number of tracks to return (1-50)'), }, handler: async (args, extra: SpotifyHandlerExtra) => { const { limit = 50 } = args; const history = await handleSpotifyRequest(async (spotifyApi) => { return await spotifyApi.player.getRecentlyPlayedTracks( limit as MaxInt<50>, ); }); if (history.items.length === 0) { return { content: [ { type: 'text', text: "You don't have any recently played tracks on Spotify", }, ], }; } const formattedHistory = history.items .map((item, i) => { const track = item.track; if (!track) return `${i + 1}. [Removed track]`; if (isTrack(track)) { const artists = track.artists.map((a) => a.name).join(', '); const duration = formatDuration(track.duration_ms); return `${i + 1}. "${track.name}" by ${artists} (${duration}) - ID: ${track.id}`; } return `${i + 1}. Unknown item`; }) .join('\n'); return { content: [ { type: 'text', text: `# Recently Played Tracks\n\n${formattedHistory}`, }, ], }; }, }; const getFollowedArtists: tool<{ after: z.ZodOptional<z.ZodString>; limit: z.ZodOptional<z.ZodNumber>; }> = { name: 'getFollowedArtists', description: 'Get a list of artists the user is following on Spotify', schema: { after: z .string() .optional() .describe( 'The last artist ID from the previous request. Cursor for pagination.', ), limit: z .number() .min(1) .max(50) .optional() .describe('Maximum number of artists to return (1-50)'), }, handler: async (args, extra: SpotifyHandlerExtra) => { const { limit = 50, after } = args; const artists = await handleSpotifyRequest(async (spotifyApi) => { return await spotifyApi.currentUser.followedArtists( after, limit as MaxInt<50>, ); }); if (artists.artists.items.length === 0) { return { content: [ { type: 'text', text: "User doesn't follow any artists on Spotify", }, ], }; } const formattedArtists = artists.artists.items .map((artist, i) => { return `${i + 1}. "${artist.name}" - ID: ${artist.id}`; }) .join('\n'); return { content: [ { type: 'text', text: `# Artists You Follow\n\n${formattedArtists}`, }, ], }; }, }; const getUserTopItems: tool<{ type: z.ZodString; time_range: z.ZodString; limit: z.ZodOptional<z.ZodNumber>; offset: z.ZodOptional<z.ZodNumber>; }> = { name: 'getUserTopItems', description: "Get a list of the user's top artists or tracks", schema: { type: z .string() .describe( 'The type of items to get top for. Must be "artists" or "tracks"', ), time_range: z .string() .describe( 'The time range for the top items. Must be "short_term", "medium_term", or "long_term"', ), limit: z .number() .min(1) .max(50) .optional() .describe('Maximum number of items to return (1-50)'), offset: z .number() .optional() .describe('The index of the first item to return. Defaults to 0'), }, handler: async (args, extra: SpotifyHandlerExtra) => { const { type, time_range, limit = 50, offset = 0 } = args; const topItems = await handleSpotifyRequest(async (spotifyApi) => { return await spotifyApi.currentUser.topItems( type as 'artists' | 'tracks', time_range as 'short_term' | 'medium_term' | 'long_term', limit as MaxInt<50>, offset, ); }); if (topItems.items.length === 0) { return { content: [ { type: 'text', text: `User doesn't have any top ${type} on Spotify`, }, ], }; } const formattedItems = topItems.items .map((item, i) => { if (type === 'artists') { return `${i + 1}. "${item.name}" - ID: ${item.id}`; } else if ( type === 'tracks' && 'artists' in item && Array.isArray(item.artists) ) { const artists = item.artists.map((a) => a.name).join(', '); return `${i + 1}. "${item.name}" by ${artists} - ID: ${item.id}`; } else { // fallback for type safety return `${i + 1}. "${item.name}" - ID: ${item.id}`; } }) .join('\n'); return { content: [ { type: 'text', text: `# Top ${type}\n\n${formattedItems}`, }, ], }; }, }; /* ---------- export list ---------- */ export const readTools = [ searchSpotify, getNowPlaying, getUserPlaylists, getPlaylistTracks, getRecentlyPlayed, getFollowedArtists, getUserTopItems, ];

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/igorgarbuz/spotify-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server