Skip to main content
Glama
iceener

Spotify Streamable MCP Server

by iceener
catalog.ts4.01 kB
/** * Spotify catalog search service. */ import { z } from 'zod'; import { MinimalEntityCodec, SearchResponseCodec, TrackCodec, } from '../../types/spotify.codecs.js'; import { mapStatusToCode } from '../../utils/http-result.js'; import { toSlimAlbum, toSlimArtist, toSlimPlaylist, toSlimTrack, } from '../../utils/mappers.js'; import { getSpotifyAppClient } from './sdk.js'; export type SearchParams = { q: string; types: string[]; market?: string; limit?: number; offset?: number; include_external?: 'audio'; }; export async function searchCatalog(params: SearchParams, _signal?: AbortSignal) { const client = getSpotifyAppClient(); const searchParams = new URLSearchParams(); searchParams.set('q', params.q); searchParams.set('type', params.types.join(',')); if (params.limit) { searchParams.set('limit', String(params.limit)); } if (params.offset) { searchParams.set('offset', String(params.offset)); } if (params.market) { searchParams.set('market', params.market); } if (params.include_external) { searchParams.set('include_external', params.include_external); } try { const json = await client.makeRequest<unknown>( 'GET', `search?${searchParams.toString()}`, ); const parsedResponse = SearchResponseCodec.parse(json); const totals: Record<string, number> = {}; const items: Array< | ReturnType<typeof toSlimTrack> | ReturnType<typeof toSlimAlbum> | ReturnType<typeof toSlimArtist> | ReturnType<typeof toSlimPlaylist> > = []; if (parsedResponse.tracks) { totals.track = parsedResponse.tracks.total ?? 0; const trackItems = Array.isArray(parsedResponse.tracks.items) ? parsedResponse.tracks.items : []; for (const raw of trackItems) { const parsed = TrackCodec.safeParse(raw); if (parsed.success) { const slim = toSlimTrack(parsed.data); if (slim.id && slim.name) { items.push(slim); } } } } if (parsedResponse.albums) { totals.album = parsedResponse.albums.total ?? 0; const albumItems = Array.isArray(parsedResponse.albums.items) ? parsedResponse.albums.items : []; for (const raw of albumItems) { const parsed = MinimalEntityCodec.safeParse(raw); if (parsed.success) { const slim = toSlimAlbum(parsed.data); if (slim.id && slim.name) { items.push(slim); } } } } if (parsedResponse.artists) { totals.artist = parsedResponse.artists.total ?? 0; const artistItems = Array.isArray(parsedResponse.artists.items) ? parsedResponse.artists.items : []; for (const raw of artistItems) { const parsed = MinimalEntityCodec.safeParse(raw); if (parsed.success) { const slim = toSlimArtist(parsed.data); if (slim.id && slim.name) { items.push(slim); } } } } if (parsedResponse.playlists) { totals.playlist = parsedResponse.playlists.total ?? 0; const playlistItems = Array.isArray(parsedResponse.playlists.items) ? parsedResponse.playlists.items : []; for (const raw of playlistItems) { const parsed = MinimalEntityCodec.extend({ owner: z .object({ display_name: z.string().nullable().optional() }) .optional(), }).safeParse(raw); if (parsed.success) { const slim = toSlimPlaylist(parsed.data); if (slim.id && slim.name) { items.push(slim); } } } } return { totals, items } as const; } catch (error) { const status = (error as { status?: number }).status; const code = typeof status === 'number' ? mapStatusToCode(status) : 'bad_response'; const message = (error as Error).message; throw new Error(`Search failed: ${message} [${code}]`); } }

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/iceener/spotify-streamable-mcp-server'

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