Skip to main content
Glama

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
NameRequiredDescriptionDefault
actionYesMusic control action
uriNoSpotify URI or search term
volumeNoVolume level (0-100)
moodNoMood-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 definition
    server.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)
    );
  • 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 class
    async 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);
      }
    }
  • Builds the platform-specific AppleScript command for Spotify control based on the action and parameters
    protected 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)}`);
      }
    }
  • TypeScript interface defining the input parameters for the music tool
    export 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
    }
  • Default mood-based Spotify playlist configurations used as fallback
    const 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'
      }
    };

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/jaggederest/mcp_reviewer'

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