searchSpotify
Search Spotify music and content using keywords and filters for artists, tracks, albums, playlists, genres, years, or popularity tags to find specific items.
Instructions
Search Spotify by keyword and field filters (e.g. artist, track, playlist, tag:new, tag:hipster) and return items of the given type
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | 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". | |
| type | Yes | Which item type to return: track, album or playlist | |
| limit | No | Max number of results to return (1-50) | |
| offset | No | The index of the first item to return. Defaults to 0 |
Implementation Reference
- src/read.ts:56-124 (handler)The core handler function for searchSpotify tool. It takes query, type (track/album/playlist), limit, offset; calls Spotify search API via handleSpotifyRequest; formats results into numbered list with artists, duration/ID; returns markdown text or error.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) }`, }, ], }; } },
- src/read.ts:22-55 (schema)Input schema using Zod for validation: query (string with search syntax), type (enum track/album/playlist), optional limit (1-50), offset (0-1000). Includes detailed descriptions.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'), },
- src/index.ts:12-14 (registration)Registers the searchSpotify tool (included in readTools) with the MCP server by calling server.tool() for each tool in the combined list from playTools, readTools, writeTools.[...playTools, ...readTools, ...writeTools].forEach((tool) => { server.tool(tool.name, tool.description, tool.schema, tool.handler); });
- src/read.ts:521-522 (registration)Adds searchSpotify to the readTools array which is later imported and registered in index.ts.export const readTools = [ searchSpotify,
- src/utils.ts:354-389 (helper)Helper function used in the handler to execute Spotify API calls with automatic access token refresh on expiry or 401 errors.export async function handleSpotifyRequest<T>( action: (spotifyApi: SpotifyApi) => Promise<T>, ): Promise<T> { let config = loadSpotifyConfig(); let spotifyApi: SpotifyApi; try { // If token is expired, refresh first if ( config.accessTokenExpiresAt && config.accessTokenExpiresAt - Date.now() < 60 * 1000 ) { config = await refreshAccessToken(config); } spotifyApi = createSpotifyApi(); return await action(spotifyApi); } catch (error: any) { // If 401, try refresh once if (error?.status === 401 || /401|unauthorized/i.test(error?.message)) { config = await refreshAccessToken(config); cachedSpotifyApi = null; spotifyApi = createSpotifyApi(); return await action(spotifyApi); } // Skip JSON parsing errors as these are actually successful operations const errorMessage = error instanceof Error ? error.message : String(error); if ( errorMessage.includes('Unexpected token') || errorMessage.includes('Unexpected non-whitespace character') || errorMessage.includes('Exponent part is missing a number in JSON') ) { return undefined as T; } // Rethrow other errors throw error; } }