lyricmood-mcp
Uses Last.fm data to add mood and genre tags to track profiles, provide artist top tracks, and support similar-song discovery. Without an API key, tags are omitted and artist track ordering falls back to Spotify search.
Integrates with the Spotify Web API for searching tracks, fetching track and artist data, reading now-playing and top/liked tracks, listing and editing playlists, and building mood playlists. Uses PKCE OAuth for personal Spotify accounts.
Planned milestone 7 integration; the provided README does not yet describe specific YouTube capabilities.
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., "@lyricmood-mcpfind songs lyrically similar to what I'm playing right now"
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.
lyricmood
A personal, lyrics-aware music assistant. Ask it why you like a song, what the lyrics actually say (including in languages you don't speak well), which songs feel like this one lyrically, or for a playlist that moves from sad to hopeful.
lyricmood has no chat UI of its own. It's a set of tools you can use from any LLM client:
MCP server: use Claude Code, Codex, Gemini CLI or Claude Desktop as the chat interface. See Use it from an MCP client.
Terminal chat (
lyricmood chat, coming in milestone 5): a chat in your terminal with a swappable LLM backend. The first backend is your own Claude Code (no API key). An API backend (LiteLLM: OpenRouter, Gemini, OpenAI, Anthropic) can be added later.
Both read the same tool registry, so every tool is defined once.
Status: milestones 1–4 and 6 of 7 done; 5 (terminal chat) is next. Spotify search, lyrics (LRCLIB), audio features (ReccoBeats), Last.fm tags, artist top tracks, the song you're playing now, lyrics-based similar songs, artist comparisons, your top and liked songs, mood playlists and playlist editing, all available to MCP clients. See Roadmap.
Why this exists
Plenty of Spotify MCP servers exist (playback control, playlist CRUD, a few that fetch lyrics for one track). None of them combine lyrics, mood features and tags to answer "what is this song about, and why do I like it?", or rank songs by lyrical similarity. Existing lyrics-similarity projects are mostly notebooks over static Kaggle datasets. lyricmood is built around three things they lack:
understanding lyrics for a non-native speaker
lyric-based discovery that only embeds tracks when they come up as candidates, never a whole catalog
working with whatever LLM you already use
Related MCP server: Mr. Magic MCP Server
Setup
Requires uv. uv installs Python 3.12 itself if you don't have it.
git clone https://github.com/yigitates17/lyricmood.git
cd lyricmood
uv sync --extra embeddings # or plain `uv sync`, see below
cp .env.example .envThe embeddings extra adds what find_similar_by_lyrics needs:
ONNX Runtime, Hugging Face tokenizers, and LanceDB. That's about 385 MB
on top of the base install's ~215 MB. The first similarity search also
downloads the lyrics model,
intfloat/multilingual-e5-small,
once: about 480 MB, into ~/.lyricmood/models. It runs on CPU through ONNX,
so PyTorch is not needed. Leave the extra out if you only want lyrics
explanations, profiles and playlists. Everything else works, and the
similarity tool will say how to enable it.
Spotify (required)
Spotify's Web API now requires the app owner to have Premium, and Development Mode apps are limited to 5 users. Both are fine for personal use.
Open the Spotify Developer Dashboard and log in with your Premium account. Accept the developer terms if asked.
Click Create app and fill in:
App name:
lyricmood(anything works)App description:
Personal lyrics-aware music assistantRedirect URIs: enter
http://127.0.0.1:8888/callbackand click Add. Use127.0.0.1, notlocalhost; Spotify rejectslocalhost.Which API/SDKs are you planning to use?: tick Web API.
Accept the terms and click Save.
Open the app, go to Settings, and copy the Client ID into
.env:SPOTIFY_CLIENT_ID=your_client_id_hereYou don't need the Client Secret. lyricmood logs in with PKCE, which is designed for apps that can't keep a secret.
Log in:
uv run lyricmood auth spotifyYour browser opens Spotify's consent page. Click Agree. The browser then shows "Authentication successful" and the terminal prints
Spotify: logged in. The token is saved to~/.lyricmood/spotify_token.jsonand refreshes automatically.
If it doesn't work:
INVALID_CLIENT: Invalid redirect URI: the URI in.envmust match the dashboard character for character, including/callbackand no trailing slash.Address already in use: something else is using port 8888. Pick another port, say 8899, and change it in both the dashboard andSPOTIFY_REDIRECT_URI.403 "user not registered": in the dashboard, go to User Management and add the email of your Spotify account.
"Spotify session expired" after several months: Spotify refresh tokens now last six months. Run
uv run lyricmood auth spotifyagain.
The login asks for all the permissions lyricmood will eventually need (top tracks, saved tracks, reading and editing playlists, the currently playing song), so later milestones won't make you log in again.
Last.fm (recommended)
Last.fm provides mood and genre tags, artist top tracks and, later, similar
songs. Without a key, get_track_profile leaves out tags and
get_artist_tracks falls back to Spotify's search order.
Go to https://www.last.fm/api/account/create (log in or create a free account).
Fill in Application name (e.g.
lyricmood). The other fields can stay empty.Copy the API key (not the shared secret) into
.env:LASTFM_API_KEY=your_key_here
LRCLIB and ReccoBeats
Neither needs a key. lyricmood sends a User-Agent that identifies it, as LRCLIB asks.
YouTube
This comes with milestone 7.
Usage
uv run lyricmood status # keys, login state, cache size
uv run lyricmood tools # list tools
uv run lyricmood tools --schema # ...with JSON input schemas
uv run lyricmood call search_track query="nemo nightwish"
uv run lyricmood call search_track query="artist:Nightwish" limit=10
uv run lyricmood call search_track query="https://open.spotify.com/track/..."
uv run lyricmood call get_track_profile track="Carnival of Rust Poets of the Fall"
uv run lyricmood call get_lyrics track=2DtPEKs3Pj7E06EoWW4vw8
uv run lyricmood call get_artist_tracks artist="Haloo Helsinki!" limit=5
uv run lyricmood call now_playing # play something on Spotify first
uv run lyricmood call now_playing with_profile=false
uv run lyricmood call find_similar_by_lyrics track=2DtPEKs3Pj7E06EoWW4vw8 k=8
uv run lyricmood call find_similar_by_lyrics track=2DtPEKs3Pj7E06EoWW4vw8 mood_weight=0.4
uv run lyricmood call find_similar_by_lyrics track="Nemo Nightwish" expand_pool=false
uv run lyricmood call analyze_artists --json '{"artists": ["Poets of the Fall", "Nightwish"]}'
uv run lyricmood call get_my_top_tracks time_range=short_term limit=10
uv run lyricmood call get_my_playlists
uv run lyricmood call build_mood_playlist --json '{"mood_text": "heartbreak, lonely nights", "valence": 0.2, "energy": 0.35, "seed_artists": ["Nightwish"], "size": 10}'
uv run lyricmood call build_mood_playlist --json '{"mood_text": "-", "size": 10, "arc": [{"mood_text": "despair, loss", "valence": 0.15}, {"mood_text": "hope, light after the storm", "valence": 0.75}]}'call runs any tool directly and prints exactly the JSON an LLM would get.
It's the quickest way to test a tool without an LLM.
Your data (SQLite cache, Spotify token, lyrics vectors, the model) lives in
~/.lyricmood. It sits outside the repo on purpose: MCP clients start the
server from whatever directory they like, and they all need to share one
cache. Set LYRICMOOD_DATA_DIR to move it.
Use it from an MCP client
The MCP server (lyricmood-mcp) exposes every tool over stdio. It finds
.env and ~/.lyricmood by itself, so clients need no environment
variables. Before registering it:
cd ~/lyricmood
uv sync --extra embeddings
uv run lyricmood auth spotify # the server can't open a login prompt itself
uv run lyricmood download-model # so the first similarity search doesn't
# spend a tool call downloading 480 MBEvery example below points at the server binary inside the project's
virtualenv. Replace $HOME/lyricmood if you cloned somewhere else. A direct
path works no matter which directory the client starts in, needs no extra
flags, and starts in about half a second. Re-run uv sync --extra embeddings
after pulling updates.
Claude Code
claude mcp add -s user lyricmood -- "$HOME/lyricmood/.venv/bin/lyricmood-mcp"
claude mcp list # should show: lyricmood ... ✓ Connected-s user makes it available in every project. Inside Claude Code, /mcp
lists the tools. Try "What is Carnival of Rust by Poets of the Fall
about?" or "Explain the song I'm playing right now."
Codex CLI
codex mcp add lyricmood -- "$HOME/lyricmood/.venv/bin/lyricmood-mcp"Codex stops tool calls after 60 s by default, and a song's first
find_similar_by_lyrics can take about a minute. Raise the limit in
~/.codex/config.toml:
[mcp_servers.lyricmood]
command = "/Users/you/lyricmood/.venv/bin/lyricmood-mcp"
tool_timeout_sec = 180Gemini CLI
gemini mcp add -s user lyricmood "$HOME/lyricmood/.venv/bin/lyricmood-mcp"Use -s user. Gemini refuses project-scoped config in folders it doesn't
trust.
Claude Desktop
Add this to ~/Library/Application Support/Claude/claude_desktop_config.json
and restart the app:
{
"mcpServers": {
"lyricmood": {
"command": "/Users/you/lyricmood/.venv/bin/lyricmood-mcp"
}
}
}The desktop app has been reported to cancel tool calls after about 60 s.
If a first similarity search fails there, run it once from the terminal
(uv run lyricmood call find_similar_by_lyrics track=...) to fill the
cache, then ask again.
The server also sends the client short instructions: the user may not be a
native speaker, so explain idioms and translate key lines; "this song"
means now_playing; and mention missing data instead of guessing.
Tools
Tool | Milestone | Status |
| 1 | ✅ |
| 2 | ✅ |
| 2 | ✅ |
| 2 | ✅ |
| 2 | ✅ |
| 3 | ✅ |
| 3 | ✅ |
| 6 | ✅ |
| 6 | ✅ |
| 6 | ✅ (added: needed to find playlist IDs) |
| 6 | ✅ (added: needed before reordering) |
| 6 | ✅ |
| 6 | ✅ |
| 6 | ✅ |
| 7 | planned |
Every track argument takes a Spotify ID, URL or URI, or free text, which
resolves to the top search hit.
get_track_profile returns, in one call:
the lyrics (cut at a stanza boundary past 2,500 characters) and simple stats.
repeated_linesusually marks the chorus.ReccoBeats audio features: valence (musical positivity), energy, danceability, acousticness, tempo and key.
Last.fm tags. Last.fm rarely tags individual songs, so the artist's tags are often used instead;
tags_sourcesays which.
Each source can fail on its own (ReccoBeats in particular is flaky). The
profile still comes back, with a missing field saying what's absent and
why. The LLM interprets the lyrics; the code only gathers facts.
Lyrics matching: artist + title decide the match; duration only breaks
ties. lyricmood tries LRCLIB /api/get with artist, title and duration
first. If that misses, it searches and takes the entry whose artist and
title match (fuzzy), choosing the closest length and preferring entries
with lyrics over ones flagged instrumental. Length never rejects a match,
because plain lyrics are the same across remasters, compilations and radio
edits. Length only matters for synced (timestamped) lyrics, which lyricmood
doesn't use. Version suffixes ("- Remastered 2004", "(feat. X)") are
stripped before lookup. "Live" and "Orchestral Version" are kept, because
their lyrics can differ or be missing. Lyrics are cached forever; misses are
retried after 14 days.
find_similar_by_lyrics finds songs whose lyrics are about similar
things, across languages:
It gathers about 40 candidates from Last.fm (similar tracks, the seed's top tags, similar artists' hits) and ReccoBeats recommendations, taking from each source in turn so no single one dominates.
It resolves each candidate to Spotify and fetches its lyrics.
It embeds the lyrics, one vector per stanza, with repeated choruses counted once and the song vector being their average. Vectors are stored in LanceDB, so a song is only ever embedded once.
It ranks the candidates by similarity to the seed.
Each result includes matching_lines: the two passages, one from each
song, that matched best. mood_weight (0–1) also favors songs with
similar valence and energy. expand_pool=false skips the network and ranks
only songs analyzed in earlier searches.
Scores are relative within one search (the best match is 1.0), not absolute. The first search for a song takes about a minute (mostly LRCLIB, which is rate-limited politely); repeats take under a second.
Known limitation: the model groups lyrics partly by language. An English song about loss can score close to an English party song. In a mixed English/Finnish test, the nearest neighbour was always the right topic, but scores aren't comparable across languages. See DESIGN §8.2.
build_mood_playlist proposes an ordered list. It doesn't save
anything.
Where songs come from: the seed artists' top songs plus three similar artists each, or your own top and liked songs when no artists are given. The pool is about 60 songs (
LYRICMOOD_MOOD_POOL_SIZE).How each song is scored: half on lyrics, comparing
mood_textagainst each song's stanzas, and half on music, how close its valence and energy are to the targets.feature_weightchanges that split. Claude chooses the targets from your words, for example "sad" ≈ valence 0.2. A song missing lyrics or features counts as typical on that side, so missing data neither helps nor hurts it.arcgives 2–5 stages, for example heartbroken → hopeful. Songs move through them gradually, with targets blended position by position.max_per_artistkeeps variety (default 3).Mixed languages: give the mood in each language on separate lines, because the model favours lyrics in the query's language.
Playlist writes (create_spotify_playlist, update_spotify_playlist)
change your Spotify account. New playlists are private by default, and
their docstrings tell the LLM to confirm with you first. Your MCP client
also asks before running a tool unless you've allowed it.
update_spotify_playlistworks out the end state and checks it before writing anything, so a badreorderchanges nothing.It refuses to reorder playlists that contain podcast episodes or local files, which a rewrite would drop.
It skips songs that are already in the playlist.
Spotify only lets apps read and edit your own (or collaborative) playlists.
analyze_artists samples each artist's top songs and returns:
lyric excerpts (opening stanza plus chorus, about 500 characters each)
average valence, energy, danceability and acousticness
Last.fm tags, plus the tags the artists share
The LLM reads the excerpts to find the common themes behind "why do I like these?". It doesn't need the embeddings extra.
Adding a tool
Put a module in lyricmood/tools/. Modules there are discovered
automatically and show up in the MCP server, the chat and the CLI:
from pydantic import BaseModel
from lyricmood.context import AppContext
from lyricmood.registry import tool
class EchoInput(BaseModel):
text: str
class EchoOutput(BaseModel):
text: str
@tool
async def echo(ctx: AppContext, args: EchoInput) -> EchoOutput:
"""The docstring is the description the LLM sees."""
return EchoOutput(text=args.text)API clients live in lyricmood/core/ and must not contain LLM logic. Tools
get those clients from ctx.
Development
uv sync --extra embeddings
uv run pytest # offline: HTTP is mocked with respx + JSON fixtures
uv run ruff check . && uv run ruff format --check .Similarity tests use a fake word-overlap embedder, so they're instant and
need no model. tests/test_model_quality.py checks the real model on a
small invented English/Finnish topic set. It runs only when the model is
already downloaded, so CI skips it.
CI runs the same commands and never calls a real API. LRCLIB, Last.fm and
ReccoBeats fixtures in tests/fixtures/ are recorded responses with images
and country lists removed. All lyrics in them are replaced with invented
text, because test data must never contain copyrighted lyrics. The Spotify
fixtures are hand-built from Spotify's documented response shapes.
Spotify API constraints (2026)
Spotify's February 2026 changes for Development Mode apps shape the design:
Gone: recommendations, audio features, related artists (removed in 2024), artist top tracks, and batch
GET /tracks?ids=. lyricmood gets audio features from ReccoBeats and similar tracks and artists from Last.fm.Search returns at most 10 results per page. lyricmood paginates when asked for more.
ISRC (
external_ids) was removed and then restored in March 2026. lyricmood treats it as optional.Playlist contents are only returned for playlists you own.
convert_playlistcan't read other people's Spotify playlists.
docs/DESIGN.md explains the architecture, the reasoning behind it, and open questions.
Roadmap
✅ Skeleton, config, SQLite cache, Spotify OAuth,
search_track✅ LRCLIB, Last.fm and ReccoBeats clients,
get_lyrics,get_track_profile,get_artist_tracks,now_playing✅ Embeddings (multilingual-e5-small via ONNX) + LanceDB,
find_similar_by_lyrics,analyze_artists✅ MCP server exposing all tools (
lyricmood-mcp)Terminal chat (
lyricmood chat) with a swappable backend, starting with Claude Code, pluslyricmood watch, which summarizes each new song as it starts playing✅ Top/liked songs, playlist create/update/read,
build_mood_playlist(done before 5)YouTube client,
convert_playlist
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Lyrics OVH MCP — keyless lyrics lookup.
Search, analyze, and discover commercially released music using sonic intelligence.
Write lyrics in 100+ styles, score them, generate full songs with 4 engines, split stems. OAuth.
Genius MCP — wraps the Genius API (api.genius.com)
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceAn MCP server for Spotify control and synchronized lyrics retrieval that enables playback management, queue navigation, and music search capabilities. It also features perception tools for real-time track analysis, including BPM, key detection, and timestamped lyrics.39 npm3Apache 2.0
- AlicenseNot gradedqualityCmaintenanceBridges multiple lyrics providers (LRCLIB, Genius, Musixmatch, Melon) into a single toolchain accessible via MCP, HTTP, or CLI.122 npm1MIT
- AlicenseAqualityBmaintenanceMCP server for lyrics.com that enables searching songs by lyrics or title and retrieving full lyrics, with no API key required.387 npmMIT
- AlicenseAqualityAmaintenanceAn MCP server that enables searching tracks and fetching lyrics, including time-synced LRC lyrics, from LRCLIB without requiring an API key.3184 npmMIT