Last.fm Taste MCP
Provides tools for accessing and analyzing a Last.fm user's listening history, including top artists, tracks, albums, recent scrobbles, taste profiles, listening sessions, and personalized recommendations, with support for syncing and searching the full scrobble history.
Enriches Last.fm data with MusicBrainz metadata, including canonical artist/album/track identities, MBIDs, tags, similar artists, and tracklists, enabling deeper context and album exposure analysis.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Last.fm Taste MCPWhat are my top artists for the last 6 months?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Last.fm Taste MCP
A personal Last.fm MCP server built with Node.js and TypeScript. It uses stateless Streamable HTTP, maintains a canonical local listening index, combines Last.fm with MusicBrainz metadata, records explicit preferences, and produces evidence-backed taste analytics and recommendations.
The default deployment uses read-only Last.fm and MusicBrainz methods. Optional Spotify integration adds private library reads, playlist creation, and bidirectional likes/loves synchronization. Spotify likes can automatically become Last.fm loves on startup and hourly. API credentials stay inside the container and are never returned through MCP. Feedback, exclusions, and recommendation events are written only to the local SQLite database.
See Spotify setup and automatic likes sync for the optional integration.
Features
MCP tool | Purpose |
| Public profile, total play count, and library size |
| Compact summary for |
| Top artists for a period |
| Top tracks for a period |
| Top albums for a period |
| Recent scrobbles with optional time filtering |
| Artist, album, or track search using the local index or a bounded live scan |
| SQLite index coverage and freshness |
| Full history backfill or incremental sync |
| Artist and track share changes between two periods |
| Core artists, favorite tracks and albums, discoveries, forgotten favorites, and listening patterns |
| Tags, similar artists, play counts, and a short biography context |
| Canonical artist/album/track identities, aliases, and known MBIDs |
| Fast unheard/sample/explored/established/favorite checks |
| Active days/months, sessions, returns, concentration, and explainable affinity |
| Session grouping with configurable inactivity gap |
| Track coverage, ordered runs, stopping points, and returns using MusicBrainz tracklists |
| Arbitrary UTC ranges and day/week/month/year buckets by artist, album, or tag |
| Pageable sparse time-bucket × artist/album matrix with global window totals, active days, concentration, and explicit coverage |
| Statistical change points in monthly listening distributions |
| Combined Last.fm tags/similarity and MusicBrainz metadata/relationships |
| Artists, albums, tags, sessions, eras, external similarity, and preference edges |
| Structured love/like/mixed/boring/dislike/not-now feedback |
| Explicit -5..5 signals for atmosphere, groove, melody, structure, vocals, and more |
| Feedback, dimension summaries, and active exclusions |
| Safe, bridge, or explore recommendations with evidence, risks, and starting points |
| Permanent, six-month, or new-releases-only artist exclusion |
| Active recommendation exclusions |
| Store an externally issued recommendation and its exposure baseline |
| Measure post-recommendation sampling, engagement, and later returns |
Informational tools are marked read-only. sync_listening_history, feedback/preference recording, exclusions, and recommendation recording are explicitly annotated as local writes. get_recommendations also records every emitted recommendation so it can be evaluated later.
Related MCP server: lastfm-mcp
1. Get a Last.fm API key
Sign in to your Last.fm account.
Enter an application name and description. This server does not require a callback URL.
Copy the API key.
Get your username from your profile URL:
https://www.last.fm/user/<username>.
Last.fm also displays a shared secret. The original read-only tools do not need it: user.getInfo, user.getTop*, user.getRecentTracks, user.getLovedTracks, and artist.getInfo do not require a user session. To sync Spotify likes into Last.fm loves, set LASTFM_API_SECRET and authorize Last.fm using the CLI described below.
MusicBrainz does not require an API key. It does require a meaningful User-Agent; set MUSICBRAINZ_USER_AGENT to an application name/version plus your public URL or email. The client serializes calls and defaults to one request every 1.1 seconds.
2. Configure and run with Docker Compose
cp .env.example .envAt minimum, set:
LASTFM_API_KEY=your-api-key
LASTFM_USERNAME=your-lastfm-username
MCP_ALLOWED_HOSTS=localhost,127.0.0.1
MCP_ENABLE_MUTATIONS=false
MUSICBRAINZ_USER_AGENT=lastfm-mcp/0.3.0 (https://your-domain.example/)Start the service:
docker compose up -d --build
docker compose ps
curl http://127.0.0.1:3000/healthzMCP endpoint: http://127.0.0.1:3000/mcp.
Test protocol and tool discovery:
curl -sS http://127.0.0.1:3000/mcp \
-X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'The response may be JSON or an SSE event: message; both are valid Streamable HTTP MCP responses.
3. Index your listening history once
Summary, top-chart, recent-track, and taste tools work immediately without an index. However, Last.fm does not provide server-side listening-history search by artist, album, or track. Exact search and reliable recentDiscoveries therefore require a full backfill:
docker compose exec lastfm-mcp node dist/src/sync.js full 250000Afterward, periodically fetch new scrobbles:
docker compose exec lastfm-mcp node dist/src/sync.js incremental 10000You can also invoke the MCP tool sync_listening_history (when MCP_ENABLE_MUTATIONS=true) and inspect progress with get_history_status. The container CLI remains available regardless of that MCP safety flag.
If a full sync reaches HISTORY_MAX_SYNC_TRACKS, call it again. The server persists the oldest backfill cursor and resumes from it; it does not redownload the same newest slice. Incremental syncs with a capped backlog advance oldest-first so no middle segment is skipped. coveredThroughAt advances only after the requested range is complete.
The index is stored in the lastfm-data named volume as normalized SQLite data rather than raw Last.fm responses. Intelligence tools lazily backfill canonical entity keys and alias catalogs after each sync, so an existing v0.1 database is migrated in place.
The full sync is foundational: without it, first-listen dates, long-term returns, eras, exposure filtering, and recommendation evaluation can be incomplete. Every affected response includes the current history status and a caveat when fullHistorySynced=false.
Exact window and era analysis
get_listening_matrix is the raw statistical surface for custom era analysis. It selects artist or album columns using totals across the complete requested window—not a separate top list inside each bucket—and returns a compact sparse coordinate matrix:
bucketscontains UTC boundaries, total plays, active days, selected plays, and omitted plays;entitiescontains whole-window totals, active days/buckets, first and last play, peak bucket/day concentration, active span, and bucket density;matrix.cellsuses[bucketIndex, entityIndex, plays, activeDays]; a missing cell means zero plays;filteringreports exact play coverage, page position, and every omission caused byminPlaysor entity pagination.
The default bucket=month, dimension=artist, and minPlays=1 starts an exact month × artist matrix in globally ranked pages of 250 entities. No per-month top-N truncation is applied. Re-call the tool with entityOffset=filtering.nextEntityOffset until that value is null; entity rank remains global while index addresses the current page's matrix cells. Increase limitEntities up to 5,000 when the client can accept a larger response. The ordinary text result stays compact while the complete page is returned in structuredContent, preserving ChatGPT context. If a sparse page exceeds maxCells, the call fails with narrowing options instead of silently dropping evidence. activeDays, maxDayShare, and bucketDensity help distinguish a one-evening spike from gradual discovery and recurring affinity.
Last.fm can expose imported or undated records with placeholder Unix timestamps near the 1970 epoch. The matrix and era detector keep those rows in the local history but exclude timestamps before 2002-01-01T00:00:00Z from temporal evidence. Responses expose minimumTimestamp and excludedBeforeMinimumTimestamp, so this cleanup is explicit rather than silently rewriting dates or deleting plays.
4. Make the endpoint reachable by ChatGPT
ChatGPT cannot connect directly to localhost; it needs a remote HTTPS endpoint. The recommended setup is to keep the Compose port bound to 127.0.0.1 and run a reverse proxy such as Caddy on the same server:
mcp.example.com {
reverse_proxy 127.0.0.1:3000
}Add the public hostname to .env without a scheme or port:
MCP_ALLOWED_HOSTS=mcp.example.com,localhost,127.0.0.1Apply the change and verify HTTPS:
docker compose up -d --build
curl https://mcp.example.com/healthzIf the server remains on a private network or local machine, use OpenAI Secure MCP Tunnel instead of exposing an arbitrary public port.
5. Connect it to ChatGPT
Current flow in ChatGPT web:
Enable Developer mode under
Settings → Apps → Advanced Settings. A workspace admin or owner may need to allow it first.Open
Settings → Apps → Create.Enter a name such as
My Last.fmand the endpointhttps://mcp.example.com/mcp. For this deployment, usehttps://lastfm.mcp.sptm.online/mcp.Select No authentication only for a read-only deployment or an endpoint already protected by a private tunnel. Use an OAuth 2.1 gateway before enabling mutations on a public hostname.
Click Scan Tools, wait for all 30 tools to appear, and create the app. If the app was created against an older version, rescan or recreate it so ChatGPT discovers the new tools.
Enable the app from the tools menu in a new chat.
Example first prompt:
Call get_history_status first. If fullHistorySynced=true, build a detailed music taste profile with get_taste_profile and compare_listening_periods for 12month versus 3month. Separate facts from interpretations.
Recommendation-oriented prompt:
Check my exposure and explicit feedback first. Then call get_recommendations in bridge mode, exclude anything above sampled exposure, explain each evidence path and risk, and give me one album plus three tracks to start with.
Deep-history prompt:
Call get_listening_matrix for my complete history with bucket=month, dimension=artist, and minPlays=1. Continue through entityOffset pages until filtering.nextEntityOffset is null. Combine columns by entity rank/key, then use the sparse matrix, active-day evidence, and normalized monthly shares to calculate change points, identify artists shared by adjacent eras, and distinguish one-evening spikes from gradual discoveries. Report aggregate coverage before interpreting the result, then compare your boundaries with detect_listening_eras.
Developer mode and custom MCP app availability depend on your plan and workspace settings. See the official ChatGPT instructions for current details.
Authentication and mutation safety
ChatGPT custom apps should not rely on an arbitrary user-supplied API key or header. LASTFM_API_KEY remains server-side, but it is not client authentication.
MCP_ENABLE_MUTATIONS=false is the safe default. The server still advertises all original Last.fm tools, but sync, feedback, preference, exclusion, recommendation generation/recording, and private feedback/recommendation reads reject calls; the taste graph omits explicit preference edges. Enable them only behind trusted access control such as a private Secure MCP Tunnel or OAuth 2.1 gateway. A public no-auth endpoint with mutations enabled lets any caller read or alter your local preference database and trigger expensive syncs. Query-string tokens are intentionally unsupported because URLs are commonly recorded in logs and browser history.
Spotify tools are separately opt-in with MCP_ENABLE_SPOTIFY_TOOLS=true, because even their read operations expose private account data. Manual Spotify/Last.fm writes additionally require MCP_ENABLE_MUTATIONS=true. The automatic Spotify → Last.fm scheduler is controlled independently by SPOTIFY_AUTO_SYNC_ENABLED; it does not expose a public trigger or require enabling MCP writes.
Configuration
Variable | Default | Purpose |
| required | Last.fm API key |
| required | Fixed user whose data is exposed by the MCP server |
| official endpoint | Primarily useful for tests |
|
| Bind address inside the container |
|
| Port inside the container |
|
| Host address used to publish the Compose port |
|
| Published host port |
| required for | Host and Origin allowlist for DNS rebinding protection |
|
| Enable sync, feedback, exclusions, and recommendation recording only behind trusted access control |
|
| Timeout for one API request |
|
| Retry count for temporary and rate-limit errors |
|
| Minimum delay between Last.fm requests, approximately four requests per second |
|
| In-memory cache duration for chart and info calls |
| official WS/2 endpoint | Primarily useful for tests |
| project URL | Required MusicBrainz application identity/contact |
|
| Timeout for one MusicBrainz request |
|
| Retry count for temporary MusicBrainz errors |
|
| Serialized MusicBrainz request interval |
|
| SQLite database path |
|
| Maximum live scan size when no index exists |
|
| Safety cap for one resumable sync call |
The from and to parameters accept Unix seconds, a UTC date such as 2026-08-01, or ISO 8601 with an explicit timezone such as 2026-08-01T00:00:00Z. A date-only from means 00:00:00 UTC; a date-only inclusive to means 23:59:59 UTC. Ambiguous local date-times without Z or a UTC offset are rejected.
Intelligence methodology
Canonicalization uses NFKC Unicode normalization, locale-independent case folding, punctuation spacing normalization, conservative trailing
feat.removal for artist credits, and conservative remaster/deluxe suffix removal for albums. Track qualifiers remain distinct.Exposure levels are explicit heuristics: zero plays is
unheard, 1–10 issampled, and a one-track/one-day repeat remainssampledeven above ten plays. Broader trials areexplored, distributed returns areestablished, and high sustained exposure isfavorite.Artist affinity scores play depth, active-day/month breadth, returning sessions, 30-day returns, and distribution. Each component and weight is returned; album-completion evidence is reported separately rather than silently folded into the score.
Sessions default to a 45-minute inactivity gap. Album completion is only classified when MusicBrainz supplies an ordered tracklist; otherwise completion remains unknown rather than fabricated.
Timeline buckets use UTC. Era boundaries compare monthly artist distributions statistically and preserve genuine inactive-month gaps.
Recommendation
confidencemeasures evidence coverage/consistency, not the probability that the user will like an artist. Risks always disclose missing audio-feature evidence and weak/single-cluster support.safefavors strong similarity to established seeds,bridgerequires links to at least two tag-derived/provisional seed clusters, andexplorefavors grounded but more moderate similarity. Prior recommendation outcomes and artist-level feedback adjust ranking; album/track dislikes only remove that starting item, not the whole artist.
Persistent local data
The SQLite volume stores:
complete normalized scrobbles and sync status;
canonical artist, album, and track catalogs plus aliases;
explicit feedback and taste-dimension signals;
recommendation exclusions and expiration policies;
recommendation events, baseline exposure, and evaluation inputs.
This data is personal. Back up the lastfm-data Docker volume, and do not expose a no-auth deployment if its listening history or feedback should remain private.
Taste profile methodology
coreArtists: all-time top artists with play counts for the last three months.trend: when the local index has sufficient coverage, the most recent 30 days are compared with the preceding non-overlapping 30 days. Otherwise, the server approximately compares normalized three-month and overall shares.favoriteTracks: all-time top tracks, recent plays, and thelovedsignal.recentDiscoveries: the exact first-listen date is available only after a complete, current sync; otherwise the result is explicitly marked as an approximation.forgottenFavorites: strong all-time artists with almost no plays in the recent three-month chart.repeatHeavy: unique-track ratio and the top-ten track share across the last 90 days or the available sample.albumOriented: album metadata coverage and the share of consecutive transitions within the same album.
Every profile response contains confidence and caveat fields so the model can distinguish evidence from heuristics.
Last.fm API limitations
user.getRecentTracksreturns at most 200 items per page and may include a now-playing item without a timestamp.Artist, album, and track history search is performed locally because Last.fm provides no equivalent server-side filter.
MBIDs are frequently empty; the fallback identity is built from normalized names.
MusicBrainz metadata is community-edited and may not resolve local files, obscure editions, or ambiguous names. The response reports when no ordered tracklist is available.
MusicBrainz relationships are metadata, not a general similarity graph. Candidate generation currently uses Last.fm similar artists and the local taste graph.
Spotify audio features and scraped recommendation sites are intentionally not used. They require separate credentials, licensing, or scraping decisions and should be integrated explicitly rather than silently.
Last.fm does not publish a fixed numeric rate limit. The client limits its request rate and retries temporary errors
11,16, and29, as well as HTTP429and5xx, with backoff.Images are intentionally neither returned nor cached because the API Terms place separate restrictions on artwork and image use.
For commercial or research use, review the Last.fm API Terms and contact Last.fm if required.
Official methods and APIs: Last.fm REST API, user.getRecentTracks, user.getTopArtists, user.getTopTracks, user.getTopAlbums, user.getLovedTracks, user.getInfo, and MusicBrainz Web Service.
Local development
npm install
cp .env.example .env
npm run devRun checks:
npm run check
npm audit --omit=dev
docker compose buildThe project uses the official MCP TypeScript SDK v2, the Express adapter with Host and Origin validation, Node.js 24, and the built-in node:sqlite module.
Spotify setup and automatic likes sync
Configuration and authorization
Create an app with Web API access in the Spotify developer dashboard. In Development Mode, the app owner needs Premium and the account using the app must have access in its user settings. See the Development Mode migration guide.
Register the exact redirect URI
http://127.0.0.1:8888/callback. Spotify permits HTTP for explicit loopback IPs, but notlocalhost; other redirects require HTTPS. See redirect URI rules.Set
SPOTIFY_CLIENT_ID,SPOTIFY_CLIENT_SECRET, andSPOTIFY_REDIRECT_URIin.env. SetLASTFM_API_SECRETto the shared secret belonging to your existingLASTFM_API_KEY.Authorize both services once. For local development, set
HISTORY_DB_PATH=./data/lastfm.sqliteso credential files are written into the local ignoreddata/directory:
npm run auth -- spotify
npm run auth -- lastfmFor Docker, rebuild/recreate the service to apply code and .env changes, then use the same persistent container volume:
docker compose up -d --build
docker compose exec lastfm-mcp npm run auth:built -- spotify
docker compose exec lastfm-mcp npm run auth:built -- lastfm
docker compose restart lastfm-mcpSpotify authorization prints a browser URL. Approve it, then paste the full redirect URL into the waiting CLI prompt within ten minutes. The browser may show connection refused at 127.0.0.1:8888; that is expected because this flow reads the URL manually and opens no callback listener. It works when the CLI runs over SSH or in Docker and the browser runs on your computer. Do not paste the URL into a chat or put it in shell history. OAuth state is random, expires, and is consumed once.
The Last.fm command prints an authorization link and waits for Enter after approval. It exchanges the authorized token for a session and refuses to save a session belonging to a different LASTFM_USERNAME. Last.fm passwords are never requested. See Last.fm desktop authentication and track.love.
If authentication succeeds but Docker reports a missing session, check where the CLI ran. /app/data on the host and /app/data inside the container are different storage locations: Compose mounts a named volume only inside the container. Run the auth:built commands via docker compose exec above so the server reads the same files. The CLI prints the saved file path and warns when host authentication uses Docker-style /app/ paths.
Spotify requests these scopes:
user-library-read user-library-modify user-read-recently-played user-top-read
playlist-read-private playlist-read-collaborative
playlist-modify-private playlist-modify-publicThe access token refreshes automatically, including one refresh after an API 401. Rotated refresh tokens are saved; an omitted refresh token preserves the existing one. Revoked authorization requires running the CLI again. Tokens/session keys use atomic files with mode 0600, defaulting to spotify-tokens.json and lastfm-session.json beside HISTORY_DB_PATH. Docker's /app/data volume preserves them across restarts. Custom paths must also be private, persistent, and excluded from source control. Tokens are not encrypted on disk.
Variable | Default | Purpose |
| Unset | Both required to enable the integration |
|
| Exact registered OAuth redirect |
| Beside | Persistent Spotify token file |
| Unset | Required for Last.fm authentication and loving tracks |
| Beside | Persistent authorized Last.fm session |
|
| Automatic Spotify → Last.fm writes |
|
| Delay after each completed run; range 60–86400 |
|
| Minimum pause between Spotify Web API requests in the shared queue |
|
| Maximum queue wait before rejecting an operation locally |
|
| Successful GET response cache; sync always refreshes liked-library pages |
|
| Successful catalog search cache, including empty results |
|
| Expose private Spotify and cross-provider MCP tools |
|
| Permit manual remote writes with |
Automatic synchronization
With both accounts authorized and SPOTIFY_AUTO_SYNC_ENABLED=true, the server reconciles Spotify Liked Songs with Last.fm loved tracks on startup, then one hour after each completed run. Set the interval to 300 for polling about every five minutes. This implementation polls saved tracks; it does not receive an instant like notification.
The first successful run imports the entire existing liked library, not just future likes. To preview first, set SPOTIFY_AUTO_SYNC_ENABLED=false, authorize, enable Spotify MCP tools behind trusted access, and call sync_spotify_likes_to_lastfm with dryRun=true. Then set the flag to true and recreate the container. Updating .env requires docker compose up -d; restart alone does not reload container environment variables.
Both directions are additive: no likes/loves are removed. If you manually unlove a track on Last.fm while keeping it liked on Spotify, the next automatic run can love it again. Last.fm → Spotify runs only when explicitly called. Automatic runs do not require enabling public MCP tools or MCP_ENABLE_MUTATIONS.
Each run reads full libraries with pagination, resolves missing identities, and writes only confident matches. Source duplicates are collapsed. A process-wide service guard prevents overlapping syncs; the scheduler waits for a run to finish before arming the next timer. Failed/unresolved items are reconsidered next time. A restart always reconciles current libraries, so there is no fragile timestamp watermark that can miss older likes. Run one server replica per credential store; no distributed lock is implemented.
The latest automatic result is available through get_music_sync_status when Spotify tools are enabled, in the log event spotify_lastfm_auto_sync, and in music-sync-status.json beside the history database. For a deployment with Spotify tools disabled:
docker compose exec lastfm-mcp cat /app/data/music-sync-status.jsonThe status file survives restarts; the MCP status describes the current process. A failed scan does not start writes. Per-track failures appear in the summary; errorCount is complete and errors contains at most 100 details. The scheduler retries on the next interval and stops scheduling new runs on shutdown.
New MCP tools
All 13 tools below require MCP_ENABLE_SPOTIFY_TOOLS=true. The original 30 Last.fm tools retain their existing behavior. Put the endpoint behind trusted access before exposing Spotify's private data; Spotify account OAuth authorizes this server, not callers of /mcp.
Tool | Main inputs / behavior |
|
|
|
|
|
|
| Same range/limit inputs |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| Read-only differences, ambiguity/unresolved counts; |
| No inputs; scheduler status and most recent result |
Read tools paginate internally up to their requested result limit; sync and comparison load all available library pages. Returned tracks contain artist, title, provider identity, album/duration when available, and timestamps where relevant. Search results are candidates rather than asserted matches.
Example MCP arguments (use these objects when calling the named tool):
{"name":"sync_spotify_likes_to_lastfm","arguments":{"dryRun":true}}{"name":"sync_lastfm_loves_to_spotify","arguments":{"dryRun":false}}{"name":"compare_spotify_lastfm_library","arguments":{"limit":100,"offset":0}}{"name":"create_spotify_playlist_from_tracks","arguments":{"name":"Favorites","tracks":["spotify:track:4iV5W9uYEdYUVa79Axb7Rh"],"public":false,"dryRun":true}}All mutating tools default to dryRun=true. Real manual writes require MCP_ENABLE_MUTATIONS=true. Sync summaries include scanned, duplicates, alreadySynced, wouldAdd, added, unmatched, ambiguous, probable, errorCount, and bounded errors. wouldAdd counts planned unique additions; added counts confirmed writes and stays zero during a dry run. probable is a subset of unmatched. duplicates includes repeated source identities and distinct source entries resolving to the same planned destination; counters should not be blindly summed.
Playlist creation deduplicates input IDs/URIs; explicit appends preserve supplied ordering and duplicates. Playlist creation/appending is not idempotent. If appending fails after creation, the result includes partial=true and the created playlist, so inspect/resume it rather than creating another playlist. Failed appends report how many items were confirmed before the failed batch; a timeout can leave that batch's outcome unknown.
Matching and API limits
Unicode NFKC/case folding, whitespace, typographic punctuation, equivalent
feat./ft./featuringtitle markers, and explicit remaster suffixes are normalized. Generic edition/version labels, live, acoustic, remix, radio-edit and other recording qualifiers remain significant.Matching MBIDs or Spotify IDs are strong identity evidence. Conflicting MBIDs are rejected. Spotify does not supply MBIDs, so no MBID is invented from an artist/title pair.
Only
exactandnormalized_exactmatches can be written. Multiple equally strong candidates areambiguous. Missing feature credits or lossy punctuation matches areprobableand skipped. Different featured performers are not silently equated.Last.fm resolution uses
track.getInfowith autocorrection disabled and may retry an explicit remaster title without its label. Spotify resolution searches up to ten candidates. Catalog aliases, absent metadata, region restrictions and multiple releases can remain unresolved; catalog-wide uniqueness is not guaranteed by a bounded search.Last.fm may canonicalize names when saving loves (for example, Latin
FlëurbecomesFlёurwith Cyrillicё). Before writing a resolved missing track,track.getCorrectionchecks whether its provider-confirmed canonical identity is already loved. This prevents repeated writes without globally equating lookalike characters or relaxing the matching rules for new writes.Full-library polling favors recovery and correctness over minimum API traffic. The first import and comparisons can be slow for large libraries. Libraries can change while offset pages are fetched; subsequent automatic runs reconcile again. No cross-provider transaction or removal propagation is implemented.
The client uses the current save-library endpoint (
PUT /me/library, up to 40 URIs), current-user playlist creation (POST /me/playlists), and playlist items endpoint (POST /playlists/{id}/items, up to 100 items). Deprecated write routes are not used.Every Spotify Web API request, including pagination, writes and retries, uses one serialized runtime queue with a default two-second pause between requests. Identical simultaneous GET requests share a single network call. Successful reads are cached for 60 seconds and searches for one hour, up to 1000 entries. Writes invalidate the read cache; failed responses are not cached. Sync reads fresh liked-library pages to preserve idempotency. MCP tools and the automatic scheduler share this client; separate CLI processes/replicas do not share the in-memory queue or cache.
A
429immediately pauses new and queued network requests across endpoints for the server'sRetry-After, without an immediate retry. The deadline is saved asspotify-rate-limit.jsonbeside the history database and survives container recreation. Missing/invalidRetry-Afterfalls back to 60 seconds, or one hour forQUOTA_EXCEEDED. Catalog sync/comparison stops on a provider-wide 429 instead of querying each remaining track. A batch write interrupted by 429 returns aninterruptedfield with confirmedaddedcounts. A new operation can resume after the deadline; fresh cached reads remain available during the pause. Expired cache entries are not served.get_music_sync_status.spotifyRequestsreports request counts, queue size, cache hits, merged requests, cooldown deadline and quota reason. Log eventspotify_rate_limitedrecords the endpoint path and requested pause without credentials or query text. An overfull queue or a wait longer thanSPOTIFY_MAX_QUEUE_WAIT_MSfails locally without sending that request.The two-second interval is a conservative local setting, not a promised Spotify quota. Spotify documents a rolling 30-second rate window and separate Development Mode quotas. Since July 2026, quotas are shared per developer account. Slowing requests cannot immediately lift an existing provider block; follow its cooldown. Large first-time library comparisons may take longer with throttling.
Playlists default to
public=false. Spotify's playlist visibility documentation distinguishes profile publication from link access; this setting is not a guarantee that a playlist link is inaccessible to others.
Implementation and validation
src/providers/spotify/ owns OAuth, token refresh, normalized API responses, pagination, and write batching. LastFmClient adds full loved-library reads, track resolution and signed writes without replacing existing reads. track-identity.ts implements conservative cross-provider identity; it deliberately does not change the existing history canonicalization rules. MusicLibraryService owns sync, comparison and playlist orchestration; MusicSyncScheduler owns background execution. MCP registration only validates inputs and delegates. No new dependencies were added.
Mock API tests cover OAuth state/scopes and refresh-token preservation, concurrent refresh, pagination, unsafe/repeated pagination links, current Spotify write routes/batches, rate limits, Last.fm signatures and account validation, normalization/version/feature cases, ambiguity, duplicate inputs, idempotent syncs in both directions, dry-run, partial failures, MCP flags/defaults, scheduler persistence/retry/shutdown, and the original Last.fm surface. Run npm run check for strict type checking, the full test suite, and build. Real-account OAuth and live writes must be verified after configuring credentials.
Potential next steps are a reviewed mapping-override store for ambiguous tracks and a durable resolution cache to reduce repeated catalog lookups. Recommendation algorithms and new analytics playlist logic are intentionally left for later.
This server cannot be deployed
Maintenance
Related MCP Connectors
Last.fm artist/album/track metadata (free API key required)
MusicBrainz MCP — wraps MusicBrainz Web Service v2 (free, no auth)
The media memory layer for AI agents and their humans. Your AI client gets 29 tools to search your collection, add items, update ratings, preview music, and find patterns across everything you've read, watched, and listened to.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceA Model Context Protocol (MCP) server for Last.fm. Gives AI assistants access to your listening history, music discovery, and detailed track/artist/album information.47MIT
- AlicenseAqualityCmaintenanceAn MCP server for the Last.fm API that enables LLM agents to access music data including user profiles, listening history, top charts, search, and artist/album/track metadata.16MIT
- AlicenseAqualityBmaintenanceA personal MCP server that analyzes your Spotify streaming history locally, enabling queries, artist insights, and recommendations using a local SQLite database.7MIT
- AlicenseNot gradedqualityBmaintenanceAn MCP server for the Last.fm API, exposing artist, album, track, chart, and user data as read-only tools. Supports optional writes, auth, and experimental methods over stdio or HTTP.MIT