import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { spotifyRequest, formatDuration, calculateProgress } from "./tools/spotify-api.js";
// Initialize Server
const server = new McpServer({
name: "spotify-mcp-server",
version: "1.0.0",
});
// ============================================================================
// BULK PLAYBACK STATE TOOLS
// ============================================================================
server.tool(
"spotify_get_playback_state",
"Get complete information about the user's current playback state, including track, artist, album, progress, device, and all settings. Returns comprehensive JSON data.",
{
market: z.string().optional().describe("ISO 3166-1 alpha-2 country code (e.g., 'US')"),
},
async ({ market }) => {
const data = await spotifyRequest("GET", "/me/player", undefined, { market });
if (!data) {
return { content: [{ type: "text", text: "No active playback found. Please start playing music on a Spotify device." }] };
}
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
server.tool(
"spotify_get_current_track",
"Get complete information about the currently playing track, including all metadata. Returns comprehensive JSON data.",
{
market: z.string().optional().describe("ISO 3166-1 alpha-2 country code"),
},
async ({ market }) => {
const data = await spotifyRequest("GET", "/me/player/currently-playing", undefined, { market });
if (!data) {
return { content: [{ type: "text", text: "Nothing currently playing." }] };
}
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
// ============================================================================
// GRANULAR PLAYBACK INFO TOOLS (Perfect for HUDs!)
// ============================================================================
server.tool(
"spotify_get_track_name",
"Get ONLY the name of the currently playing track. Returns plain text - perfect for HUD displays.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data || !data.item) {
return { content: [{ type: "text", text: "No track playing" }] };
}
return { content: [{ type: "text", text: data.item.name }] };
}
);
server.tool(
"spotify_get_artist_name",
"Get ONLY the primary artist name of the currently playing track. Returns plain text - perfect for HUD displays.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data || !data.item || !data.item.artists || data.item.artists.length === 0) {
return { content: [{ type: "text", text: "No artist" }] };
}
return { content: [{ type: "text", text: data.item.artists[0].name }] };
}
);
server.tool(
"spotify_get_all_artists",
"Get all artist names of the currently playing track (comma-separated). Returns plain text.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data || !data.item || !data.item.artists) {
return { content: [{ type: "text", text: "No artists" }] };
}
const artists = data.item.artists.map((a: any) => a.name).join(", ");
return { content: [{ type: "text", text: artists }] };
}
);
server.tool(
"spotify_get_album_name",
"Get ONLY the album name of the currently playing track. Returns plain text - perfect for HUD displays.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data || !data.item || !data.item.album) {
return { content: [{ type: "text", text: "No album" }] };
}
return { content: [{ type: "text", text: data.item.album.name }] };
}
);
server.tool(
"spotify_get_track_duration_ms",
"Get ONLY the total duration of the current track in milliseconds. Returns number as text.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data || !data.item) {
return { content: [{ type: "text", text: "0" }] };
}
return { content: [{ type: "text", text: data.item.duration_ms.toString() }] };
}
);
server.tool(
"spotify_get_track_duration_formatted",
"Get ONLY the total duration of the current track in formatted time (M:SS or H:MM:SS). Returns formatted text - perfect for HUD displays.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data || !data.item) {
return { content: [{ type: "text", text: "0:00" }] };
}
return { content: [{ type: "text", text: formatDuration(data.item.duration_ms) }] };
}
);
server.tool(
"spotify_get_track_progress_ms",
"Get ONLY the current playback position in milliseconds. Returns number as text.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data) {
return { content: [{ type: "text", text: "0" }] };
}
return { content: [{ type: "text", text: (data.progress_ms || 0).toString() }] };
}
);
server.tool(
"spotify_get_track_progress_formatted",
"Get ONLY the current playback position in formatted time (M:SS or H:MM:SS). Returns formatted text - perfect for HUD displays.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data) {
return { content: [{ type: "text", text: "0:00" }] };
}
return { content: [{ type: "text", text: formatDuration(data.progress_ms || 0) }] };
}
);
server.tool(
"spotify_get_time_remaining_ms",
"Get ONLY the time remaining in the current track in milliseconds. Returns number as text - perfect for countdown timers.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data || !data.item) {
return { content: [{ type: "text", text: "0" }] };
}
const remaining = data.item.duration_ms - (data.progress_ms || 0);
return { content: [{ type: "text", text: remaining.toString() }] };
}
);
server.tool(
"spotify_get_time_remaining_formatted",
"Get ONLY the time remaining in the current track in formatted time (M:SS or H:MM:SS). Returns formatted text - perfect for HUD countdown displays.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data || !data.item) {
return { content: [{ type: "text", text: "0:00" }] };
}
const remaining = data.item.duration_ms - (data.progress_ms || 0);
return { content: [{ type: "text", text: formatDuration(remaining) }] };
}
);
server.tool(
"spotify_get_track_progress_percentage",
"Get ONLY the playback progress as a percentage (0-100). Returns number as text - perfect for progress bars in HUDs.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data || !data.item) {
return { content: [{ type: "text", text: "0" }] };
}
const percentage = calculateProgress(data.progress_ms || 0, data.item.duration_ms);
return { content: [{ type: "text", text: percentage.toString() }] };
}
);
server.tool(
"spotify_get_track_uri",
"Get ONLY the Spotify URI of the currently playing track. Returns URI as text.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data || !data.item) {
return { content: [{ type: "text", text: "No URI" }] };
}
return { content: [{ type: "text", text: data.item.uri }] };
}
);
server.tool(
"spotify_get_track_id",
"Get ONLY the Spotify ID of the currently playing track. Returns ID as text.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data || !data.item) {
return { content: [{ type: "text", text: "No ID" }] };
}
return { content: [{ type: "text", text: data.item.id }] };
}
);
server.tool(
"spotify_get_album_art_url",
"Get ONLY the URL of the album artwork for the currently playing track. Returns the largest available image URL - perfect for displaying in HUDs.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data || !data.item || !data.item.album || !data.item.album.images || data.item.album.images.length === 0) {
return { content: [{ type: "text", text: "No artwork" }] };
}
// Return the first (largest) image
return { content: [{ type: "text", text: data.item.album.images[0].url }] };
}
);
server.tool(
"spotify_get_track_explicit",
"Get ONLY whether the current track is explicit. Returns 'true' or 'false' as text.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data || !data.item) {
return { content: [{ type: "text", text: "false" }] };
}
return { content: [{ type: "text", text: data.item.explicit ? "true" : "false" }] };
}
);
server.tool(
"spotify_get_track_popularity",
"Get ONLY the popularity score of the current track (0-100). Returns number as text.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data || !data.item) {
return { content: [{ type: "text", text: "0" }] };
}
return { content: [{ type: "text", text: (data.item.popularity || 0).toString() }] };
}
);
// ============================================================================
// GRANULAR DEVICE TOOLS
// ============================================================================
server.tool(
"spotify_get_device_name",
"Get ONLY the name of the currently active playback device. Returns plain text - perfect for HUD displays.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player");
if (!data || !data.device) {
return { content: [{ type: "text", text: "No active device" }] };
}
return { content: [{ type: "text", text: data.device.name }] };
}
);
server.tool(
"spotify_get_device_type",
"Get ONLY the type of the currently active playback device (Computer, Smartphone, Speaker, etc.). Returns plain text.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player");
if (!data || !data.device) {
return { content: [{ type: "text", text: "Unknown" }] };
}
return { content: [{ type: "text", text: data.device.type }] };
}
);
server.tool(
"spotify_get_device_volume",
"Get ONLY the current volume percentage of the active device (0-100). Returns number as text - perfect for volume displays.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player");
if (!data || !data.device) {
return { content: [{ type: "text", text: "0" }] };
}
return { content: [{ type: "text", text: (data.device.volume_percent || 0).toString() }] };
}
);
server.tool(
"spotify_get_device_id",
"Get ONLY the ID of the currently active playback device. Returns ID as text.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player");
if (!data || !data.device) {
return { content: [{ type: "text", text: "No device ID" }] };
}
return { content: [{ type: "text", text: data.device.id }] };
}
);
server.tool(
"spotify_is_device_active",
"Get ONLY whether a device is currently active. Returns 'true' or 'false' as text.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player");
if (!data || !data.device) {
return { content: [{ type: "text", text: "false" }] };
}
return { content: [{ type: "text", text: data.device.is_active ? "true" : "false" }] };
}
);
server.tool(
"spotify_get_available_devices",
"Get a list of all available Spotify devices. Returns JSON array of devices with their details.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/devices");
if (!data || !data.devices) {
return { content: [{ type: "text", text: "No devices found" }] };
}
return { content: [{ type: "text", text: JSON.stringify(data.devices, null, 2) }] };
}
);
// ============================================================================
// GRANULAR PLAYBACK STATE TOOLS
// ============================================================================
server.tool(
"spotify_is_playing",
"Get ONLY whether music is currently playing. Returns 'true' or 'false' as text - perfect for play/pause button states in HUDs.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player/currently-playing");
if (!data) {
return { content: [{ type: "text", text: "false" }] };
}
return { content: [{ type: "text", text: data.is_playing ? "true" : "false" }] };
}
);
server.tool(
"spotify_get_shuffle_state",
"Get ONLY whether shuffle is enabled. Returns 'true' or 'false' as text - perfect for shuffle button states in HUDs.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player");
if (!data) {
return { content: [{ type: "text", text: "false" }] };
}
return { content: [{ type: "text", text: data.shuffle_state ? "true" : "false" }] };
}
);
server.tool(
"spotify_get_repeat_mode",
"Get ONLY the current repeat mode. Returns 'off', 'track', or 'context' as text - perfect for repeat button states in HUDs.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player");
if (!data) {
return { content: [{ type: "text", text: "off" }] };
}
return { content: [{ type: "text", text: data.repeat_state || "off" }] };
}
);
server.tool(
"spotify_get_context_type",
"Get ONLY the type of playback context (album, artist, playlist, etc.). Returns context type as text.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player");
if (!data || !data.context) {
return { content: [{ type: "text", text: "No context" }] };
}
return { content: [{ type: "text", text: data.context.type }] };
}
);
server.tool(
"spotify_get_context_uri",
"Get ONLY the URI of the playback context (album, playlist, etc.). Returns URI as text.",
{},
async () => {
const data: any = await spotifyRequest("GET", "/me/player");
if (!data || !data.context) {
return { content: [{ type: "text", text: "No context" }] };
}
return { content: [{ type: "text", text: data.context.uri }] };
}
);
// ============================================================================
// PLAYBACK CONTROL TOOLS
// ============================================================================
server.tool(
"spotify_play",
"Start or resume playback. Can optionally specify device, context (album/playlist URI), specific tracks, or starting position.",
{
device_id: z.string().optional().describe("The ID of the device to target"),
context_uri: z.string().optional().describe("Spotify URI of the context to play (album, artist, playlist)"),
uris: z.array(z.string()).optional().describe("Array of track URIs to play"),
position_ms: z.number().optional().describe("Position in milliseconds to start playback"),
},
async ({ device_id, context_uri, uris, position_ms }) => {
const body: any = {};
if (context_uri) body.context_uri = context_uri;
if (uris) body.uris = uris;
if (position_ms !== undefined) body.position_ms = position_ms;
await spotifyRequest("PUT", "/me/player/play", body, { device_id });
return { content: [{ type: "text", text: "Playback started/resumed" }] };
}
);
server.tool(
"spotify_pause",
"Pause playback on the user's account.",
{
device_id: z.string().optional().describe("The ID of the device to target"),
},
async ({ device_id }) => {
await spotifyRequest("PUT", "/me/player/pause", undefined, { device_id });
return { content: [{ type: "text", text: "Playback paused" }] };
}
);
server.tool(
"spotify_next",
"Skip to the next track in the user's queue.",
{
device_id: z.string().optional().describe("The ID of the device to target"),
},
async ({ device_id }) => {
await spotifyRequest("POST", "/me/player/next", undefined, { device_id });
return { content: [{ type: "text", text: "Skipped to next track" }] };
}
);
server.tool(
"spotify_previous",
"Skip to the previous track in the user's queue.",
{
device_id: z.string().optional().describe("The ID of the device to target"),
},
async ({ device_id }) => {
await spotifyRequest("POST", "/me/player/previous", undefined, { device_id });
return { content: [{ type: "text", text: "Skipped to previous track" }] };
}
);
server.tool(
"spotify_seek",
"Seek to a specific position in the currently playing track.",
{
position_ms: z.number().describe("The position in milliseconds to seek to"),
device_id: z.string().optional().describe("The ID of the device to target"),
},
async ({ position_ms, device_id }) => {
await spotifyRequest("PUT", "/me/player/seek", undefined, { position_ms, device_id });
return { content: [{ type: "text", text: `Seeked to position ${formatDuration(position_ms)}` }] };
}
);
server.tool(
"spotify_set_shuffle",
"Toggle shuffle mode on or off.",
{
state: z.boolean().describe("true to enable shuffle, false to disable"),
device_id: z.string().optional().describe("The ID of the device to target"),
},
async ({ state, device_id }) => {
await spotifyRequest("PUT", "/me/player/shuffle", undefined, { state, device_id });
return { content: [{ type: "text", text: `Shuffle ${state ? 'enabled' : 'disabled'}` }] };
}
);
server.tool(
"spotify_set_repeat",
"Set the repeat mode for playback.",
{
state: z.enum(["track", "context", "off"]).describe("'track' = repeat current track, 'context' = repeat playlist/album, 'off' = no repeat"),
device_id: z.string().optional().describe("The ID of the device to target"),
},
async ({ state, device_id }) => {
await spotifyRequest("PUT", "/me/player/repeat", undefined, { state, device_id });
return { content: [{ type: "text", text: `Repeat mode set to: ${state}` }] };
}
);
// ============================================================================
// VOLUME AND DEVICE CONTROL TOOLS
// ============================================================================
server.tool(
"spotify_set_volume",
"Set the volume for the user's current playback device.",
{
volume_percent: z.number().min(0).max(100).describe("Volume level from 0 to 100"),
device_id: z.string().optional().describe("The ID of the device to target"),
},
async ({ volume_percent, device_id }) => {
await spotifyRequest("PUT", "/me/player/volume", undefined, { volume_percent, device_id });
return { content: [{ type: "text", text: `Volume set to ${volume_percent}%` }] };
}
);
server.tool(
"spotify_transfer_playback",
"Transfer playback to a different device.",
{
device_id: z.string().describe("The ID of the device to transfer playback to"),
play: z.boolean().optional().describe("true to ensure playback starts on the new device, false to keep current state"),
},
async ({ device_id, play }) => {
await spotifyRequest("PUT", "/me/player", { device_ids: [device_id], play });
return { content: [{ type: "text", text: `Playback transferred to device ${device_id}` }] };
}
);
// ============================================================================
// QUEUE MANAGEMENT TOOLS
// ============================================================================
server.tool(
"spotify_get_queue",
"Get the user's current playback queue, including currently playing and upcoming tracks.",
{},
async () => {
const data = await spotifyRequest("GET", "/me/player/queue");
if (!data) {
return { content: [{ type: "text", text: "Could not retrieve queue" }] };
}
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
server.tool(
"spotify_add_to_queue",
"Add a track to the user's playback queue.",
{
uri: z.string().describe("The Spotify URI of the track to add (e.g., spotify:track:...)"),
device_id: z.string().optional().describe("The ID of the device to target"),
},
async ({ uri, device_id }) => {
await spotifyRequest("POST", "/me/player/queue", undefined, { uri, device_id });
return { content: [{ type: "text", text: `Added track to queue: ${uri}` }] };
}
);
server.tool(
"spotify_get_recently_played",
"Get tracks from the user's recently played history.",
{
limit: z.number().optional().describe("Number of items to return (max 50, default 20)"),
},
async ({ limit }) => {
const data = await spotifyRequest("GET", "/me/player/recently-played", undefined, { limit: limit || 20 });
if (!data) {
return { content: [{ type: "text", text: "Could not retrieve recently played tracks" }] };
}
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
// ============================================================================
// SEARCH AND DISCOVERY TOOLS
// ============================================================================
server.tool(
"spotify_search",
"Search for tracks, albums, artists, or playlists on Spotify.",
{
query: z.string().describe("The search query (track name, artist, album, etc.)"),
type: z.array(z.enum(["track", "album", "artist", "playlist"])).describe("Types to search for"),
limit: z.number().optional().describe("Number of results per type (max 50, default 10)"),
},
async ({ query, type, limit }) => {
const data = await spotifyRequest("GET", "/search", undefined, {
q: query,
type: type.join(','),
limit: limit || 10
});
if (!data) {
return { content: [{ type: "text", text: "Search failed" }] };
}
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
server.tool(
"spotify_get_track_info",
"Get detailed information about a specific track by its Spotify ID.",
{
track_id: z.string().describe("The Spotify ID of the track"),
},
async ({ track_id }) => {
const data = await spotifyRequest("GET", `/tracks/${track_id}`);
if (!data) {
return { content: [{ type: "text", text: "Track not found" }] };
}
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
server.tool(
"spotify_get_album_info",
"Get detailed information about a specific album by its Spotify ID.",
{
album_id: z.string().describe("The Spotify ID of the album"),
},
async ({ album_id }) => {
const data = await spotifyRequest("GET", `/albums/${album_id}`);
if (!data) {
return { content: [{ type: "text", text: "Album not found" }] };
}
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
server.tool(
"spotify_get_artist_info",
"Get detailed information about a specific artist by their Spotify ID.",
{
artist_id: z.string().describe("The Spotify ID of the artist"),
},
async ({ artist_id }) => {
const data = await spotifyRequest("GET", `/artists/${artist_id}`);
if (!data) {
return { content: [{ type: "text", text: "Artist not found" }] };
}
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
server.tool(
"spotify_get_artist_top_tracks",
"Get an artist's top tracks.",
{
artist_id: z.string().describe("The Spotify ID of the artist"),
market: z.string().optional().describe("ISO 3166-1 alpha-2 country code (e.g., 'US')"),
},
async ({ artist_id, market }) => {
const data = await spotifyRequest("GET", `/artists/${artist_id}/top-tracks`, undefined, { market: market || 'US' });
if (!data) {
return { content: [{ type: "text", text: "Could not retrieve top tracks" }] };
}
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
// ============================================================================
// LIBRARY TOOLS
// ============================================================================
server.tool(
"spotify_get_saved_tracks",
"Get the user's saved (liked) tracks.",
{
limit: z.number().optional().describe("Number of items to return (max 50, default 20)"),
offset: z.number().optional().describe("The index of the first item to return (default 0)"),
},
async ({ limit, offset }) => {
const data = await spotifyRequest("GET", "/me/tracks", undefined, { limit: limit || 20, offset: offset || 0 });
if (!data) {
return { content: [{ type: "text", text: "Could not retrieve saved tracks" }] };
}
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
server.tool(
"spotify_save_track",
"Save (like) a track to the user's library.",
{
track_id: z.string().describe("The Spotify ID of the track to save"),
},
async ({ track_id }) => {
await spotifyRequest("PUT", "/me/tracks", { ids: [track_id] });
return { content: [{ type: "text", text: `Track ${track_id} saved to your library` }] };
}
);
server.tool(
"spotify_remove_saved_track",
"Remove (unlike) a track from the user's library.",
{
track_id: z.string().describe("The Spotify ID of the track to remove"),
},
async ({ track_id }) => {
await spotifyRequest("DELETE", "/me/tracks", { ids: [track_id] });
return { content: [{ type: "text", text: `Track ${track_id} removed from your library` }] };
}
);
server.tool(
"spotify_get_playlists",
"Get the user's playlists.",
{
limit: z.number().optional().describe("Number of items to return (max 50, default 20)"),
offset: z.number().optional().describe("The index of the first item to return (default 0)"),
},
async ({ limit, offset }) => {
const data = await spotifyRequest("GET", "/me/playlists", undefined, { limit: limit || 20, offset: offset || 0 });
if (!data) {
return { content: [{ type: "text", text: "Could not retrieve playlists" }] };
}
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
server.tool(
"spotify_get_playlist",
"Get detailed information about a specific playlist.",
{
playlist_id: z.string().describe("The Spotify ID of the playlist"),
},
async ({ playlist_id }) => {
const data = await spotifyRequest("GET", `/playlists/${playlist_id}`);
if (!data) {
return { content: [{ type: "text", text: "Playlist not found" }] };
}
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
// ============================================================================
// START THE SERVER
// ============================================================================
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`
╔════════════════════════════════════════════════════════════╗
║ Spotify MCP Server - Production Ready v1.0.0 ║
╚════════════════════════════════════════════════════════════╝
✓ Server running on stdio
✓ 51 tools available
✓ Ready for dynamic HUD integration
Tools categories:
• Bulk State Tools (2)
• Granular Track Info (16)
• Granular Device Info (6)
• Granular Playback State (5)
• Playback Control (7)
• Volume & Device Control (2)
• Queue Management (3)
• Search & Discovery (5)
• Library Management (5)
`);
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});