music
Control Spotify playback on macOS to play, pause, skip tracks, adjust volume, and select mood-based playlists for development workflows.
Instructions
Control Spotify for background music (macOS only)
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | Music control action | |
| uri | No | Spotify URI or search term | |
| volume | No | Volume level (0-100) | |
| mood | No | Mood-based playlist selection (focus, relax, energize, chill, work, or custom) |
Implementation Reference
- src/index.ts:111-123 (registration)Registration of the music tool in the MCP server, including Zod input schema definitionserver.registerTool( 'music', { description: 'Control Spotify for background music (macOS only)', inputSchema: { action: z.enum(['play', 'pause', 'playpause', 'next', 'previous', 'volume', 'mute', 'info']).describe('Music control action'), uri: z.string().optional().describe('Spotify URI or search term'), volume: z.number().min(0).max(100).optional().describe('Volume level (0-100)'), mood: z.string().optional().describe('Mood-based playlist selection (focus, relax, energize, chill, work, or custom)'), }, }, async (args) => music(args) );
- src/tools/music.ts:212-316 (handler)Main handler function executing the music tool logic, including platform checks, safety validations (mute, volume protection, playback interruption), Spotify status checks, and command execution via parent classasync execute(args: MusicOptions): Promise<CallToolResult> { // Check platform first if (platform() !== 'darwin') { return createErrorResult('Music Control', 'Music control is only available on macOS'); } try { // Safety check: Check if system is muted const isMuted = await this.isSystemMuted(); if (isMuted && args.action !== 'info') { return this.createSafetyErrorResult( '🔇 System Audio Muted', 'System is muted', ['unmute first', 'use info action'] ); } // Check if Spotify is running const checkCommand = `osascript -e 'tell application "System Events" to (name of processes) contains "Spotify"'`; const { stdout: isRunning } = await execAsync(checkCommand); const spotifyRunning = isRunning.trim() === 'true'; if (!spotifyRunning && args.action !== 'play') { return createErrorResult('Music Control', 'Spotify is not running. Please start Spotify first.'); } // Safety check: Avoid interrupting currently playing music if (args.action === 'play' && spotifyRunning) { const isPlaying = await this.isSpotifyPlaying(); if (isPlaying) { // Check if this is a generic play command or mood playlist switch if (args.uri === undefined || args.mood !== undefined) { return this.createSafetyErrorResult( '🎵 Already Playing', args.mood ? `Cannot switch to ${args.mood} playlist` : 'Cannot interrupt playback', ['pause first', 'specify track/artist', args.mood ? `pause then play mood ${args.mood}` : 'use next/previous'] ); } } } // Safety check: Volume protection if (args.action === 'volume' && args.volume !== undefined && spotifyRunning) { const currentVolume = await this.getCurrentSpotifyVolume(); const MAX_SAFE_VOLUME = await this.getSafeVolume(); const GRADUAL_INCREASE_LIMIT = await this.getVolumeIncrement(); // If trying to increase volume if (args.volume > currentVolume) { // Check if it's a significant increase if (args.volume > MAX_SAFE_VOLUME && currentVolume <= MAX_SAFE_VOLUME) { return this.createSafetyErrorResult( '⚠️ Volume Too High', `${args.volume}% exceeds safe level (${MAX_SAFE_VOLUME}%). Current: ${currentVolume}%`, [`try ${MAX_SAFE_VOLUME}%`, `increase by ${GRADUAL_INCREASE_LIMIT}% max`, 'use multiple steps'] ); } // Warn for any significant volume increase if (args.volume - currentVolume > GRADUAL_INCREASE_LIMIT) { const suggestedVolume = Math.min(currentVolume + GRADUAL_INCREASE_LIMIT, args.volume); return this.createSafetyErrorResult( '⚠️ Volume Jump Too Large', `+${args.volume - currentVolume}% increase (${currentVolume}% → ${args.volume}%)`, [`try ${suggestedVolume}% first`, 'increase gradually', 'protect hearing'] ); } } } // If playing and Spotify isn't running, start it first if (args.action === 'play' && !spotifyRunning) { await execAsync(`osascript -e 'tell application "Spotify" to activate'`); // Give Spotify time to start await new Promise(resolve => setTimeout(resolve, 3000)); } // Use the parent class execute method for the main command const result = await super.execute(args); // If it's info command and we got output, customize the message if (args.action === 'info' && result.content[0] && 'text' in result.content[0]) { const text = (result.content[0] as { text: string }).text; if (text.includes('Now playing:')) { return result; } } if (args.action === 'play' && args.mood) { const playlists = await this.loadPlaylists(); if (args.mood in playlists) { const playlist = playlists[args.mood]; return createSuccessResult(`Playing ${playlist.name} playlist for ${args.mood} mood`); } return result; } else if (args.action === 'volume' && args.volume !== undefined) { const newVolume = Math.min(args.volume, 100); return createSuccessResult(`Volume set to ${newVolume}% (hearing protection active)`); } return result; } catch (error) { return createErrorResult('Music Control', error); } }
- src/tools/music.ts:140-210 (handler)Builds the platform-specific AppleScript command for Spotify control based on the action and parametersprotected async buildCommand(args: MusicOptions): Promise<string> { // Only support macOS for now if (platform() !== 'darwin') { throw new Error('Music control is only available on macOS'); } const { action, uri, volume, mood } = args; // Build AppleScript command based on action switch (action) { case 'play': if (mood) { // Play mood-based playlist const playlists = await this.loadPlaylists(); if (!(mood in playlists)) { throw new Error(`Unknown mood: ${mood}. Available moods: ${Object.keys(playlists).join(', ')}`); } const playlist = playlists[mood]; return `osascript -e 'tell application "Spotify" to play track "${playlist.uri}"'`; } else if (uri !== undefined) { // Play specific URI or search if (uri.startsWith('spotify:')) { return `osascript -e 'tell application "Spotify" to play track "${uri}"'`; } else { // Search and play (this is more complex, would need search API) return `osascript -e 'tell application "Spotify" to play'`; } } else { // Just play return `osascript -e 'tell application "Spotify" to play'`; } case 'pause': return `osascript -e 'tell application "Spotify" to pause'`; case 'playpause': return `osascript -e 'tell application "Spotify" to playpause'`; case 'next': return `osascript -e 'tell application "Spotify" to next track'`; case 'previous': return `osascript -e 'tell application "Spotify" to previous track'`; case 'volume': if (volume !== undefined) { return `osascript -e 'tell application "Spotify" to set sound volume to ${Math.max(0, Math.min(100, volume))}'`; } throw new Error('Volume level required for volume action'); case 'mute': return `osascript -e 'tell application "Spotify" to set sound volume to 0'`; case 'info': // Get current track info return `osascript -e ' tell application "Spotify" if player state is playing then set trackName to name of current track set artistName to artist of current track set albumName to album of current track return "Now playing: " & trackName & " by " & artistName & " from " & albumName else return "Spotify is not playing" end if end tell'`; default: throw new Error(`Unknown action: ${String(action)}`); } }
- src/types/index.ts:66-71 (schema)TypeScript interface defining the input parameters for the music toolexport interface MusicOptions { action: 'play' | 'pause' | 'playpause' | 'next' | 'previous' | 'volume' | 'mute' | 'info'; uri?: string; // Spotify URI or search term volume?: number; // 0-100 mood?: string; // Allow custom moods from config }
- src/tools/music.ts:18-39 (helper)Default mood-based Spotify playlist configurations used as fallbackconst DEFAULT_MOOD_PLAYLISTS: Record<string, MusicPlaylist> = { focus: { uri: 'spotify:playlist:37i9dQZF1DWZeKCadgRdKQ', name: 'Deep Focus' }, relax: { uri: 'spotify:playlist:37i9dQZF1DWU0ScTcjJBdj', name: 'Relax & Unwind' }, energize: { uri: 'spotify:playlist:37i9dQZF1DX3rxVfibe1L0', name: 'Mood Booster' }, chill: { uri: 'spotify:playlist:37i9dQZF1DX4WYpdgoIcn6', name: 'Chill Hits' }, work: { uri: 'spotify:playlist:37i9dQZF1DWZk0frd3wbHL', name: 'Productive Morning' } };