MCP Servers

A collection of Model Context Protocol servers, templates, tools and more.

Claude based let claude create Playlist and choose your music via Spotify

Created 3/12/2026
Updated about 19 hours ago
Repository documentation and setup instructions

Spotify-MCP

Claude based let claude create Playlist and choose your music via Spotify

Spotify MCP Setup Guide

For Cloudflare Workers + Claude MCP Integration

Last updated: March 8, 2026


What Works

All features working: Play, pause, skip, queue tracks, search, now playing, recently played, top tracks/artists, get recommendations, create playlists, add tracks to playlists, remove tracks, update playlists

⚠️ Critical: February 2026 Spotify API Change

Spotify renamed all playlist track management endpoints from /tracks to /items. If you're getting 403 errors on add/remove/get playlist tracks, this is why. The code below has been updated with the fix.


Prerequisites

  • Cloudflare account (free tier works)
  • Node.js installed locally
  • Wrangler CLI: npm install -g wrangler
  • Spotify account (free or premium)

Part 1: Create Spotify Developer App

  1. Go to developer.spotify.com/dashboard
  2. Click "Create App"
  3. Fill in:
    • App name: Whatever you want (e.g., "Saints Jukebox")
    • App description: Anything
    • Redirect URI: https://YOUR-WORKER-NAME.YOUR-SUBDOMAIN.workers.dev/callback
    • Check "Web API"
  4. Save. Copy the Client ID.
  5. Click "View client secret" and copy that too.
  6. Go to "User Management" tab → Add your Spotify email address

Part 2: Create Cloudflare Worker

  1. In terminal, create project folder:
mkdir spotify-mcp && cd spotify-mcp
  1. Login to Cloudflare:
wrangler login
  1. Create wrangler.toml file:
name = "spotify-mcp"
main = "src/index.js"
compatibility_date = "2024-01-01"

[[kv_namespaces]]
binding = "SPOTIFY_KV"
id = "YOUR_KV_ID_HERE"
  1. Create KV namespace:
wrangler kv:namespace create SPOTIFY_KV

Copy the ID it gives you into wrangler.toml

  1. Create src folder:
mkdir src

Then create src/index.js with the worker code (see Part 6).


Part 3: Set Environment Variables

Run these commands (replace with your actual values):

wrangler secret put SPOTIFY_CLIENT_ID

(paste your client ID when prompted)

wrangler secret put SPOTIFY_CLIENT_SECRET

(paste your client secret when prompted)

wrangler secret put SPOTIFY_REDIRECT_URI

(paste: https://YOUR-WORKER.workers.dev/callback)


Part 4: Deploy and Authorize

  1. Deploy the worker:
wrangler deploy
  1. Go to: https://YOUR-WORKER.workers.dev/auth
  2. Log in with your Spotify account
  3. Accept ALL permissions (playlists, playback, etc.)
  4. You'll be redirected to /callback — copy the refresh token shown
  5. Set the refresh token:
wrangler secret put SPOTIFY_REFRESH_TOKEN

(paste the refresh token)

  1. Redeploy:
wrangler deploy

Part 5: Connect to Claude

  1. In Claude.ai, go to Settings → Connectors
  2. Add MCP Server
  3. URL: https://YOUR-WORKER.workers.dev/mcp
  4. Give it a name
  5. Test it: "What's playing on Spotify?"

Troubleshooting

"Invalid access token" or 401 errors:

  • Go to Cloudflare Dashboard → Workers → your worker → KV
  • Delete the "access_token" key
  • Try again (it will auto-refresh)

403 Forbidden on add_tracks / remove_tracks / get_playlist_tracks:

  • FIXED! Spotify changed the endpoint from /tracks to /items in February 2026
  • Update all playlist item endpoints: /playlists/{id}/tracks → /playlists/{id}/items
  • This applies to: add tracks, remove tracks, get playlist tracks, reorder items

Scopes missing:

  • Go to spotify.com/account/apps → Remove access for your app
  • Delete access_token and refresh_token_backup from KV
  • Go through /auth again
  • Make sure you see ALL permissions on the consent screen
  • Get new refresh token and update the secret

Part 6: The Worker Code

Create src/index.js with this code:

// Spotify MCP Worker for Cloudflare
// Full OAuth + MCP implementation

const SCOPES = [
  "user-read-playback-state",
  "user-modify-playback-state", 
  "user-read-currently-playing",
  "playlist-read-private",
  "playlist-read-collaborative",
  "playlist-modify-public",
  "playlist-modify-private",
  "user-read-recently-played",
  "user-top-read"
].join(" ");

async function getAccessToken(env) {
  // Check KV cache first
  const cached = await env.SPOTIFY_KV.get("access_token");
  if (cached) {
    const data = JSON.parse(cached);
    if (data.expires_at > Date.now() + 60000) {
      return data.access_token;
    }
  }

  // Refresh the token
  const response = await fetch("https://accounts.spotify.com/api/token", {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      "Authorization": "Basic " + btoa(env.SPOTIFY_CLIENT_ID + ":" + env.SPOTIFY_CLIENT_SECRET)
    },
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token: env.SPOTIFY_REFRESH_TOKEN
    })
  });

  const data = await response.json();
  if (!response.ok) {
    throw new Error(`Token refresh failed: ${JSON.stringify(data)}`);
  }

  // Cache it
  await env.SPOTIFY_KV.put("access_token", JSON.stringify({
    access_token: data.access_token,
    expires_at: Date.now() + (data.expires_in * 1000)
  }));

  // If Spotify sent a new refresh token, back it up
  if (data.refresh_token) {
    await env.SPOTIFY_KV.put("refresh_token_backup", data.refresh_token);
  }

  return data.access_token;
}

async function spotifyFetch(env, endpoint, options = {}) {
  const token = await getAccessToken(env);
  const url = endpoint.startsWith("http") ? endpoint : `https://api.spotify.com/v1${endpoint}`;
  
  const response = await fetch(url, {
    ...options,
    headers: {
      "Authorization": `Bearer ${token}`,
      "Content-Type": "application/json",
      ...options.headers
    }
  });

  if (response.status === 204) return { success: true };
  
  const text = await response.text();
  if (!response.ok) {
    throw new Error(`Spotify API error (${response.status}): ${text}`);
  }
  
  return text ? JSON.parse(text) : { success: true };
}

const TOOLS = [
  { name: "spotify_now_playing", description: "Get currently playing track", inputSchema: { type: "object", properties: {} } },
  { name: "spotify_play", description: "Start/resume playback", inputSchema: { type: "object", properties: { track_uri: { type: "string" }, playlist_uri: { type: "string" }, device_id: { type: "string" } } } },
  { name: "spotify_pause", description: "Pause playback", inputSchema: { type: "object", properties: {} } },
  { name: "spotify_skip", description: "Skip track", inputSchema: { type: "object", properties: { direction: { type: "string", enum: ["next", "previous"], default: "next" } } } },
  { name: "spotify_queue", description: "Add track to queue", inputSchema: { type: "object", properties: { track_uri: { type: "string" } }, required: ["track_uri"] } },
  { name: "spotify_search", description: "Search Spotify", inputSchema: { type: "object", properties: { query: { type: "string" }, type: { type: "string", enum: ["track", "album", "artist", "playlist"], default: "track" }, limit: { type: "number", default: 10 } }, required: ["query"] } },
  { name: "spotify_recently_played", description: "Get recent tracks", inputSchema: { type: "object", properties: { limit: { type: "number", default: 20 } } } },
  { name: "spotify_top_tracks", description: "Get top tracks", inputSchema: { type: "object", properties: { time_range: { type: "string", enum: ["short_term", "medium_term", "long_term"], default: "medium_term" }, limit: { type: "number", default: 20 } } } },
  { name: "spotify_top_artists", description: "Get top artists", inputSchema: { type: "object", properties: { time_range: { type: "string", enum: ["short_term", "medium_term", "long_term"], default: "medium_term" }, limit: { type: "number", default: 20 } } } },
  { name: "spotify_get_recommendations", description: "Get recommendations", inputSchema: { type: "object", properties: { seed_tracks: { type: "array", items: { type: "string" } }, seed_artists: { type: "array", items: { type: "string" } }, seed_genres: { type: "array", items: { type: "string" } }, limit: { type: "number", default: 20 } } } },
  { name: "spotify_get_playlists", description: "Get user playlists", inputSchema: { type: "object", properties: { limit: { type: "number", default: 20 }, offset: { type: "number", default: 0 } } } },
  { name: "spotify_get_playlist_tracks", description: "Get playlist tracks", inputSchema: { type: "object", properties: { playlist_id: { type: "string" }, limit: { type: "number", default: 50 }, offset: { type: "number", default: 0 } }, required: ["playlist_id"] } },
  { name: "spotify_create_playlist", description: "Create playlist", inputSchema: { type: "object", properties: { name: { type: "string" }, description: { type: "string" }, public: { type: "boolean", default: false } }, required: ["name"] } },
  { name: "spotify_add_tracks", description: "Add tracks to playlist", inputSchema: { type: "object", properties: { playlist_id: { type: "string" }, track_uris: { type: "array", items: { type: "string" } }, position: { type: "number" } }, required: ["playlist_id", "track_uris"] } },
  { name: "spotify_remove_tracks", description: "Remove tracks from playlist", inputSchema: { type: "object", properties: { playlist_id: { type: "string" }, track_uris: { type: "array", items: { type: "string" } } }, required: ["playlist_id", "track_uris"] } },
  { name: "spotify_update_playlist", description: "Update playlist details", inputSchema: { type: "object", properties: { playlist_id: { type: "string" }, name: { type: "string" }, description: { type: "string" }, public: { type: "boolean" } }, required: ["playlist_id"] } }
];

async function handleTool(name, args, env) {
  switch (name) {
    case "spotify_now_playing": {
      const data = await spotifyFetch(env, "/me/player/currently-playing");
      if (!data || !data.item) return { playing: false };
      return {
        playing: data.is_playing,
        track: data.item.name,
        artist: data.item.artists.map(a => a.name).join(", "),
        album: data.item.album.name,
        progress_ms: data.progress_ms,
        duration_ms: data.item.duration_ms,
        uri: data.item.uri
      };
    }

    case "spotify_play": {
      const body = {};
      if (args.track_uri) body.uris = [args.track_uri];
      if (args.playlist_uri) body.context_uri = args.playlist_uri;
      const endpoint = args.device_id ? `/me/player/play?device_id=${args.device_id}` : "/me/player/play";
      await spotifyFetch(env, endpoint, { method: "PUT", body: JSON.stringify(body) });
      return { success: true };
    }

    case "spotify_pause":
      await spotifyFetch(env, "/me/player/pause", { method: "PUT" });
      return { success: true };

    case "spotify_skip":
      const dir = args.direction === "previous" ? "previous" : "next";
      await spotifyFetch(env, `/me/player/${dir}`, { method: "POST" });
      return { success: true };

    case "spotify_queue":
      await spotifyFetch(env, `/me/player/queue?uri=${encodeURIComponent(args.track_uri)}`, { method: "POST" });
      return { success: true };

    case "spotify_search": {
      const type = args.type || "track";
      const limit = args.limit || 10;
      const data = await spotifyFetch(env, `/search?q=${encodeURIComponent(args.query)}&type=${type}&limit=${limit}`);
      const key = type + "s";
      return data[key]?.items || [];
    }

    case "spotify_recently_played": {
      const data = await spotifyFetch(env, `/me/player/recently-played?limit=${args.limit || 20}`);
      return data.items.map(i => ({ track: i.track.name, artist: i.track.artists[0].name, played_at: i.played_at, uri: i.track.uri }));
    }

    case "spotify_top_tracks": {
      const data = await spotifyFetch(env, `/me/top/tracks?time_range=${args.time_range || "medium_term"}&limit=${args.limit || 20}`);
      return data.items.map(t => ({ name: t.name, artist: t.artists[0].name, uri: t.uri }));
    }

    case "spotify_top_artists": {
      const data = await spotifyFetch(env, `/me/top/artists?time_range=${args.time_range || "medium_term"}&limit=${args.limit || 20}`);
      return data.items.map(a => ({ name: a.name, genres: a.genres, uri: a.uri, id: a.id }));
    }

    case "spotify_get_recommendations": {
      const params = new URLSearchParams();
      if (args.seed_tracks?.length) params.set("seed_tracks", args.seed_tracks.join(","));
      if (args.seed_artists?.length) params.set("seed_artists", args.seed_artists.join(","));
      if (args.seed_genres?.length) params.set("seed_genres", args.seed_genres.join(","));
      params.set("limit", args.limit || 20);
      const data = await spotifyFetch(env, `/recommendations?${params}`);
      return data.tracks.map(t => ({ name: t.name, artist: t.artists[0].name, uri: t.uri, id: t.id }));
    }

    case "spotify_get_playlists": {
      const data = await spotifyFetch(env, `/me/playlists?limit=${args.limit || 20}&offset=${args.offset || 0}`);
      return data.items.map(p => ({ name: p.name, id: p.id, tracks: p.tracks.total, uri: p.uri }));
    }

    case "spotify_get_playlist_tracks": {
      const data = await spotifyFetch(env, `/playlists/${args.playlist_id}/items?limit=${args.limit || 50}&offset=${args.offset || 0}`);
      return data.items.map(i => ({ track: i.track.name, artist: i.track.artists[0].name, uri: i.track.uri }));
    }

    case "spotify_create_playlist": {
      const data = await spotifyFetch(env, "/me/playlists", {
        method: "POST",
        body: JSON.stringify({ name: args.name, description: args.description || "", public: args.public || false })
      });
      return { id: data.id, name: data.name, url: data.external_urls.spotify };
    }

    case "spotify_add_tracks": {
      const uris = args.track_uris.join(",");
      const url = `https://api.spotify.com/v1/playlists/${args.playlist_id}/items?uris=${encodeURIComponent(uris)}`;
      const token = await getAccessToken(env);
      const response = await fetch(url, {
        method: "POST",
        headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }
      });
      const text = await response.text();
      if (!response.ok) throw new Error(`Spotify API error (${response.status}): ${text}`);
      return JSON.parse(text);
    }

    case "spotify_remove_tracks": {
      await spotifyFetch(env, `/playlists/${args.playlist_id}/items`, {
        method: "DELETE",
        body: JSON.stringify({ tracks: args.track_uris.map(uri => ({ uri })) })
      });
      return { success: true };
    }

    case "spotify_update_playlist": {
      const body = {};
      if (args.name) body.name = args.name;
      if (args.description !== undefined) body.description = args.description;
      if (args.public !== undefined) body.public = args.public;
      await spotifyFetch(env, `/playlists/${args.playlist_id}`, { method: "PUT", body: JSON.stringify(body) });
      return { success: true };
    }

    default:
      throw new Error(`Unknown tool: ${name}`);
  }
}

async function handleMCP(request, env) {
  const body = await request.json();
  
  if (body.method === "initialize") {
    return { jsonrpc: "2.0", id: body.id, result: { protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "spotify-mcp", version: "1.0.0" } } };
  }
  
  if (body.method === "tools/list") {
    return { jsonrpc: "2.0", id: body.id, result: { tools: TOOLS } };
  }
  
  if (body.method === "tools/call") {
    try {
      const result = await handleTool(body.params.name, body.params.arguments || {}, env);
      return { jsonrpc: "2.0", id: body.id, result: { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] } };
    } catch (error) {
      return { jsonrpc: "2.0", id: body.id, error: { code: -32000, message: error.message } };
    }
  }

  return { jsonrpc: "2.0", id: body.id, error: { code: -32601, message: "Method not found" } };
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // Auth endpoint - initiates OAuth
    if (url.pathname === "/auth") {
      const authUrl = new URL("https://accounts.spotify.com/authorize");
      authUrl.searchParams.set("client_id", env.SPOTIFY_CLIENT_ID);
      authUrl.searchParams.set("response_type", "code");
      authUrl.searchParams.set("redirect_uri", env.SPOTIFY_REDIRECT_URI);
      authUrl.searchParams.set("scope", SCOPES);
      return Response.redirect(authUrl.toString(), 302);
    }

    // Callback - exchanges code for tokens
    if (url.pathname === "/callback") {
      const code = url.searchParams.get("code");
      if (!code) return new Response("No code provided", { status: 400 });

      const response = await fetch("https://accounts.spotify.com/api/token", {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          "Authorization": "Basic " + btoa(env.SPOTIFY_CLIENT_ID + ":" + env.SPOTIFY_CLIENT_SECRET)
        },
        body: new URLSearchParams({
          grant_type: "authorization_code",
          code,
          redirect_uri: env.SPOTIFY_REDIRECT_URI
        })
      });

      const data = await response.json();
      if (!response.ok) return new Response(JSON.stringify(data), { status: 400 });

      return new Response(`
        <h1>Success!</h1>
        <p>Copy this refresh token and set it as SPOTIFY_REFRESH_TOKEN:</p>
        <pre style="background:#f0f0f0;padding:10px;word-break:break-all">${data.refresh_token}</pre>
        <p>Run: wrangler secret put SPOTIFY_REFRESH_TOKEN</p>
      `, { headers: { "Content-Type": "text/html" } });
    }

    // MCP endpoint
    if (url.pathname === "/mcp") {
      if (request.method === "OPTIONS") {
        return new Response(null, { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST", "Access-Control-Allow-Headers": "*" } });
      }
      const result = await handleMCP(request, env);
      return new Response(JSON.stringify(result), { headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } });
    }

    return new Response("Spotify MCP - Use /auth to authorize, /mcp for MCP calls", { status: 200 });
  }
};

Built by Kay + Saint + Tyson after 72+ hours of debugging.

Update March 8, 2026: Jace identified the fix — Spotify changed /tracks to /items in February 2026. Confirmed working.

Quick Setup
Installation guide for this server

Installation Command (package not published)

git clone https://github.com/kaydartistry-maker/Spotify-MCP
Manual Installation: Please check the README for detailed setup instructions and any additional dependencies required.

Cursor configuration (mcp.json)

{ "mcpServers": { "kaydartistry-maker-spotify-mcp": { "command": "git", "args": [ "clone", "https://github.com/kaydartistry-maker/Spotify-MCP" ] } } }