commendation
Provides tools for discovering and recommending new songs via YouTube Music, including recommendations based on a seed song or playlist, and fetching an artist's catalog, while automatically excluding any songs already in the user's library or playlists.
Click on "Install 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., "@commendationRecommend songs similar to Kryptonite by 3 Doors Down"
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.
commendation
An MCP server that recommends new songs — never a song already in your library, meaning never a song already in Liked Music or in any of your playlists, not just the one you seeded from.
It's built to do better than a streaming service's built-in radio/autoplay by pooling multiple independent discovery signals (radio, related content, artist catalog expansion) and ranking candidates by how many of them agree, instead of trusting one black-box algorithm.
Backend: YouTube Music (v1). Commendation is designed as a general recommendation engine, not tied to one service — v1 is built entirely against YouTube Music (via ytmusicapi). Spotify support is planned as a second backend; see PLAN.md's "v3 — Multi-provider support" section for the design questions around that.
Tools
Tool | Description |
| Recommend new songs similar to a seed song. Pass |
| Recommend new songs based on an entire playlist (samples seed tracks from it). |
| Return actual songs by a named artist — a direct catalog pull, not a similarity recommendation. |
| Force-rebuild the cached library exclusion set. See Library cache. |
| v2. Recommend new songs matching how you actually feel, shaped into a sequence that moves. See Mood. |
| v2. Infer your current mood from recent listening, with the evidence for it. |
| v2. Why a song was picked, in mood terms. |
| v2. |
| v2. How much of the mood index exists, so gaps are visible instead of silent. |
All three tools guarantee every result is absent from Liked Music and from every one of your playlists, not just the one you seeded from (if any). recommend_from_song additionally never returns the seed song itself; recommend_from_playlist additionally never returns anything from the seed playlist even if that playlist somehow isn't in your library listing.
songs_by_artist is a different kind of tool from the other two: no scoring, no radio/related signals — just that artist's real catalog, with the same library-wide exclusion applied. It's a hard requirement, not best-effort: if fewer than limit qualifying songs exist, it returns however many were found (found in the response) rather than padding the list with substitutes. It never adds anything anywhere.
Related MCP server: youtube-music-mcp
Mood-aware recommendations (v2)
recommend_from_song answers "what sounds like this?". recommend_for_mood answers a different
question: "what does this person need to hear right now?"
Why this isn't just a filter
Running the v1 engine and filtering its results by mood does not work — filter a Daft Punk radio for "melancholy" and you get the least danceable Daft-Punk-adjacent tracks, not melancholy music. So the mood decides where candidates come from:
Resolve the mood to a vector.
Pick seeds from your own library that already sit near it.
Run v1's proven radio / related / artist expansion from those seeds.
Add a fourth signal: songs from YouTube's mood playlists near the target — the only path that reaches outside your existing taste graph.
Rank on signal agreement × mood fit, then assign songs to slots along an arc.
The mood vector
Axis | Range | Low ←→ high |
| −1…1 | despairing ←→ euphoric |
| 0…1 | still ←→ frantic |
| 0…1 | resolved ←→ anxious. Separates angry from excited — two axes can't tell aggressive workout rap from joyful party pop |
| 0…1 | background wallpaper ←→ lyric-forward |
Pass vector for precision, feeling for free text (matched against a mood-word lexicon), or context
for one of YouTube's own moods. With none of them, the mood is inferred from your listening history.
Arcs
A mood-matched set is the obvious thing to return and the wrong one. From music therapy's iso-principle: to shift someone's mood you meet them where they are and move gradually — opening with upbeat songs when someone is low just gets skipped.
Arc | Behaviour |
| Stay where they are and validate it. Default. |
| Start at their mood, rise gradually across the set. |
| Descend to calm — an evening wind-down. |
| Go further in. |
| Stay in a band with energy as a curve (a workout is warmup → peak → cooldown). |
How a song's mood is known
YouTube Music exposes no audio features at all — no tempo, key, valence or energy (verified against the live API; that's why BPM was dropped rather than built). So mood is assembled from four layers, cheapest first, and the best available source for a song wins outright:
Layer | What it is | Needs |
| Claude reads the lyrics. Handles any language, and irony. | Optional — |
| Lyrics fetched and cached (2 API calls/song, incl. the negative result) | — |
| Membership in YouTube's own mood playlists — 1,592 listings, 65,438 tracks, 104,028 memberships | A crawl |
| An artist's average mood, propagated to their unlabelled songs | Free |
The atlas alone is not enough, and measurably so. On this account a 60-playlist sample covered 4.1% of the liked library, and the misses concentrate on the Punjabi, Bollywood and Reggae catalogue that YouTube's English-centric mood playlists barely touch. Artist propagation is what closes most of that gap without any API key; the Claude layer closes the rest.
After a full crawl, measured: 71.3% library coverage — 553 songs from artist propagation, 480 from playlist membership.
Measuring quality
scripts/quality_check.py scores a fixed set of mood/arc cases so changes can be judged by number rather
than impression:
python scripts/quality_check.py --titles
python scripts/quality_check.py --distinctiveness 0 # A/B the seed scoringWatch cross-mood overlap, not just mean fit. An early build scored a healthy 0.775 mean fit while returning 70% the same songs for "heartbroken" and "angry"; fit alone couldn't see it. Current numbers: mean fit 0.848, cross-mood overlap 0.064, 63 distinct songs across 80 slots.
Setup
# 1. Crawl the mood atlas (~35 min, resumable, safe to interrupt)
python scripts/build_atlas.py
# 2. Label your library (steps 1-3 need no credentials beyond YouTube Music)
python scripts/label_library.py
# 3. Genre/language labels, for the language filter (~10-15 min)
python scripts/build_genres.py
# 4. Tempo, for BPM filtering (~0.4s per song)
python scripts/build_tempo.py
# 5. Optional: read lyrics with Claude to cover what the atlas missed
pip install -e ".[llm]" && ant auth login
python scripts/label_library.py --claudeCheck progress any time with python scripts/build_atlas.py --status,
python scripts/label_library.py --report, or the index_status() tool.
Optionally, keep a real timeline of listening — get_history() reports only "Today"/"Yesterday", so
local timestamps are the only clock this system will ever have:
0 */3 * * * cd /path/to/commendation && .venv/bin/python scripts/snapshot_history.pyConfiguration
Env var | Default | Meaning |
|
| Mood index, labels, history, feedback. |
|
| Model for lyric-based labelling. |
|
| Effort level for that labelling. |
|
| Songs per labelling request. |
Everything mood-related is stored in local SQLite. The only thing that ever leaves the machine is, optionally, song titles and lyric excerpts sent to the Claude API for labelling.
Language filtering
"Find songs like this Punjabi track, but only English ones."
recommend_from_song(song="Brown Munde", artist="AP Dhillon", language=["english"])
recommend_for_mood(feeling="hyped", exclude_languages=["punjabi", "hindi"])Nothing in the YouTube Music API returns a language, so it's assembled in layers, strongest first:
Layer | Evidence | Weight |
| Title written in Gurmukhi, Devanagari, Arabic, Hangul, Kana or Han | 100 |
| Your own playlist names (matched loosely — | 50 |
| YouTube's genre-category pages | 10 |
| The same, but for anglophone genres | 1 |
English is weighted at 1 on purpose. YouTube files Punjabi and Hindi rap under "Hip-hop", so counting an English-genre hit as a normal vote labelled Sidhu Moose Wala, Karan Aujla and AP Dhillon as English. English is now what you get when no language-bearing evidence exists, rather than something that can outvote real evidence.
Two behaviours worth knowing:
Unlabelled candidates are dropped by default. Asking for English only is a request for a guarantee, and an unlabelled candidate from a Punjabi-seeded pool is probably Punjabi. The response always reports how many were dropped;
allow_unlabelled_language=Truekeeps them.Filtering alone isn't enough, so retrieval expands. Seeding from a Punjabi song and filtering for English left 3 results out of 8 — the pool simply didn't contain more. The surviving songs are re-seeded to reach further into that language, and the response says when that happened.
expand_across_language=Falsedisables it.
This infers language from genre, which is approximate — "Dance & electronic" is often instrumental, and "Reggae & caribbean" is usually English. Treat it as a strong hint.
Tempo (BPM)
YouTube Music exposes no tempo data, so BPM comes from Deezer's public API — no key, no auth, no attribution required.
recommend_from_song(song="Kryptonite", artist="3 Doors Down", match_seed_tempo=True)
recommend_for_mood(context="Workout", bpm_min=120, bpm_max=140)bpmbiases ranking toward a tempo;bpm_min/bpm_maxbound it hard.match_seed_tempo=Trueuses the seed song's own BPM.Half- and double-time count as close. 170bpm drum-and-bass and 85bpm hip-hop share a pulse; treating them as opposites would be musically wrong.
Tempo is never propagated by artist, unlike mood — an artist's songs share a sensibility, not a BPM. Propagating it would be inventing data.
Coverage is uneven, and the response says so. Measured across the whole library — 541 of 1,495 songs (36.2%):
Rock & Alternative | 67% | Hip-Hop & Rap | 47% |
R&B & Soul | 64% | Electronic & Dance | 38% |
Pop | 60% | Bollywood/Hindi | 16% |
Country | 56% | Punjabi | 6% |
Reggae & Dancehall | 49% |
The misses are genuine: those songs resolve to the correct track on Deezer and simply carry
bpm: 0. So a song with unknown BPM is never dropped, only left unscored on tempo —
dropping them would quietly delete whole languages from the results.
Build the index with python scripts/build_tempo.py (~0.4s/song, cached permanently
including the misses).
Library cache
Every recommendation excludes anything already in your library, which means building a set of every videoId in Liked Music plus all of your playlists. Measured against a real account (~1,100 liked songs, 28 playlists, ~1,550 playlist tracks) that costs ~20 seconds — and v1 paid it on every single tool call.
That set is now cached on disk. Measured on the same account:
Before | After | |
Building the exclusion set | 20.5s | 0.9s |
| ~24s | 4.3s |
| ~22s | 2.6s |
Liking a song still takes effect immediately. A cache hit re-fetches only the most recently liked
songs (one page, ~1s) and unions them in, so the novelty guarantee holds for the mutation you actually
make most. The case a cache hit can miss is a song added to some other playlist within the TTL — call
refresh_library() after doing that if it matters, e.g. right after a playlist-management tool adds tracks.
If the top-up fetch fails, the cached set is used as-is rather than failing the call — a slightly older exclusion set beats no recommendation, the same partial-results philosophy used for discovery signals.
Env var | Default | Meaning |
|
| Where the cached set lives (~22 KB). |
|
| How long a cached set stays usable. Set to |
The cache is written atomically (temp file + rename), and a missing, unreadable, malformed or expired cache is treated as a miss rather than an error — worst case you pay the ~20s rebuild v1 always paid.
Not included (v1): BPM/tempo-based comparison. YouTube Music doesn't expose tempo data, so this needs a second data source (e.g. a third-party BPM API) — a stretch goal for a future version, not part of this build. See PLAN.md for the full design rationale.
Setup
1. Install dependencies
python3 -m venv .venv
source .venv/bin/activate
pip install -e .2. Authenticate (YouTube Music)
There's no official YouTube Music API, so ytmusicapi authenticates by reusing headers from your logged-in browser session.
Open music.youtube.com in Firefox (recommended — its raw-header copy is more reliable than Chrome's) while logged in.
Open DevTools (
Cmd+Option+I/F12) → Network tab → filter bybrowse.Click into a playlist, or reload the page, to trigger a
browsePOST request.Click that request → Headers tab → toggle Raw headers → select and copy the whole block.
Paste it into a new file named
raw_headers.txtin the project root and save.Run:
python scripts/setup_auth_from_file.pyThis writes
headers_auth.jsonand deletesraw_headers.txt.
Alternatively, python scripts/setup_auth.py does the same thing via an interactive terminal prompt instead of a file, if you prefer to paste directly.
headers_auth.json is equivalent to your logged-in session — never commit it or share it. It's already gitignored.
Verify auth works and sanity-check recommendations before going further:
python scripts/test_recommend.pyThese headers expire/rotate periodically. If tools start failing with an auth error, redo this step.
3. Add to Claude Code
claude mcp add commendation -s user \
-e COMMENDATION_AUTH_PATH="$(pwd)/headers_auth.json" \
-- "$(pwd)/.venv/bin/python" "$(pwd)/server.py"-s user makes it available in any Claude Code session, not just this directory. Use absolute paths for the python interpreter, server.py, and COMMENDATION_AUTH_PATH since the server can be launched from any working directory.
For other MCP clients (Claude Desktop, etc.), point them at the same command and env var using their respective config format.
Testing
Unit tests (tests/) cover the pure logic — normalization, scoring, ranking, exclusion filtering, library-cache behaviour (hits, misses, expiry, corruption, top-up, write failures), artist/song search resolution, error translation, and every tool end-to-end (happy path, signal failures, shortfalls, validation errors) — against a hand-rolled fake YTMusic client. No network access or headers_auth.json required. A conftest.py fixture redirects the library cache to a temp path for every test, so runs never touch your real cache.
pip install -e ".[dev]"
pytestCheck coverage with:
pytest --cov=server --cov-report=term-missing197 tests, 97% line coverage across the whole project. tests/test_v2.py covers the mood engine — the vector space, arcs, label resolution and artist propagation, the atlas crawler's resume and rate-limit behaviour, lyric caching, mood sensing, the Claude judge (against a fake client), and every v2 tool end to end. What remains uncovered is _client()'s real YTMusic() construction and the if __name__ == "__main__" entrypoints, neither meaningfully testable without a live auth session.
conftest.py redirects both the library cache and the SQLite store to temp paths for every test, so runs never touch your real data.
scripts/test_recommend.py is a separate, complementary smoke test that hits your real account (see Setup step 2) to sanity-check that auth and live recommendations actually work.
How recommendations are ranked
For each seed song, candidates are pulled from three independent signals:
Radio — YouTube Music's own autoplay/radio for that song.
Related — a separate "related content" signal, algorithmically distinct from radio.
Artist expansion — the seed artist's own other songs, plus top songs from a couple of their related artists.
A candidate's score is how many distinct (seed, signal) combinations surfaced it — the more independent signals agree, the higher it ranks. Every result includes a sources field showing which signals surfaced it, so recommendations are explainable rather than a black box.
Liked Music and every playlist in your library are excluded last, always, as a hard filter — no recommendation can ever be a song you've already liked or already saved anywhere.
Error handling
Tool calls translate common failure modes into clear messages instead of raw tracebacks:
Missing/expired/malformed auth → tells you to rerun
scripts/setup_auth_from_file.py.Rate limiting (HTTP 429) → tells you to wait and retry.
Gated/restricted content → reported as unavailable rather than crashing.
Network errors → reported directly.
If an individual signal (radio, related, or artist expansion) fails for a given seed, that signal is silently skipped for that seed rather than failing the whole recommendation.
License
MIT — see LICENSE.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseAqualityDmaintenanceAn MCP server that enables highly personalized music recommendations from TIDAL based on custom criteria, allowing users to create and manage playlists directly in their TIDAL account.7MIT
- FlicenseNot gradedqualityBmaintenanceAn MCP server for managing YouTube Music playlists via Claude. Add and remove songs, create playlists, and ask Claude to suggest music — all through conversation.
- FlicenseNot gradedqualityDmaintenanceMCP server for YouTube Music that enables searching songs and artists, managing playlists, and authenticating via Google OAuth, using STDIO transport.
- FlicenseNot gradedqualityDmaintenanceA production-grade MCP server that connects YouTube Music to AI assistants, enabling music search, library management, playlist creation, and personalized recommendations through 15 tools, 3 resources, and 3 prompts.
Related MCP Connectors
MCP server for Producer/Riffusion AI music generation
MCP server for Suno AI music generation, lyrics, and covers
MCP server for Google Veo AI video generation
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/umsachde/commendation'
If you have feedback or need assistance with the MCP directory API, please join our Discord server