Skip to main content
Glama

Sonos MCP Server

server.py14.1 kB
from typing import Dict, List, Optional, Any, Literal from mcp.server.fastmcp import FastMCP import soco mcp = FastMCP("Sonos", dependencies=["soco"]) devices: Dict[str, soco.SoCo] = {} device: Optional[soco.SoCo] = None def discover_devices() -> Dict[str, soco.SoCo]: """Discover Sonos devices on the network and update the global devices dictionary. Returns: Dict[str, soco.SoCo]: A dictionary mapping device names to their respective SoCo objects. """ global devices devices = {device.player_name: device for device in soco.discover()} return devices def get_devices() -> Dict[str, soco.SoCo]: """Retrieve the dictionary of discovered Sonos devices. Returns: Dict[str, soco.SoCo]: A dictionary mapping device names to their respective SoCo objects. """ global devices if not devices: discover_devices() return devices def get_device(name: Optional[str] = None) -> soco.SoCo: """Retrieve a Sonos device by name or return the current device. Args: name: The name of the device to retrieve. If None, returns the current device. Returns: soco.SoCo: The Sonos device object. Raises: ValueError: If the specified device name is not found. """ global device if not name and device: return device devices = get_devices() if not name: device = devices[list(devices.keys())[0]] return device if name in devices: device = devices[name] return device for key in devices: if key.lower() == name.lower(): device = devices[key] return device raise ValueError(f"Device {name} not found") def get_info_from(device: soco.SoCo) -> Dict[str, Any]: """Retrieve detailed information from a Sonos device. Args: device: The Sonos device to retrieve information from. Returns: Dict[str, Any]: A dictionary containing the device's name, volume, state, and current track information. """ track_info = device.get_current_track_info() return { "name": device.player_name, "volume": device.volume, "state": device.get_current_transport_info()["current_transport_state"], "track": { "title": track_info.get("title"), "artist": track_info.get("artist"), "album": track_info.get("album"), "position": track_info.get("position"), "duration": track_info.get("duration"), "playlist_position": track_info.get("playlist_position"), "album_art": track_info.get("album_art") } } @mcp.tool() def get_all_device_states() -> List[Dict[str, Any]]: """Retrieve the state information for all discovered Sonos devices. Returns: List[Dict[str, Any]]: A list of dictionaries containing state information for each device. """ devices = get_devices() infos = [] for device in devices.values(): infos.append(get_info_from(device)) return infos @mcp.tool() def now_playing() -> List[Dict[str, str]]: """Retrieve information about currently playing tracks on all Sonos devices. Returns: List[Dict[str, str]]: A list of dictionaries containing the name, title, artist, and album of currently playing tracks. """ devices = get_devices() infos = [] for device in devices.values(): track = device.get_current_track_info() if not track: continue is_playing = device.get_current_transport_info()["current_transport_state"] == "PLAYING" if is_playing: infos.append({ "name": device.player_name, "title": track["title"], "artist": track["artist"], "album": track["album"] }) return infos @mcp.tool() def get_device_state(name: Optional[str] = None) -> Dict[str, Any]: """Retrieve the state information for a specific Sonos device. Args: name: The name of the device to retrieve state information for. If None, uses the current device. Returns: Dict[str, Any]: A dictionary containing the device's name, volume, state, and current track information. """ device = get_device(name) return { "name": device.player_name, "volume": device.volume, "state": device.get_current_transport_info()["current_transport_state"], "track": device.get_current_track_info() } @mcp.tool() def pause(name: Optional[str] = None) -> Dict[str, Any]: """Pause playback on a Sonos device. Args: name: The name of the device to pause. If None, uses the current device. Returns: Dict[str, Any]: The device's state after pausing, including name, volume, state, and track info. """ device = get_device(name) device.pause() return get_info_from(device) @mcp.tool() def stop(name: Optional[str] = None) -> Dict[str, Any]: """Stop playback on a Sonos device. Args: name: The name of the device to stop. If None, uses the current device. Returns: Dict[str, Any]: The device's state after stopping, including name, volume, state, and track info. """ device = get_device(name) device.stop() return get_info_from(device) @mcp.tool() def play(name: Optional[str] = None) -> Dict[str, Any]: """Start playback on a Sonos device. Args: name: The name of the device to start playback on. If None, uses the current device. Returns: Dict[str, Any]: The device's state after starting playback, including name, volume, state, and track info. """ device = get_device(name) device.play() return get_info_from(device) @mcp.tool() def next(name: Optional[str] = None) -> Dict[str, Any]: """Skip to the next track on a Sonos device. Args: name: The name of the device to skip the track on. If None, uses the current device. Returns: Dict[str, Any]: The device's state after skipping to the next track, including name, volume, state, and track info. """ device = get_device(name) device.next() return get_info_from(device) @mcp.tool() def previous(name: Optional[str] = None) -> Dict[str, Any]: """Skip to the previous track on a Sonos device. Args: name: The name of the device to skip the track on. If None, uses the current device. Returns: Dict[str, Any]: The device's state after skipping to the previous track, including name, volume, state, and track info. """ device = get_device(name) device.previous() return get_info_from(device) @mcp.tool() def get_queue(name: Optional[str] = None) -> List[Dict[str, Any]]: """Retrieve the queue of tracks for a Sonos device. Args: name: The name of the device to retrieve the queue from. If None, uses the current device. Returns: List[Dict[str, Any]]: A list of dictionaries containing track information in the queue. """ sonos = get_device(name) tracks = sonos.get_queue() current = int(sonos.get_current_track_info()['playlist_position']) return [{ "index": idx-1, "title": track.title, "artist": track.creator, "album": track.album, **({"current": True} if idx == current else {}) } for idx, track in enumerate(tracks, 1)] @mcp.tool() def mode( mode: Optional[Literal["NORMAL", "SHUFFLE_NOREPEAT", "SHUFFLE", "REPEAT_ALL"]] = None, name: Optional[str] = None ) -> str: """Get or set the play mode of a Sonos device. Args: mode: The play mode to set (e.g., "NORMAL", "SHUFFLE_NOREPEAT", "SHUFFLE", "REPEAT_ALL"). If None, returns the current mode. name: The name of the device to set the mode for. If None, uses the current device. Returns: str: The current play mode after the operation. """ device = get_device(name) if mode: device.play_mode = mode return device.play_mode @mcp.tool() def partymode() -> Dict[str, Any]: """Enable party mode on the current Sonos device. Returns: Dict[str, Any]: The device's state after enabling party mode, including name, volume, state, and track info. """ device = get_device() device.partymode() return get_info_from(device) @mcp.tool() def speaker_info(name: Optional[str] = None) -> Dict[str, str]: """Retrieve speaker information for a Sonos device. Args: name: The name of the device to retrieve speaker information from. If None, uses the current device. Returns: Dict[str, str]: A dictionary containing speaker information. """ return get_device(name).get_speaker_info() @mcp.tool() def get_current_track_info(name: Optional[str] = None) -> Dict[str, str]: """Retrieve current track information for a Sonos device. Args: name: The name of the device to retrieve track information from. If None, uses the current device. Returns: Dict[str, str]: A dictionary containing the current track's artist, title, album, playlist position, and duration. """ track = get_device(name).get_current_track_info() return { "artist": track['artist'], "title": track['title'], "album": track['album'], "playlist_position": track['playlist_position'], "duration": track['duration'] } @mcp.tool() def volume(volume: Optional[int] = None, name: Optional[str] = None) -> int: """Get or set the volume of a Sonos device. Args: volume: The volume level to set (0-99). If None, returns current volume. name: The name of the device to control. If None, uses the current device. Returns: int: The current volume level after the operation. Raises: ValueError: If volume is not between 0 and 99. ValueError: If the specified device is not found. """ device = get_device(name) if volume is not None: if not 0 <= volume <= 99: raise ValueError("Volume must be between 0 and 99") device.volume = volume return device.volume @mcp.tool() def skip(increment: int = 1, name: Optional[str] = None) -> Dict[str, Any]: """Skip tracks in the queue for a Sonos device. Args: increment: The number of tracks to skip forward. Defaults to 1. name: The name of the device to skip tracks on. If None, uses the current device. Returns: Dict[str, Any]: The device's state after skipping tracks, including name, volume, state, and track info. Raises: ValueError: If the new track position is out of the queue's range. """ sonos = get_device(name) current = int(sonos.get_current_track_info()['playlist_position']) new_index = current + increment queue_length = sonos.queue_size if not 0 <= new_index < queue_length: raise ValueError(f"Cannot skip to position {new_index}") sonos.play_from_queue(new_index) return get_info_from(sonos) @mcp.tool() def play_index(index: int, name: Optional[str] = None) -> Dict[str, Any]: """Play a specific track from the queue on a Sonos device. Args: index: The index of the track to play. name: The name of the device to play the track on. If None, uses the current device. Returns: Dict[str, Any]: The device's state after playing the specified track, including name, volume, state, and track info. Raises: ValueError: If the index is out of the queue's range. """ sonos = get_device(name).group.coordinator queue_length = sonos.queue_size if not 0 <= index <= queue_length: raise ValueError(f"Index {index} is not within range 1-{queue_length}") current = int(sonos.get_current_track_info()['playlist_position']) if index != current: sonos.play_from_queue(index) return get_info_from(sonos) @mcp.tool() def remove_index_from_queue(index: int, name: Optional[str] = None) -> List[Dict[str, Any]]: """Remove a specific track from the queue on a Sonos device. Args: index: The index of the track to remove. name: The name of the device to remove the track from. If None, uses the current device. Returns: List[Dict[str, Any]]: The updated queue after removing the track. Raises: ValueError: If the index is out of the queue's range. """ sonos = get_device(name).group.coordinator queue_length = sonos.queue_size if not 1 <= index <= queue_length: raise ValueError(f"Index {index} is not within range 1-{queue_length}") sonos.remove_from_queue(index) # Return the updated queue return get_queue(name) def is_index_in_queue(index, queue_length): """Helper function to verify if an index exists within the queue length. Args: index: The index to check. queue_length: The total length of the queue. Returns: bool: True if the index is within the queue length, False otherwise. """ if 0 <= index < queue_length: return True return False def fetch_queue_length(sonos): """Return the queue length for a Sonos device. Args: sonos: The Sonos device to retrieve the queue length from. Returns: int: The length of the queue. """ return sonos.queue_size @mcp.tool() def get_queue_length(name: Optional[str] = None) -> int: """Retrieve the queue length for a Sonos device. Args: name: The name of the device to retrieve the queue length from. If None, uses the current device. Returns: int: The length of the queue. """ return fetch_queue_length(get_device(name)) def main(): discover_devices() device = get_device() if __name__ == "__main__": main()

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/WinstonFassett/sonos-mcp-server'

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