Skip to main content
Glama
sptmru
by sptmru

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

get_user_profile

Public profile, total play count, and library size

get_listening_summary

Compact summary for overall, 12month, 6month, 3month, 1month, or 7day

get_top_artists

Top artists for a period

get_top_tracks

Top tracks for a period

get_top_albums

Top albums for a period

get_recent_tracks

Recent scrobbles with optional time filtering

search_listening_history

Artist, album, or track search using the local index or a bounded live scan

get_history_status

SQLite index coverage and freshness

sync_listening_history

Full history backfill or incremental sync

compare_listening_periods

Artist and track share changes between two periods

get_taste_profile

Core artists, favorite tracks and albums, discoveries, forgotten favorites, and listening patterns

get_artist_context

Tags, similar artists, play counts, and a short biography context

resolve_canonical_entities

Canonical artist/album/track identities, aliases, and known MBIDs

check_listening_exposure

Fast unheard/sample/explored/established/favorite checks

get_artist_affinity

Active days/months, sessions, returns, concentration, and explainable affinity

get_listening_sessions

Session grouping with configurable inactivity gap

get_album_exposure

Track coverage, ordered runs, stopping points, and returns using MusicBrainz tracklists

get_listening_timeline

Arbitrary UTC ranges and day/week/month/year buckets by artist, album, or tag

get_listening_matrix

Pageable sparse time-bucket × artist/album matrix with global window totals, active days, concentration, and explicit coverage

detect_listening_eras

Statistical change points in monthly listening distributions

get_artist_features

Combined Last.fm tags/similarity and MusicBrainz metadata/relationships

build_taste_graph

Artists, albums, tags, sessions, eras, external similarity, and preference edges

record_music_feedback

Structured love/like/mixed/boring/dislike/not-now feedback

record_preference_signal

Explicit -5..5 signals for atmosphere, groove, melody, structure, vocals, and more

get_feedback_context

Feedback, dimension summaries, and active exclusions

get_recommendations

Safe, bridge, or explore recommendations with evidence, risks, and starting points

exclude_recommendation

Permanent, six-month, or new-releases-only artist exclusion

list_recommendation_exclusions

Active recommendation exclusions

record_recommendation

Store an externally issued recommendation and its exposure baseline

evaluate_recommendations

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

  1. Sign in to your Last.fm account.

  2. Open Last.fm Create API account.

  3. Enter an application name and description. This server does not require a callback URL.

  4. Copy the API key.

  5. 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 .env

At 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/healthz

MCP 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 250000

Afterward, periodically fetch new scrobbles:

docker compose exec lastfm-mcp node dist/src/sync.js incremental 10000

You 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:

  • buckets contains UTC boundaries, total plays, active days, selected plays, and omitted plays;

  • entities contains whole-window totals, active days/buckets, first and last play, peak bucket/day concentration, active span, and bucket density;

  • matrix.cells uses [bucketIndex, entityIndex, plays, activeDays]; a missing cell means zero plays;

  • filtering reports exact play coverage, page position, and every omission caused by minPlays or 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.1

Apply the change and verify HTTPS:

docker compose up -d --build
curl https://mcp.example.com/healthz

If 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:

  1. Enable Developer mode under Settings → Apps → Advanced Settings. A workspace admin or owner may need to allow it first.

  2. Open Settings → Apps → Create.

  3. Enter a name such as My Last.fm and the endpoint https://mcp.example.com/mcp. For this deployment, use https://lastfm.mcp.sptm.online/mcp.

  4. 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.

  5. 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.

  6. 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

LASTFM_API_KEY

required

Last.fm API key

LASTFM_USERNAME

required

Fixed user whose data is exposed by the MCP server

LASTFM_API_BASE_URL

official endpoint

Primarily useful for tests

MCP_HOST

0.0.0.0

Bind address inside the container

MCP_PORT

3000

Port inside the container

MCP_BIND_ADDRESS

127.0.0.1

Host address used to publish the Compose port

MCP_PUBLIC_PORT

3000

Published host port

MCP_ALLOWED_HOSTS

required for 0.0.0.0

Host and Origin allowlist for DNS rebinding protection

MCP_ENABLE_MUTATIONS

false

Enable sync, feedback, exclusions, and recommendation recording only behind trusted access control

LASTFM_TIMEOUT_MS

10000

Timeout for one API request

LASTFM_MAX_RETRIES

3

Retry count for temporary and rate-limit errors

LASTFM_MIN_REQUEST_INTERVAL_MS

250

Minimum delay between Last.fm requests, approximately four requests per second

LASTFM_CACHE_TTL_SECONDS

300

In-memory cache duration for chart and info calls

MUSICBRAINZ_BASE_URL

official WS/2 endpoint

Primarily useful for tests

MUSICBRAINZ_USER_AGENT

project URL

Required MusicBrainz application identity/contact

MUSICBRAINZ_TIMEOUT_MS

10000

Timeout for one MusicBrainz request

MUSICBRAINZ_MAX_RETRIES

2

Retry count for temporary MusicBrainz errors

MUSICBRAINZ_MIN_REQUEST_INTERVAL_MS

1100

Serialized MusicBrainz request interval

HISTORY_DB_PATH

/app/data/lastfm.sqlite

SQLite database path

HISTORY_LIVE_SCAN_LIMIT

5000

Maximum live scan size when no index exists

HISTORY_MAX_SYNC_TRACKS

250000

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 is sampled, and a one-track/one-day repeat remains sampled even above ten plays. Broader trials are explored, distributed returns are established, and high sustained exposure is favorite.

  • 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 confidence measures 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.

  • safe favors strong similarity to established seeds, bridge requires links to at least two tag-derived/provisional seed clusters, and explore favors 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 the loved signal.

  • 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.getRecentTracks returns 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, and 29, as well as HTTP 429 and 5xx, 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 dev

Run checks:

npm run check
npm audit --omit=dev
docker compose build

The 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

  1. 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.

  2. Register the exact redirect URI http://127.0.0.1:8888/callback. Spotify permits HTTP for explicit loopback IPs, but not localhost; other redirects require HTTPS. See redirect URI rules.

  3. Set SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, and SPOTIFY_REDIRECT_URI in .env. Set LASTFM_API_SECRET to the shared secret belonging to your existing LASTFM_API_KEY.

  4. Authorize both services once. For local development, set HISTORY_DB_PATH=./data/lastfm.sqlite so credential files are written into the local ignored data/ directory:

npm run auth -- spotify
npm run auth -- lastfm

For 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-mcp

Spotify 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-public

The 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

SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET

Unset

Both required to enable the integration

SPOTIFY_REDIRECT_URI

http://127.0.0.1:8888/callback

Exact registered OAuth redirect

SPOTIFY_TOKEN_PATH

Beside HISTORY_DB_PATH

Persistent Spotify token file

LASTFM_API_SECRET

Unset

Required for Last.fm authentication and loving tracks

LASTFM_SESSION_PATH

Beside HISTORY_DB_PATH

Persistent authorized Last.fm session

SPOTIFY_AUTO_SYNC_ENABLED

true when Spotify is configured

Automatic Spotify → Last.fm writes

SPOTIFY_AUTO_SYNC_INTERVAL_SECONDS

3600

Delay after each completed run; range 60–86400

SPOTIFY_MIN_REQUEST_INTERVAL_MS

2000

Minimum pause between Spotify Web API requests in the shared queue

SPOTIFY_MAX_QUEUE_WAIT_MS

60000

Maximum queue wait before rejecting an operation locally

SPOTIFY_CACHE_TTL_SECONDS

60

Successful GET response cache; sync always refreshes liked-library pages

SPOTIFY_SEARCH_CACHE_TTL_SECONDS

3600

Successful catalog search cache, including empty results

MCP_ENABLE_SPOTIFY_TOOLS

false

Expose private Spotify and cross-provider MCP tools

MCP_ENABLE_MUTATIONS

false

Permit manual remote writes with dryRun=false

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.json

The 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

spotify_get_liked_tracks

limit (default 50, max 1000)

spotify_get_recent_tracks

limit; bounded by Spotify's retained recent history

spotify_get_top_tracks

limit, timeRange: short_term, medium_term, long_term

spotify_get_top_artists

Same range/limit inputs

spotify_get_playlists

limit; current user's playlists

spotify_search_track

artist, title; up to 10 catalog candidates

spotify_create_playlist

name, optional description, tracks, public, dryRun

spotify_add_tracks_to_playlist

playlistId, tracks, dryRun

create_spotify_playlist_from_tracks

name, tracks, optional description, public, dryRun

sync_spotify_likes_to_lastfm

dryRun; complete library reconciliation

sync_lastfm_loves_to_spotify

dryRun; complete reverse reconciliation

compare_spotify_lastfm_library

Read-only differences, ambiguity/unresolved counts; limit and offset paginate returned details

get_music_sync_status

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./featuring title 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 exact and normalized_exact matches can be written. Multiple equally strong candidates are ambiguous. Missing feature credits or lossy punctuation matches are probable and skipped. Different featured performers are not silently equated.

  • Last.fm resolution uses track.getInfo with 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ëur becomes Flёur with Cyrillic ё). Before writing a resolved missing track, track.getCorrection checks 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 429 immediately pauses new and queued network requests across endpoints for the server's Retry-After, without an immediate retry. The deadline is saved as spotify-rate-limit.json beside the history database and survives container recreation. Missing/invalid Retry-After falls back to 60 seconds, or one hour for QUOTA_EXCEEDED. Catalog sync/comparison stops on a provider-wide 429 instead of querying each remaining track. A batch write interrupted by 429 returns an interrupted field with confirmed added counts. 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.spotifyRequests reports request counts, queue size, cache hits, merged requests, cooldown deadline and quota reason. Log event spotify_rate_limited records the endpoint path and requested pause without credentials or query text. An overfull queue or a wait longer than SPOTIFY_MAX_QUEUE_WAIT_MS fails 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.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    A Model Context Protocol (MCP) server for Last.fm. Gives AI assistants access to your listening history, music discovery, and detailed track/artist/album information.
    47
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    An 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.
    16
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    A personal MCP server that analyzes your Spotify streaming history locally, enabling queries, artist insights, and recommendations using a local SQLite database.
    7
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    An 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