Spotify MCP Custom MCP server for full Spotify Web API access โ€” playlist cleanup, library, listening history, playback

Custom MCP server at ~/.mcp-servers/spotify-mcp/server.py exposing 28 tools backed by the Spotify Web API. Built to back playlist-cleanup workflows (“dedupe”, “cap to 60”, “one per artist”, “atomic rewrite from a curated list”) plus general daily Spotify operation โ€” listening history, top tracks, library management, basic playback control. Authenticated via Authorization Code + PKCE with a refresh-token cache on disk.

Why this exists

The Cowork-managed Spotify connector is intentionally read/discover/create-only โ€” it can search the catalog, create playlists, and report what’s playing, but it cannot read existing playlist contents, remove tracks, or reorder. That’s a Spotify partner-integration policy, not a Claude limitation.

The four community Spotify MCPs on GitHub (josuemj, Carrieukie, marcelmarais, PeterAkande) all wrap the Web API directly with a self-managed OAuth app, which is where the destructive playlist ops live. Rather than depend on a third-party fork, this MCP follows the same pattern Ed uses for every other custom MCP (farm-data, netbird, memory, inaturalist) โ€” local Python + FastMCP under ~/.mcp-servers/, secrets in the Claude Desktop config env block.

Tool surface (28 tools)
Category Tools
Reads list_my_playlists, get_playlist, get_playlist_tracks, search, get_track, get_currently_playing, get_recently_played, get_top_tracks, get_top_artists, get_saved_tracks, check_library_contains, get_my_profile
Playlist writes create_playlist, update_playlist_details, add_tracks_to_playlist, remove_tracks_from_playlist, reorder_playlist_tracks, replace_playlist_tracks
Library writes save_tracks, remove_saved_tracks, save_to_library, remove_from_library
Playback pause, resume, skip_next, skip_previous, set_volume, add_to_queue

All write tools are explicit โ€” no “delete by criteria” footguns. replace_playlist_tracks is the canonical “rewrite this playlist with my curated list” operation: atomic 100-track PUT followed by 100-track POSTs for anything beyond. remove_tracks_from_playlist optionally takes a snapshot_id for concurrent-edit detection. Auto-pagination is built into add/remove/replace and the library writes (Spotify caps at 100 or 50 per call depending on endpoint).

OAuth setup (one-time, ~5 minutes)
  1. Create a Spotify Developer app at developer.spotify.com/dashboard:

    • App name: any (e.g. Ed's MCP)
    • App description: any
    • Redirect URI: http://127.0.0.1:8888/callback โ€” must match exactly, including the trailing /callback
    • APIs used: Web API (the only one needed)
    • Accept the developer terms
  2. Copy the Client ID from the app’s settings page. (Client Secret is not used โ€” PKCE doesn’t need it.)

  3. Patch claude_desktop_config.json โ€” the spotify-mcp block has a placeholder; replace REPLACE_WITH_CLIENT_ID_FROM_DEVELOPER_DASHBOARD with the real Client ID. The config lives at ~/Sync/ED/config/claude_desktop_config.json and Syncthing replicates it to both Macs.

  4. Run the login helper once to mint a refresh token:

    cd ~/.mcp-servers/spotify-mcp
    SPOTIFY_CLIENT_ID=<your-client-id> .venv/bin/python3 login.py
    

    It opens your default browser to Spotify’s consent screen, captures the redirect on 127.0.0.1:8888, exchanges the code for tokens, and writes them to .token-cache.json (chmod 600).

  5. Restart Claude Desktop fully (Cmd-Q, then relaunch) so the new MCP loads.

The login helper has to run on each machine that runs Claude Desktop (Studio + MacBook). The token cache file is per-machine โ€” simpler than syncing OAuth state across hosts. Pre-shared Syncthing exclusion: .token-cache.json is in .stignore via the per-folder .gitignore-style pattern.

OAuth scopes requested

The login helper requests all 14 scopes the MCP needs so future tool additions don’t require re-authentication:

Scope Purpose
playlist-read-private list + read private playlists
playlist-read-collaborative read collaborative playlists
playlist-modify-private create/edit private playlists
playlist-modify-public edit your public playlists
user-library-read read Liked Songs + saved albums
user-library-modify add/remove Liked Songs
user-read-currently-playing what’s playing
user-read-playback-state device + queue state
user-modify-playback-state pause/resume/skip/volume
user-read-recently-played listening history (last 50 only โ€” Spotify limit)
user-top-read top tracks + artists
user-read-private profile read (get_my_profile)
user-follow-read check whether artists/playlists are followed (check_library_contains)
user-follow-modify follow/unfollow artists + playlists via save_to_library / remove_from_library
Claude Desktop config block
"spotify-mcp": {
  "command": "/Users/bee/.mcp-servers/spotify-mcp/.venv/bin/python3",
  "args": ["/Users/bee/.mcp-servers/spotify-mcp/server.py"],
  "env": {
    "SPOTIFY_CLIENT_ID": "<your-client-id>",
    "SPOTIFY_REDIRECT_URI": "http://127.0.0.1:8888/callback"
  }
}

SPOTIFY_TOKEN_CACHE defaults to ~/.mcp-servers/spotify-mcp/.token-cache.json โ€” override only if you want the cache somewhere else.

Files on disk
Path What
~/.mcp-servers/spotify-mcp/server.py The MCP server itself (28 tools, ~700 lines)
~/.mcp-servers/spotify-mcp/login.py One-time PKCE OAuth helper
~/.mcp-servers/spotify-mcp/requirements.txt mcp>=1.0.0, httpx>=0.27, pydantic>=2
~/.mcp-servers/spotify-mcp/.venv/ Python 3.14 venv with deps
~/.mcp-servers/spotify-mcp/.token-cache.json Refresh token + cached access token (chmod 600, gitignored, per-machine)
~/.mcp-servers/spotify-mcp/.gitignore excludes .venv/, __pycache__/, .token-cache.json
Example: playlist cleanup workflow

The original use case. Open a chat and say something like “Help me clean up my ‘Drive Music’ playlist โ€” dedupe and cap to 90 minutes.” Claude can now:

  1. list_my_playlists โ†’ find the playlist id
  2. get_playlist_tracks(id, limit=100) (page until exhausted) โ†’ assemble the full track list with duration_ms + popularity + added_at
  3. Reason over the list in chat: identify duplicates, sort by your rule, trim to target length
  4. Confirm the cut list with you
  5. replace_playlist_tracks(id, curated_uris) โ€” atomic rewrite

Or for a sibling-playlist workflow (keep the original as backup):

  1. create_playlist("Drive Music โ€” cleaned") โ†’ new id
  2. add_tracks_to_playlist(new_id, curated_uris) โ†’ done
Health + observability

The MCP writes to ~/Library/Logs/Claude/mcp-server-spotify-mcp.log like every other server. check-mcp-health.py (runs 6ร—/day via com.bee.mcp-health-check) will pick up failures automatically.

Two known failure modes that don’t auto-recover:

  • Refresh token revoked. If you click “remove app” at spotify.com/account/apps the refresh token dies and every call returns HTTP 400 invalid_grant. Fix: re-run login.py.
  • Premium-only endpoints on a Free account. set_volume, pause/resume, queue ops require Premium. Tools return HTTP 403 Premium required. Not a bug.
February 2026 endpoint migration

On 2026-02-06 Spotify announced a reduced Web API surface for Development Mode apps; the changes hit existing integrations on 2026-03-09. Several legacy endpoints were silently removed โ€” they now return a bare HTTP 403 Forbidden with no body detail, which is genuinely misleading because it looks identical to a scope error. The MCP was hit by this on 2026-05-26 when every write tool started 403’ing despite a fully scoped + allowlisted token.

What changed:

Removed Replacement
POST /users/{user_id}/playlists POST /me/playlists
PUT /me/tracks (save Liked Songs) PUT /me/library with {"uris": [...]}
DELETE /me/tracks DELETE /me/library
POST /playlists/{id}/tracks POST /playlists/{id}/items
GET /playlists/{id}/tracks GET /playlists/{id}/items
PUT /playlists/{id}/tracks PUT /playlists/{id}/items
DELETE /playlists/{id}/tracks (body key tracks) DELETE /playlists/{id}/items (body key items)
GET /audio-features, GET /audio-analysis (none โ€” removed for new dev apps)
GET /search limit=50 GET /search limit=10 max

The response shape also changed: a playlist’s tracks.items.track is now items.items.item, and popularity / available_markets / linked_from are gone from track responses. User profile drops country, email, explicit_content, followers, product.

The server.py rewrite swapped every endpoint, renamed the response field handling, and dropped get_audio_features. Three new tools were added by piggy-backing on the new unified /me/library endpoint that accepts any URI type:

  • save_to_library(uris) โ€” saves tracks and albums, episodes, shows, audiobooks, plus follows artists/playlists, in one mixed-URI call
  • remove_from_library(uris) โ€” mirror of the save
  • check_library_contains(uris) โ€” unified “is this saved/followed?” check

The login.py scope list grew from 12 to 14, adding user-follow-read and user-follow-modify for artist/playlist follow operations via the new generic library endpoints. Existing tokens must be re-minted (re-run login.py) to pick up the new scopes; the migration itself is otherwise transparent.

Backups of the pre-migration code: ~/.mcp-servers/spotify-mcp/server.py.bak-feb2026-* and login.py.bak-feb2026-*.

References: