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'
      }
    };
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It mentions platform limitation (macOS only) and implies interaction with Spotify, but doesn't describe what happens when actions are invoked (e.g., does 'play' start playback, does 'info' return track details?), error conditions, or authentication requirements. For a tool with multiple actions and parameters, this leaves significant behavioral gaps.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise (7 words) and front-loaded with all essential information: tool purpose and key constraint. Every word earns its place with zero wasted text, making it easy for an agent to parse quickly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with 4 parameters, multiple actions, no annotations, and no output schema, the description is insufficient. It doesn't explain what the tool returns (especially for 'info' action), how parameters interact (e.g., can 'uri' and 'mood' be used together?), or error handling. The high parameter count and action complexity require more contextual guidance than provided.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, with all parameters well-documented in the schema itself (e.g., 'action' enum values, 'volume' range, 'mood' options). The description adds no parameter-specific information beyond what's in the schema, so it meets the baseline of 3 for high schema coverage without compensating value.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Control Spotify for background music (macOS only)'. It specifies the action (control), target resource (Spotify for background music), and platform constraint (macOS only). However, it doesn't distinguish this tool from sibling tools like 'notify' or 'review_code', which serve completely different domains, so sibling differentiation isn't relevant here.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides minimal usage guidance with the 'macOS only' constraint, but offers no guidance on when to use specific actions (e.g., 'play' vs 'playpause') or parameters (e.g., 'uri' vs 'mood'). It doesn't mention alternatives or exclusions, leaving the agent to infer usage from the schema alone.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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