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.
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.
| 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).
-
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
- App name: any (e.g.
-
Copy the Client ID from the app’s settings page. (Client Secret is not used โ PKCE doesn’t need it.)
-
Patch
claude_desktop_config.jsonโ thespotify-mcpblock has a placeholder; replaceREPLACE_WITH_CLIENT_ID_FROM_DEVELOPER_DASHBOARDwith the real Client ID. The config lives at~/Sync/ED/config/claude_desktop_config.jsonand Syncthing replicates it to both Macs. -
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.pyIt 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). -
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.
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 |
"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.
| 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 |
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:
list_my_playlistsโ find the playlist idget_playlist_tracks(id, limit=100)(page until exhausted) โ assemble the full track list with duration_ms + popularity + added_at- Reason over the list in chat: identify duplicates, sort by your rule, trim to target length
- Confirm the cut list with you
replace_playlist_tracks(id, curated_uris)โ atomic rewrite
Or for a sibling-playlist workflow (keep the original as backup):
create_playlist("Drive Music โ cleaned")โ new idadd_tracks_to_playlist(new_id, curated_uris)โ done
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-runlogin.py. - Premium-only endpoints on a Free account.
set_volume,pause/resume, queue ops require Premium. Tools returnHTTP 403 Premium required. Not a bug.
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 callremove_from_library(uris)โ mirror of the savecheck_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: