Skip to main content
Glama
iceener

Spotify Streamable MCP Server

by iceener
player-status.ts11.5 kB
/** * Player Status Tool - Get current Spotify player state, devices, and queue. */ import type { z } from 'zod'; import { config } from '../../config/env.js'; import { toolsMetadata } from '../../config/metadata.js'; import { SpotifyStatusInputSchema } from '../../schemas/inputs.js'; import { SpotifyStatusOutput } from '../../schemas/outputs.js'; import { getCurrentlyPlaying, getPlayerState, getQueue, listDevices, } from '../../services/spotify/player.js'; import { getSpotifyUserClient } from '../../services/spotify/sdk.js'; import { sharedLogger as logger } from '../utils/logger.js'; import { defineTool, type ToolContext, type ToolResult } from './types.js'; type ErrorCode = 'unauthorized' | 'forbidden' | 'rate_limited' | 'bad_response'; function errorResult(message: string, code?: ErrorCode): ToolResult { return { isError: true, content: [{ type: 'text', text: message }], structuredContent: { ok: false, action: 'status', error: message, code }, }; } export const playerStatusTool = defineTool({ name: toolsMetadata.player_status.name, title: toolsMetadata.player_status.title, description: toolsMetadata.player_status.description, inputSchema: SpotifyStatusInputSchema, outputSchema: SpotifyStatusOutput.shape, annotations: { title: toolsMetadata.player_status.title, readOnlyHint: true, openWorldHint: true, }, handler: async (args, context: ToolContext): Promise<ToolResult> => { try { const client = await getSpotifyUserClient(context); if (!client) { logger.info('player_status', { message: 'Missing user token', sessionId: context.sessionId, }); return errorResult('Missing user token. Please authenticate.', 'unauthorized'); } const wantedData = new Set( args.include ?? ['player', 'devices', 'current_track'], ); const requests: Array<Promise<unknown>> = []; const requestKeys: string[] = []; if (wantedData.has('player')) { requestKeys.push('player'); requests.push(getPlayerState(client)); } if (wantedData.has('devices')) { requestKeys.push('devices'); requests.push(listDevices(client)); } if (wantedData.has('queue')) { requestKeys.push('queue'); requests.push(getQueue(client)); } if (wantedData.has('current_track')) { requestKeys.push('current_track'); requests.push(getCurrentlyPlaying(client)); } if (wantedData.has('current_track') && !requestKeys.includes('player')) { requestKeys.push('player'); requests.push(getPlayerState(client)); } const results = await Promise.all(requests); const output: Partial<z.infer<typeof SpotifyStatusOutput>> = {}; for (let index = 0; index < requestKeys.length; index++) { const key = requestKeys[index]; const value = results[index]; if (key === 'player' && value && typeof value === 'object') { const playerValue = value as { is_playing?: boolean; shuffle_state?: boolean; repeat_state?: 'off' | 'track' | 'context'; progress_ms?: number; timestamp?: number; device?: { id?: string }; context?: { uri?: string }; }; output.player = { is_playing: !!playerValue.is_playing, shuffle_state: playerValue.shuffle_state, repeat_state: playerValue.repeat_state, progress_ms: playerValue.progress_ms, timestamp: playerValue.timestamp, device_id: playerValue.device?.id ?? undefined, context_uri: playerValue.context?.uri ?? null, }; } if (key === 'devices' && value) { const devicesValue = value as { devices?: Array<{ id?: string | null; name?: string; type?: string; is_active?: boolean; volume_percent?: number | null; }>; }; const devicesList = Array.isArray(devicesValue?.devices) ? devicesValue.devices.map((device) => ({ id: device.id ?? null, name: String(device.name ?? ''), type: String(device.type ?? ''), is_active: !!device.is_active, volume_percent: device.volume_percent ?? null, })) : []; output.devices = devicesList; output.devicesById = Object.fromEntries( devicesList.filter((d) => d.id).map((d) => [d.id as string, d]), ); } if (key === 'queue' && value) { const queueValue = value as { currently_playing?: { id?: string | null }; queue?: Array<{ id?: string | null }>; }; output.queue = { current_id: queueValue.currently_playing?.id ?? null, next_ids: Array.isArray(queueValue.queue) ? (queueValue.queue.map((item) => item?.id).filter(Boolean) as string[]) : [], }; } if (key === 'current_track') { const currentValue = value as { item?: unknown; is_playing?: boolean; }; const trackItem = currentValue?.item as | { id?: unknown; uri?: unknown; name?: unknown; artists?: Array<{ name?: string }>; album?: { name?: string }; duration_ms?: number; } | undefined; if (trackItem) { output.current_track = { type: 'track', id: String(trackItem.id), uri: String(trackItem.uri), name: String(trackItem.name), artists: Array.isArray(trackItem.artists) ? (trackItem.artists.map((a) => a.name).filter(Boolean) as string[]) : [], album: trackItem.album?.name, duration_ms: trackItem.duration_ms, }; } else { output.current_track = null; } if (typeof currentValue?.is_playing === 'boolean') { output.player = { ...(output.player ?? {}), is_playing: typeof output.player?.is_playing === 'boolean' ? output.player?.is_playing : currentValue.is_playing, }; } } } // Derive device name and build status message const devicesRequested = wantedData.has('devices'); const noDevices = devicesRequested && (output.devices ?? []).length === 0; let activeDeviceName = output.devices?.find( (d) => d.id === output.player?.device_id, )?.name; if (!activeDeviceName && output.player?.device_id && !devicesRequested) { try { const dv = await listDevices(client); const devicesList = Array.isArray(dv?.devices) ? dv.devices.map((device) => ({ id: device.id ?? null, name: String(device.name ?? ''), type: String(device.type ?? ''), is_active: !!device.is_active, volume_percent: device.volume_percent ?? null, })) : []; output.devices = devicesList; output.devicesById = Object.fromEntries( devicesList.filter((d) => d.id).map((d) => [d.id as string, d]), ); activeDeviceName = devicesList.find( (d) => d.id === output.player?.device_id, )?.name; } catch { // Ignore errors fetching devices } } const deviceLabel = activeDeviceName || undefined; const lastTrackNote = output.current_track?.name ? ` Last track was '${output.current_track.name}'.` : ''; const derivedIsPlaying = typeof output.player?.is_playing === 'boolean' ? output.player?.is_playing : undefined; // Build device list for the message (showing ID prominently) const deviceListMsg = (output.devices ?? []).length > 0 ? `\n\nAvailable devices (use device_id for control):\n${( output.devices ?? [] ) .map( (d) => `• ${d.name} (${d.type})${d.is_active ? ' [ACTIVE]' : ''} → device_id: "${d.id}"`, ) .join('\n')}` : ''; const statusMessage = (() => { const deviceBit = deviceLabel ? ` on '${deviceLabel}' (device_id: "${output.player?.device_id}")` : output.player?.device_id ? ` (device_id: "${output.player.device_id}")` : ''; if (derivedIsPlaying === true) { const trackBit = output.current_track?.name ? `'${output.current_track.name}'` : 'Content'; const contextBit = output.player?.context_uri ? ` Context: ${output.player.context_uri}.` : ''; return `${trackBit} is playing${deviceBit}.${contextBit}${deviceListMsg}`.trim(); } if (derivedIsPlaying === false) { if (devicesRequested) { return noDevices ? `No devices available.${lastTrackNote} Ask the user to open Spotify on any device, then try transfer or play again.` : `No active playback.${lastTrackNote} You can transfer to an available device and play.${deviceListMsg}`; } return `No active playback.${lastTrackNote} To check devices, call player_status including "devices".`; } const contextBit = output.player?.context_uri ? ` Context: ${output.player.context_uri}.` : ''; return output.current_track?.name ? `Playback status unknown. '${output.current_track.name}' is the current item.${contextBit} Include 'player' to confirm is_playing and 'devices' to list targets.${deviceListMsg}` : `Playback status unknown.${contextBit} Include 'player' to confirm is_playing and 'devices' to list targets.${deviceListMsg}`; })(); const structured = { ...output, _msg: statusMessage, }; const contentParts: Array<{ type: 'text'; text: string }> = [ { type: 'text', text: statusMessage }, ]; if (config.SPOTIFY_INCLUDE_JSON_IN_CONTENT) { contentParts.push({ type: 'text', text: JSON.stringify(structured) }); } return { content: contentParts, structuredContent: structured, }; } catch (error) { const err = error as Error; logger.error('player_status', { message: 'Tool error', error: err.message }); const codeMatch = err.message.match(/\[(\w+)\]$/); const code = codeMatch ? (codeMatch[1] as ErrorCode) : 'bad_response'; let userMessage = err.message.replace(/\s*\[\w+\]$/, ''); if (code === 'unauthorized') { userMessage = 'Not authenticated. Please sign in to Spotify.'; } else if (code === 'forbidden') { userMessage = 'Access denied. You may need additional permissions or Spotify Premium.'; } else if (code === 'rate_limited') { userMessage = 'Too many requests. Please wait a moment and try again.'; } return errorResult(userMessage, 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