spotify-mcp
# spotify-mcp
[](https://github.com/Pacolias/spotify-mcp/actions/workflows/tests.yml)
[](LICENSE)
An MCP (Model Context Protocol) server for interacting with the Spotify Web API, built with Python and FastAPI. Runs locally — an MCP host (Claude Desktop, Claude Code, etc.) launches it as a subprocess on your own machine.
This is a portfolio project by [Paco Molina](https://pacomolina.dev). The reasoning behind every architecture decision — and why — is logged in [`journal/`](journal/), one entry per decision, in the order they were made.
## Example
A real exchange with `import_youtube_playlist`, pulling new tracks from a YouTube mix's chapter list into an existing Spotify playlist (see [journal entry 24](journal/24-youtube-playlist-import.md) for the full story):
> **You:** Pull any new songs from this YouTube mix into my "Chill" playlist: youtube.com/watch?v=...
>
> **Claude:** *(calls `import_youtube_playlist`)* Found 14 tracklist entries in the video's chapters. 7 matched confidently on Spotify and were added; 7 were skipped — 4 pointed at the same wrong song (likely unreleased tracks not on Spotify), 2 didn't match any plausible artist, and 1 was a non-track marker ("LOOP"). Your playlist went from 3 to 10 tracks.
## How it works
[MCP](https://modelcontextprotocol.io/) is an open protocol that lets an AI client (like Claude) talk to external tools and data sources through a standard interface, instead of custom one-off integrations. A client connects to a server, and the server exposes three kinds of things: **tools** — functions the model actively decides to call, with a name, description, and typed schema generated from the function's signature and docstring; **resources** — read-only, URI-addressed data (e.g. `spotify://me/now-playing`) meant to be listed and attached to context more like a referenced document than an invoked action; and **prompts** — reusable prompt templates the host can surface directly to the user (often as a quick-access menu) that kick off a specific workflow with a pre-written message.
This project is two small, single-purpose local programs:
- **The MCP server itself** (`spotify_mcp.cli`, the `spotify-mcp` command), talking to its host over **stdio** — the host process launches it and communicates over stdin/stdout, no network involved. This is what an MCP-compatible client actually connects to.
- **A local login helper** (`spotify_mcp.main`, a small FastAPI app), run separately, that handles the Spotify OAuth2 (Authorization Code + PKCE) flow at `/auth/login` and `/auth/callback`. Spotify data (like "what's currently playing") is user-specific, so a logged-in user's access token is needed to call the Spotify API on their behalf. The login step needs a browser and an HTTP redirect regardless of how the MCP server itself talks to its host — that's a property of OAuth, not of MCP transport — so it's kept as its own small process rather than folded into the stdio one.
Once logged in, the access/refresh token pair is stored in a small SQLite database, shared by both processes, and transparently refreshed when it's close to expiring.
```mermaid
graph TD
Host["MCP Host<br/>(e.g. Claude Desktop / Claude Code)"]
Browser["User's Browser"]
subgraph Stdio["MCP server process (stdio)"]
MCPServer["mcp_server.run()<br/>stdio transport"]
Tools["MCP tools<br/>search, now_playing, top_tracks, ..."]
Resources["MCP resources<br/>spotify://me/now-playing, ..."]
Prompts["MCP prompts<br/>build_playlist, listening_recap, ..."]
end
subgraph LoginApp["Login helper (FastAPI, run separately)"]
AuthRoutes["/auth/login, /auth/callback<br/>(OAuth2 + PKCE)"]
end
SpotifyClient["Spotify client (httpx)"]
DB[("SQLite<br/>spotify_mcp.db")]
SpotifyAPI["api.spotify.com"]
SpotifyAuth["accounts.spotify.com"]
Host -->|"spawns as subprocess, stdin/stdout"| MCPServer
MCPServer --> Tools
MCPServer --> Resources
MCPServer --> Prompts
Tools --> SpotifyClient
Resources --> SpotifyClient
Prompts -.->|"guides toward"| Tools
Browser --> AuthRoutes
AuthRoutes -->|"redirect + code"| SpotifyAuth
SpotifyAuth -->|"redirect back"| AuthRoutes
AuthRoutes -->|"store token"| DB
SpotifyClient -->|"read / refresh token"| DB
SpotifyClient -->|"Authorization: Bearer access_token"| SpotifyAPI
```
### Available tools
| Tool | Description |
|---|---|
| `ping` | Health check — confirms the MCP server is reachable. |
| `search_track` | Search Spotify's catalog for tracks matching a query. |
| `now_playing` | Get the track currently playing on the logged-in user's account, if any. |
| `top_tracks` | Get the user's most-listened-to tracks (short/medium/long term). |
| `top_artists` | Get the user's most-listened-to artists (short/medium/long term). |
| `recently_played` | Get the user's most recently played tracks. |
| `list_user_playlists` | List the logged-in user's playlists. |
| `playlist_tracks` | List the tracks in a playlist. |
| `create_user_playlist` | Create a new playlist. ⚠️ `public=False` is currently ignored by a confirmed bug on Spotify's side — playlists are created public regardless. |
| `add_tracks` | Add one or more tracks to a playlist. |
| `remove_tracks` | Remove one or more tracks from a playlist. |
| `find_playlists` | Discover existing curated Spotify playlists matching a query (name/owner/description only — can't read tracks of playlists you don't own). |
| `import_youtube_playlist` | Build a new Spotify playlist from a YouTube video's tracklist (chapters/description — no audio recognition). ⚠️ Same `public=False` limitation as `create_user_playlist`. |
| `shuffle` | Turn shuffle mode on or off. |
| `repeat_mode` | Set repeat mode (track/context/off). |
| `seek` | Seek to a position in the currently playing track. |
| `liked_songs` | List the user's saved ("Liked Songs") tracks. |
| `like_tracks` | Save tracks to Liked Songs. ⚠️ requires Spotify's Extended Quota Mode — fails with a permissions error on apps without it (like this one, by default). |
| `unlike_tracks` | Remove tracks from Liked Songs. Same restriction as `like_tracks`. |
| `list_devices` | List Spotify devices already open (phone, desktop, web player, ...) and which is active. |
| `activate_device` | Switch playback to a specific device. Can't launch Spotify itself — only controls devices already running somewhere. |
| `pause` | Pause playback on the active device. |
| `resume` | Resume/start playback on the active device. |
| `skip_next` | Skip to the next track. |
| `skip_previous` | Skip to the previous track. |
| `set_playback_volume` | Set playback volume (0-100). |
| `queue_track` | Add a track to the playback queue. |
### Available resources
Read-only, returned as JSON.
| Resource | Description |
|---|---|
| `spotify://me/now-playing` | The track currently playing, if any. |
| `spotify://me/playlists` | The logged-in user's playlists. |
| `spotify://playlist/{playlist_id}` | The tracks in a specific playlist. |
| `spotify://me/profile` | Basic profile: id, display name, followers, URL/image. |
| `spotify://me/dashboard` | One-shot snapshot: now playing, devices, top tracks, recently played. |
### Available prompts
Quick-access templates a host can surface to kick off a workflow.
| Prompt | Description |
|---|---|
| `build_playlist` | Build a themed playlist using the model's own music knowledge (not generic search phrases). |
| `listening_recap` | A friendly summary of current listening, via the dashboard resource. |
| `import_youtube_mix` | Import a YouTube video's tracklist into a new playlist. |
| `start_listening` | Get music playing — check what's active, or help pick a device if nothing is. |
| `curate_from_liked` | Build a new playlist from a themed subset of the user's Liked Songs. |
## Setup
Requires Python 3.14+ and a Spotify account.
**1. Install dependencies** — pick one:
```bash
uv sync # with uv (recommended)
```
```bash
pip install -r requirements.txt # or with plain pip — kept in sync with pyproject.toml via `uv export`
```
**2. Configure and log in:**
```bash
cp .env.example .env # then set SPOTIFY_CLIENT_ID in .env — see below
uv run spotify-mcp login # opens your browser, log in once
```
(Installed with plain `pip`? Drop the `uv run` prefix — just `spotify-mcp login`.)
> `login` opens a real browser window on the machine it runs on and waits for the OAuth redirect to reach `127.0.0.1`. Run it on your own local machine — it won't work over SSH or in a remote/headless sandbox with no browser to open.
**`SPOTIFY_CLIENT_ID`**: [create a Spotify app](https://developer.spotify.com/dashboard) → add `http://127.0.0.1:8000/auth/callback` as a Redirect URI → check **Web API** → copy the Client ID (no secret needed, this uses PKCE) → paste into `.env`.
**Register it with your MCP host:**
**Claude Code** — from the repo root:
```bash
claude mcp add spotify-mcp -- uv run --directory "$(pwd)" spotify-mcp
```
**Other hosts** (Claude Desktop, etc.) — add to the host's MCP config:
```json
{
"mcpServers": {
"spotify-mcp": {
"command": "uv",
"args": ["run", "--directory", "/absolute/path/to/spotify-mcp", "spotify-mcp"]
}
}
}
```
## Testing
```bash
uv run pytest
```
Tests run against an isolated, throwaway SQLite database and mocked Spotify API responses — they never touch a real Spotify account or the local `spotify_mcp.db`. See [journal entry 11](journal/11-test-suite.md) for details.
## Linting and formatting
```bash
uv run ruff check . # lint
uv run ruff format . # format
uv run mypy # type check
```
Both run in CI (see the badge above). To run them automatically before each commit:
```bash
uv run pre-commit install
```
## License
[MIT](LICENSE) — see the license file for details.
## Author
Built by [Paco Molina](https://pacomolina.dev).
TDQS
Scored across 27 tools
Most tools clearly target a distinct resource and action: devices, playlists, playback modes, liked songs, and search are easy to tell apart. The only minor overlaps are resume vs activate_device (both can start playback) and add_tracks vs queue_track, but their targets are clearly described.
Snake_case is used consistently and many tools follow a verb_noun pattern such as list_devices, create_user_playlist, and unlike_tracks. However, several tools are noun or state phrases instead of commands (playlist_tracks, liked_songs, top_tracks, repeat_mode), and similar operations use different verbs like list_user_playlists vs find_playlists.
27 tools is heavy and just past the 25 threshold, but Spotify's domain spans playback, playlists, library, and search, so most tools have a distinct purpose. A few tools like ping and the many single-purpose playback controls add bulk and could be consolidated.
Core workflows are covered: searching tracks, creating and editing playlists, controlling playback, and viewing listening history. Obvious gaps include no playlist update/delete/reorder, no ability to play a specific playlist or album context, and no artist/album search, so some lifecycle flows dead-end.