Skip to main content
Glama
umsachde

commendation

by umsachde

re-com

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, plus a service-neutral music graph) and ranking candidates by how many of them agree, instead of trusting one black-box algorithm.

Discovery doesn't depend on any one service's API. A streaming service can revoke the endpoints a recommender is built on, and Spotify did: for API apps registered after November 2024 without Extended Quota Mode, /recommendations, related-artists, artist-top-tracks and audio-features all return 403/404. Two of re-com's three original signals became unbuildable there and recommend_from_song returned zero songs. So similarity and adjacency now come from a neutral music graph (Deezer) that belongs to no backend, while the provider supplies only whose taste this is — library, history, playlist writes. Native signals are still used wherever they exist and still rank highest; they're just no longer required. See The music graph and PLAN.md §2.3.

Backends: YouTube Music and Spotify. re-com is a general recommendation engine, not tied to one service — re-com itself holds no streaming-service credentials of any kind for either backend. Every call goes through a sibling *-mcp server that re-com spawns as an MCP subprocess and that owns auth entirely: ytmusic-mcp for YouTube Music, spotify-mcp for Spotify. Which one a given re-com instance talks to is set once, at process start, via RECOM_PROVIDER — see Setup below. Both are registered as separate MCP server instances (e.g. re-com and re-com-spotify); a single tool call always stays within one provider. See provider.py and PLAN.md §2.1, "The provider seam", for the design.

Tools

Tool

Description

recommend_from_song(video_id=None, song=None, artist=None, limit=20, language=None, match_seed_tempo=False, ...)

Recommend new songs similar to a seed song. Pass video_id directly, or song (optionally with artist). Supports language and tempo filters. Returns {"songs": [...], "notes": [...], "filters": {...}}.

recommend_from_playlist(playlist_id, limit=20, seed_sample_size=5)

Recommend new songs based on an entire playlist (samples seed tracks from it).

songs_by_artist(artist, limit=10)

Return actual songs by a named artist — a direct catalog pull, not a similarity recommendation.

refresh_library(video_ids=None)

Add just-saved songs to the cached library exclusion set instantly, or rebuild it fully when called with no ids. See Library cache.

recommend_for_mood(feeling=None, vector=None, context=None, arc="mirror", limit=20, genres=None, language=None, bpm=None, ...)

v2. Recommend new songs matching how you actually feel, shaped into a sequence that moves. See Mood.

recommend_from_playlist_for_mood(playlist_id, feeling=None, vector=None, context=None, arc="mirror", limit=20, seed_cap=None, ...)

v2. Mood and a playlist together: reads every track, seeds only from the ones that genuinely fit. See Mood + one playlist.

read_my_mood()

v2. Infer your current mood from recent listening, with the evidence for it.

explain_recommendation(video_id)

v2. Why a song was picked, in mood terms.

record_feedback(video_id, reaction)

v2. loved / saved / skipped / wrong_mood. Rejections are never recommended again. See also implicit feedback, which needs no call at all.

index_status()

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:

  1. Resolve the mood to a vector.

  2. Pick seeds from your own library that already sit near it.

  3. Run v1's proven radio / related / artist expansion from those seeds.

  4. Add a fourth signal: songs from YouTube's mood playlists near the target — the only path that reaches outside your existing taste graph.

  5. Rank on signal agreement × mood fit, then assign songs to slots along an arc.

The mood vector

Axis

Range

Low ←→ high

valence

−1…1

despairing ←→ euphoric

energy

0…1

still ←→ frantic

tension

0…1

resolved ←→ anxious. Separates angry from excited — two axes can't tell aggressive workout rap from joyful party pop

depth

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

mirror

Stay where they are and validate it. Default.

lift

Start at their mood, rise gradually across the set.

settle

Descend to calm — an evening wind-down.

deepen

Go further in.

hold

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

llm

Claude reads the lyrics. Handles any language, and irony.

Optional — pip install -e ".[llm]"

lyrics

Lyrics fetched and cached (2 API calls/song, incl. the negative result)

atlas

Membership in YouTube's own mood playlists — 1,592 listings, 65,438 tracks, 104,028 memberships

A crawl, YouTube only

graph_atlas

Membership in Deezer playlists found by mood search. Works on any backend

A crawl

artist

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. The graph_atlas layer attacks the same gap from the other side by searching for that catalogue by name, and unlike atlas it exists on every backend.

After a full crawl, measured: 71.3% library coverage — 553 songs from artist propagation, 480 from playlist membership.

Mood + one playlist

"I feel like this — look at this playlist and find me songs."

recommend_from_playlist samples five tracks at random and ignores mood entirely; recommend_for_mood honours the mood but draws seeds from the whole library. recommend_from_playlist_for_mood is the intersection, and it treats the playlist as evidence rather than as a bag to sample from:

  1. Every track in the playlist is read and scored for mood fit.

  2. Only genuine matches seed the search — a track whose mood can't be resolved, or that fits the target no better than an unlabelled song is assumed to, is not used. Seeding from tracks that don't fit would just hand back the playlist's own mood.

  3. Seeds are spread across artists and capped (default 20, seed_cap to override). Each seed costs ~4 API calls, so a 100-song playlist would otherwise fire ~400.

seed_report says how many tracks were considered, how many were genuine, and how many were capped away. If nothing fits, it says so and suggests recommend_for_mood instead rather than returning off-mood results.

Exclusion is the same hard guarantee as everywhere else: nothing from Liked Music, nothing from the seed playlist, nothing from any other playlist. The 25% filler cap applies too.

Turning a recommendation into a playlist

re-com is read-only — it never creates a playlist or adds a track anywhere. That is deliberate: a recommendation engine that also mutates the library can't be trusted to have excluded what it just added.

So "recommend me songs for this mood and make it a playlist" is two tools, in this order:

  1. recommend_for_mood(...) (or recommend_from_playlist_for_mood(...)) to get the songs.

  2. A playlist-management tool — e.g. the separate ytmusic MCP server's create_playlist / add_to_playlist — to create it from the returned videoIds.

  3. refresh_library(video_ids=[...]) with the ids you just added, so they're excluded from the next recommendation instantly, without a ~20s rebuild. Without this, the cached exclusion set is stale for up to RECOM_CACHE_TTL once the served window passes, and a later call can recommend a song you just saved.

Honesty about shortfalls

limit is a ceiling, not a guarantee. recommend_for_mood's arc sequencer will fill every requested slot from whatever's left in the candidate pool if you let it, quality be damned -- asking for 100 with 7 songs that genuinely fit the mood otherwise came back as 100, the other 93 being progressively worse guesses (an unrated song still gets a placeholder fit score and can still win a slot).

Filler -- unrated, or rated but a poor fit -- is capped at 25% of limit. Genuine matches (rated, with a real fit above the unrated baseline) are never capped or dropped for this reason. Asking for 100 with 7 genuine matches returns 32 (7 + 25), not 100. The result's match_quality field reports genuine/requested/fluff_cap/fluff_used, and notes explains it in plain language.

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 scoring
python scripts/quality_check.py --similarity --repeat # the similarity path

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

--similarity measures the other half of the engine — recommend_from_song and recommend_from_playlist, which had no number at all and were judged by impression. It reports how many independent signals backed each pick (always against the ceiling that backend allows, since Spotify has no native discovery signals and would otherwise look like a regression), how much of a result one artist owns, how much unrelated seeds return the same songs, and a native-vs-graph A/B reporting churn (how much of the top N the graph replaced) alongside the corroboration delta (whether what replaced it is better agreed-upon). The delta is the one that answers "helping or diluting" — churn alone can't, because both arms truncate to the same length.

Pass --repeat to get a noise floor in the same run, and read every delta against it. The floor is not the same on both backends: YouTube measures 0.87 because its radio/related endpoints vary between identical calls, while Spotify measures 1.00 — every candidate there comes from the locally cached music graph, so there is no upstream variance to absorb. A 5% delta means nothing on YouTube and is real on Spotify.

Learning without being told

record_feedback only fires when someone remembers to call it, which in practice is almost never — so the engine also learns from what it can observe. Two tables it already keeps are enough: recommendation (what was served, and when) and history_log (what was actually played, timestamped by scripts/snapshot_history.py). Diffing them yields two signals for free:

Inferred

When

Strength

played

The song turned up in the history log after being recommended.

Strong — the recommendation landed.

ignored

It didn't, and ≥3 history snapshots have been taken since, so there was real listening it could have shown up in.

Weak, and treated as such.

The threshold matters: below it, "not played" almost always means "the cron hasn't run yet" rather than "they didn't want it". Songs under it are reported as pending and nothing is inferred.

Inferred evidence never hard-excludes. A stated skipped/wrong_mood bans a song permanently; ignored only demotes, because absence from a history log has too many innocent explanations (they never opened the playlist, they listened on another device). The two live in the same table under different source values, and rejected_video_ids reads only the explicit ones.

What's learned is applied per artist, not per song — a song that got played usually gets liked, at which point the library exclusion means it can never be recommended again anyway. What survives is the direction it pointed in. The multiplier is bounded to 0.75–1.25 and saturates at 3 net reactions, so a learned preference breaks ties without overruling signal agreement, and any nudge it applies is reported in the result's affinity field rather than silently reordering things.

Inference runs automatically on every mood recommendation (pure local SQL, ~5ms, idempotent) — there's nothing to schedule. index_status() reports what's accumulated so far.

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 --claude

Check 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/re-com && .venv/bin/python scripts/snapshot_history.py

Configuration

Env var

Default

Meaning

RECOM_DB_PATH

~/.recom/store.db

Mood index, labels, history, feedback. Scoped per backend — Spotify uses store-spotify.db.

RECOM_JUDGE_MODEL

claude-opus-5

Model for lyric-based labelling.

RECOM_JUDGE_EFFORT

low

Effort level for that labelling.

RECOM_JUDGE_BATCH

12

Songs per labelling request.

RECOM_SEED_WORKERS

6

How many seeds are gathered concurrently. See Speed.

RECOM_GRAPH

1

Set 0 to disable the music graph and use native signals only.

LASTFM_API_KEY

(none)

last.fm API key (free, last.fm/api/account/create; the shared secret is not needed). Enables the second track-level similarity source; set it on every re-com instance.

LISTENBRAINZ_TOKEN

(none)

ListenBrainz user token (free, from listenbrainz.org/settings). Enables track-level similarity; set it on every re-com instance.

RECOM_GRAPH_DB_PATH

~/.recom/graph.db

The music-graph cache. Shared by every backend — not scoped per provider.

RECOM_SPOTIFY_CAPABILITIES

(none)

Comma-separated radio,related,artist to re-enable Spotify's native signals if your app has Extended Quota Mode.

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

script

Title written in Gurmukhi, Devanagari, Arabic, Hangul, Kana or Han

100

library

Your own playlist names (matched loosely — Punjabu counts)

50

genre

YouTube's genre-category pages

10

genre (English)

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=True keeps 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=False disables 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)
  • bpm biases ranking toward a tempo; bpm_min/bpm_max bound it hard.

  • match_seed_tempo=True uses 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).

The music graph

Similarity, artist adjacency and the mood corpus come from Deezer — no key, no auth, no attribution — and belong to no backend. This is what makes re-com a recommendation app rather than a wrapper around one service's algorithm.

Artist adjacency additionally comes from ListenBrainz, a second and independent source: measured at Jaccard 0.137 against Deezer's, so it both corroborates (60-65% of Deezer's neighbours on this library's Punjabi catalogue) and contributes artists Deezer does not carry. Its coverage is partial by design of the data, not by configuration — some artists return no neighbours, and a seed it cannot answer for quietly falls back to Deezer alone. Deezer remains the catalogue: ListenBrainz returns artists, never tracks.

With a LISTENBRAINZ_TOKEN set, ListenBrainz also supplies track-level similarity (graph_similar_lb) — the only signal that tells two songs by the same artist apart. It is dense on Western pop and thin on the Punjabi/Bollywood catalogue (often 0–1 neighbours). Without a token it is simply off.

With a LASTFM_API_KEY set, last.fm's track.getSimilar adds a second track-level source (graph_similar_lfm) that reaches the Punjabi catalogue ListenBrainz cannot — 50 neighbours each for seeds where ListenBrainz returns none. Recent Bollywood film songs remain uncovered by both.

Signal

Source

Available on

radio

the provider's per-track radio/autoplay queue

YouTube Music

related

the provider's per-track related-content feed

YouTube Music

artist

the provider's artist catalogue + related artists

YouTube Music

graph_artist

the seed artist's Deezer catalogue

every backend

graph_radio

Deezer artist radio

every backend

graph_related

adjacent artists' catalogues on Deezer

every backend

graph_related_lb

ListenBrainz-adjacent artists' catalogues

every backend

graph_similar_lb

ListenBrainz track-level similar recordings

every backend, with LISTENBRAINZ_TOKEN

graph_similar_lfm

last.fm track-level similar tracks

every backend, with LASTFM_API_KEY

Each backend declares what it can actually supply (provider.capabilities()), the engine runs whatever is available, and ranking is unchanged: a candidate scores by how many distinct signals agree on it. A backend with fewer native signals simply has fewer sources agreeing rather than returning nothing. Measured live: Spotify went from 0 songs to 10; YouTube's top ten is unchanged and still native-dominated.

If Spotify ever grants your app Extended Quota Mode, set RECOM_SPOTIFY_CAPABILITIES="radio,related,artist" to turn the native signals back on. Set RECOM_GRAPH=0 to disable the graph entirely and run on native signals alone.

Known costs, stated plainly. Deezer has no track-level radio (/track/{id}/radio doesn't exist), so graph similarity is artist-centric — genuinely weaker than YouTube's per-track radio, which is why native signals are added to rather than replaced. And the graph returns "Diljit Dosanjh — Born to Shine", not an id your backend understands, so candidates are matched back to the provider by search. That resolution is lazy: results are ranked on graph metadata first and only the top of the pool is ever resolved, so a fully-native response does none at all. A candidate that can't be matched is dropped with a note rather than substituted.

The graph cache is deliberately not per-backend

~/.recom/graph.db is shared by every provider instance — the exact opposite of the per-backend stores below, and for the exact opposite reason. Deezer ids are service-neutral: "Excuses — AP Dhillon is Deezer track 1508646682" is equally true for the YouTube instance and the Spotify one. Scoping it per backend would resolve every artist twice and grow a third copy on the next service. Override with RECOM_GRAPH_DB_PATH.

Negative results are cached alongside positive ones, so a song Deezer genuinely doesn't carry costs two searches once rather than on every pass forever.

A mood corpus that works on any backend

recommend_for_mood originally needed YouTube Music's editorial "Moods & moments" playlists, which is why mood was YouTube-only. Deezer allows exactly what Spotify forbids — playlists can be searched and read — so scripts/build_graph_atlas.py builds the same kind of evidence for every backend:

python scripts/build_graph_atlas.py                     # crawl, materialize, propagate
python scripts/build_graph_atlas.py --stage crawl --limit 20   # short trial run

The queries are deliberately not English-only. YouTube's mood playlists covered just 4.1% of this library's liked songs, with the misses concentrated on its Punjabi and Bollywood catalogue, so the neutral atlas searches for that catalogue by name (punjabi sad, bollywood romantic, …). Moods are keyed by Deezer id and inherited by provider tracks through the cached id bridge, so the labelling work is done once no matter how many services you connect.

It ranks below the native atlas in label.SOURCE_PRIORITY — a playlist merely titled "sad songs" was named by a stranger, where a YouTube mood playlist was filed by the service under a taxonomy — and above artist propagation. Best available source still wins outright.

One store per backend

Every id re-com persists — library rows, cached exclusion sets, mood labels, feedback — belongs to exactly one backend's namespace, and they are not interchangeable: a YouTube videoId is 11 characters, a Spotify track id is 22. So each provider instance gets its own files:

YouTube Music (default)

Spotify

Store

~/.recom/store.db

~/.recom/store-spotify.db

Exclusion cache

~/.recom/library_cache.json

~/.recom/library_cache-spotify.json

Music graph

~/.recom/graph.db

~/.recom/graph.dbshared on purpose

The default backend keeps the original unsuffixed names, so an existing install keeps its crawled atlas, labels and history rather than waking up to an empty store. RECOM_DB_PATH / RECOM_CACHE_PATH still override outright if set.

The music graph is the one deliberate exception: Deezer ids belong to no service, so splitting that file would duplicate work without preventing any mistake.

This is a correctness guarantee, not tidiness. Sharing one exclusion set between backends doesn't merely mix the data — it silently voids the promise this project exists for, because no YouTube videoId can ever equal a Spotify track id, so a 1,499-entry exclusion set matches nothing and every "new" recommendation could already be in your library. Mood tools are refused outright on a backend with no mood index rather than returning another provider's ids (see below).

Speed

Two costs dominate a recommendation: building the library exclusion set (solved by the library cache below) and gathering candidates from each seed.

Each seed costs ~4 sequential network round-trips, and a mood recommendation uses six seeds. Run serially that's the sum of all six; nothing about it needs to be, since no seed depends on another and the results are pooled regardless. Measured on the real account, same six seeds:

Serial

Concurrent

Seed gathering

18.9s

3.1s

recommend_for_mood end to end

~18s

5.7s

Concurrency is capped (RECOM_SEED_WORKERS, default 6) rather than unbounded: a playlist-seeded mood request can carry 20 seeds, and 20 × ~4 simultaneous in-flight requests is exactly the rate-limit exposure worth avoiding. Lower it if a backend starts throttling.

Results are not identical run-to-run, and weren't before this either. YouTube's radio is non-deterministic — measured, two serial runs of the same seeds overlap only 0.793, while serial vs. concurrent overlaps 0.819. Concurrency is not what varies the output; the API is.

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

recommend_from_song end to end

~24s

4.3s

songs_by_artist end to end

~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 — right after a playlist-management tool adds tracks, call refresh_library(video_ids=[...]) with those ids to add them to the cache instantly. refresh_library() with no ids rebuilds the whole set (~20s), which is only needed when the library changed in ways you can't list.

A song you were just handed doesn't come straight back. Every recommendation tool records what it returned, and those songs stay excluded from every tool for RECOM_SERVED_TTL, whether or not you saved them. Asking for "more like this" gives you more, not the same list again.

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

RECOM_CACHE_PATH

~/.recom/library_cache.json

Where the cached set lives (~22 KB). Scoped per backend.

RECOM_CACHE_TTL

21600 (6 hours)

How long a cached set stays usable. Set to 0 to disable caching and rebuild on every call.

RECOM_SERVED_TTL

7200 (2 hours)

How long a song any tool returned stays excluded from later calls. Set to 0 to allow repeats.

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. Connect to a backend

re-com holds no streaming-service credentials of its own for either backend — every call goes through a sibling *-mcp server that re-com spawns as a subprocess over MCP and that owns login entirely. Pick one (or set up both as two separate registrations):

YouTube Music (ytmusic-mcp)

  1. Set up ytmusic-mcp and authenticate it (see that project's own README) — this is the only place YouTube Music credentials live.

  2. Point re-com at it via RECOM_YTMUSIC_MCP_COMMAND (its interpreter) and RECOM_YTMUSIC_MCP_ARGS (its server.py path). RECOM_PROVIDER=youtube is the default, so it doesn't need to be set explicitly.

claude mcp add re-com -s user \
  -e RECOM_YTMUSIC_MCP_COMMAND="/path/to/ytmusic-mcp/.venv/bin/python" \
  -e RECOM_YTMUSIC_MCP_ARGS="/path/to/ytmusic-mcp/server.py" \
  -- "$(pwd)/.venv/bin/python" "$(pwd)/server.py"

Spotify (spotify-mcp)

  1. Set up spotify-mcp and authenticate it (see that project's own README) — this is the only place Spotify credentials live.

  2. Register a second, separate re-com instance with RECOM_PROVIDER=spotify and RECOM_SPOTIFY_MCP_COMMAND / RECOM_SPOTIFY_MCP_ARGS pointing at it:

claude mcp add re-com-spotify -s user \
  -e RECOM_PROVIDER=spotify \
  -e RECOM_SPOTIFY_MCP_COMMAND="/path/to/spotify-mcp/.venv/bin/python" \
  -e RECOM_SPOTIFY_MCP_ARGS="/path/to/spotify-mcp/server.py" \
  -- "$(pwd)/.venv/bin/python" "$(pwd)/server.py"

What's different from YouTube Music, in practice — and it is severe. Measured against a real app registration (2026-08-23), Spotify has revoked every discovery endpoint for apps created after November 2024 without Extended Quota Mode:

Still works

Returns 403/404

Saved tracks, playlists, recently played, top tracks/artists

/recommendations (404)

search (tracks, artists, playlists)

artist_related_artists, artist_top_tracks

track, artist, artist_albumsalbum_tracks

audio_features, audio_analysis

Reading any other user's playlist

categories, featured_playlists, new_releases

Two of the three discovery signals are therefore unbuildable on Spotify. spotify_client.py degrades gracefully — a forbidden endpoint is skipped rather than failing the call — but graceful degradation of every signal is nothing, and before v6 recommend_from_song returned 0 results there. v6 fixed that: similarity and adjacency now come from the music graph, which belongs to no backend, and recommend_from_song returns a full result set on Spotify (measured: 10 songs, 3.4s warm). Native signals are still preferred wherever the registration allows them — set RECOM_SPOTIFY_CAPABILITIES if your app has Extended Quota Mode.

recommend_for_mood, recommend_from_playlist_for_mood and read_my_mood work on both backends as of 2026-08-29. They were YouTube-only, because they need a mood index built from the service's own playlists and only YouTube has one; the neutral graph atlas now supplies that for any backend (measured: 40.2% mood coverage on a real Spotify library, against YouTube's editorial atlas at the same 40%). Wiring the graph into the mood path improved YouTube too — mean mood fit 0.797 → 0.820 and cross-mood overlap 0.121 → 0.096, with warm latency unchanged, since a fully-native result performs zero extra lookups. A backend with neither a native atlas nor graph coverage still refuses with an explanatory error rather than returning ids from the wrong namespace. Quality on Spotify is real but below YouTube's (0.201 cross-mood overlap vs 0.096, on a much smaller library); see PLAN.md §3, "Quality". recommend_from_song, recommend_from_playlist, songs_by_artist, refresh_library, record_feedback, explain_recommendation and index_status are available on both backends.


-s user makes either registration available in any Claude Code session, not just this directory. Use absolute paths throughout, 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 vars using their respective config format.

If a backend's *-mcp auth expires or rotates, tool calls fail with a clear message pointing at re-authenticating there — re-com has nothing of its own to re-run.

Offline maintenance scripts still authenticate directly. scripts/build_atlas.py, scripts/label_library.py, scripts/build_genres.py, scripts/build_tempo.py, scripts/snapshot_history.py, and scripts/quality_check.py are indexing/labelling jobs you run yourself from the command line, not part of the live tool-call path — they still use ytmusicapi directly and need their own headers_auth.json (see scripts/setup_auth_from_file.py / scripts/setup_auth.py, and RECOM_AUTH_PATH). That's a separate, unrelated credential from ytmusic-mcp's. ytmusicapi itself is an optional dependency (pip install -e ".[youtube]") — the live path never imports it, so a Spotify-only install doesn't need a YouTube Music client it will never call.

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 client matching ytmusic_client.YTMusicClient's surface. tests/test_spotify_client.py and tests/test_provider.py cover spotify_client.py's shape-translation logic (search/playlist/watch-playlist/related/artist/history normalization, graceful degradation when a restricted endpoint fails) and RECOM_PROVIDER backend selection the same way, against a fake _call. No network access, either *-mcp server, or any streaming-service credential 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]"
pytest

Check coverage with:

pytest --cov=server --cov-report=term-missing

532 tests across the whole project, run in CI on Python 3.10/3.12/3.13 (.github/workflows/tests.yml). tests/test_provider_isolation.py covers the per-backend split — path scoping, the default backend keeping its original filenames (so an existing install isn't orphaned), explicit env overrides still winning, and that each mood tool refuses on a foreign backend before reaching the provider while the v1 tools stay reachable. tests/test_feedback.py covers implicit feedback (the recommendation/history diff, its idempotence, retraction of a wrong ignored verdict, and the guarantee that inferred evidence never reaches the hard-exclusion set) and the bounded artist affinity it feeds. tests/test_concurrency.py covers concurrent seed gathering — that it really is concurrent (proved with a threading.Barrier, which can only clear if every seed is in flight at once, rather than a timing assertion that could pass by luck), that it stays within the worker cap, and that each call site's failure semantics are preserved. 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 (YouTube-only, per the mood engine's atlas dependency noted above). What remains uncovered is _client()'s real YTMusicClient()/SpotifyClient() construction (which actually spawns the sibling *-mcp subprocess) and the if __name__ == "__main__" entrypoint, neither meaningfully testable without a live connection.

conftest.py redirects both the library cache and the SQLite store to temp paths for every test, so runs never touch your real data.

Three layers, because the unit suite structurally can't see everything

A fake client can't model a real SQLite connection crossing a real thread pool — and that's what broke: recommend_from_playlist raised ProgrammingError on both backends, for any multi-track playlist, from the v6 merge until 2026-08-29. Every test passed the whole time, because the only live smoke test went through recommend_from_song, which deliberately keeps a single seed on the calling thread. So verification is split three ways:

Runs

Covers

pytest

CI, every push

pure logic against fakes — no network, no credentials

scripts/smoke_all.py

by hand, before a release

every tool × every backend, against the real account

scripts/quality_check.py

by hand, when ranking changes

mood fit, cross-mood overlap, distinctiveness; with --similarity, signal agreement, artist concentration, cross-seed overlap and the native-vs-graph A/B

scripts/orchestrate.py

by hand, an experiment

the v0 agent loop over the MCP tools (PLAN.md 7.7) — whether an agent can satisfy constraints no single tool call does. Needs pip install -e ".[agent]" and spends tokens per run

tests/test_graph_concurrency.py is the deliberate exception to "no real resources": it runs a real graph connection across a real thread pool, because that's the one shape a fake can't reproduce. Its tests were verified by removing the fix and confirming they fail.

python scripts/smoke_all.py                      # every configured backend
python scripts/smoke_all.py --provider spotify   # just one

Each backend runs in its own subprocess (RECOM_PROVIDER is read once at import, so one process can't honestly test two), and each tool is checked three ways: it returns something or states why it didn't, it excludes everything already in your library, and it finishes within a latency ceiling. A backend with no command configured is reported as skipped, never as a pass. record_feedback is behind --include-writes because it writes to your real store. tests/test_smoke_harness.py unit-tests the harness's own judgement — a smoke test that can't fail is worse than none, because it reads as evidence.

How recommendations are ranked

For each seed song, candidates are pulled from three independent signals:

  1. Radio — YouTube Music's own autoplay/radio for that song.

  2. Related — a separate "related content" signal, algorithmically distinct from radio.

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

On Spotify, the same three sources labels (radio/related/artist) are built from Spotify's own endpoints instead: radio from seed-track /recommendations, related from the seed artist's related artists' top tracks, and artist from the seed artist's own top tracks (plus a couple of related artists', same as YouTube Music). See spotify_client.py for the mapping and its limitations (no full-catalog endpoint, and /recommendations/related-artists may be 403'd for newer Spotify API apps).

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 YouTube Music auth, rate limiting, gated/restricted content, and network errors are all translated by ytmusic-mcp itself (re-com has no auth of its own to point at) — its message tells you what to do, e.g. re-authenticate there.

  • If ytmusic-mcp isn't reachable at all (not configured, or the subprocess won't start), re-com says so plainly rather than hanging.

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

Available Tools

10 tools
explain_recommendationA

Explain why a song was recommended, in mood terms.

Reports the song's mood vector, which layer produced it (Claude reading the lyrics, YouTube mood-playlist membership, or the artist's own average), the named moods it sits closest to, and the mood it was last served against.

ParametersJSON Schema
NameRequiredDescriptionDefault
video_idYes

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A3.9/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden, and it does disclose the internal mechanics: the mood vector, which of three source layers produced it, the nearest named moods, and the mood last served against. It stops short of stating error behavior (e.g., what happens if the song was never recommended) or confirming it is side-effect-free.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core purpose in the first clause, followed by one sentence enumerating what is reported. No filler, no repetition of the tool name.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a read-only explanatory tool with an output schema present, the description need not restate return formats and is complete enough to call correctly. The only real gap is the undocumented video_id, which is minor given the output schema carries the result contract.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% for the single required video_id, and the description never mentions it, so it adds no meaning beyond the bare property name. The parameter is fairly self-evident, which keeps this from being a 1, but the description does not compensate for the coverage gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Explain why a song was recommended') and scopes it to mood terms. This clearly separates it from the action-oriented siblings like recommend_from_song and recommend_from_playlist, which produce recommendations rather than explain them.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied: call this after a recommendation exists to understand its basis. There is no explicit when-to-use, no statement of prerequisites (e.g., must the song have been recommended?), and no routing to alternatives among the ten siblings.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

index_statusA

Report how much of the mood index exists, so gaps are visible not silent.

Covers the mood-playlist crawl, how much of the listener's library carries a mood label and from which layer, and whether Claude-based labelling is configured. Low coverage means recommendations are ranking mostly on signal agreement rather than on mood -- worth saying out loud.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.1/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description must carry the behavioral burden, and it does reasonably well by disclosing exactly what the report contains and how to interpret low coverage (ranking on signal agreement rather than mood). It never explicitly states the operation is read-only or side-effect-free, which leaves one gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The core purpose is front-loaded in the first sentence and the rest adds diagnostic meaning. A few phrases ('gaps are visible not silent', 'worth saying out loud') are stylistic flourishes that could be trimmed, but the passage stays short.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given an output schema exists and there are no parameters or annotations, the description supplies enough conceptual context to know what the report conveys and why it matters. It stops short of describing invocation conditions, but little else is needed for a no-arg diagnostic.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so per the rubric the baseline is 4. There is no parameter syntax to document and the description introduces none.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource ('Report how much of the mood index exists') and enumerates the scope it covers (crawl coverage, labelled library layers, Claude-labelling config). This clearly separates it from all siblings, which are recommendation, retrieval, or mutation tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is only implied: the tool exists to surface index gaps and the description notes the implication of low coverage for recommendations, which hints at when it's worth calling. But there is no explicit 'use this when...' instruction or contrast with alternatives like refresh_library.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

read_my_moodA

Infer the listener's current mood from recent listening, with evidence.

Returns the inferred vector, a plain-language described, a confidence, and evidence -- the specific observations behind it (a song on repeat, one artist dominating, valence drifting across the session).

Lead with the evidence, not the verdict. "You've had these three on loop since yesterday -- want something that sits there with you, or something that lifts?" is the point of this tool; asserting "you are sad" is not. Mood inference is often wrong, so offer it as a read the user can correct.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.1/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does well: it discloses the epistemic limits of the tool ('mood inference is often wrong'), instructs that the result be presented as correctable, and names the concrete signals used. It omits anything about latency, caching, or how much history is scanned, but the fallibility disclosure is unusually valuable context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core purpose, then the return shape, then the interaction rule. The quoted example earns its space by encoding tone, though the enumeration of return fields partially duplicates the output schema and could be trimmed.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter tool with a rich output schema and no annotations, this covers purpose, output framing, and the key caveat about unreliable inference. The only real gap is not routing the agent to the mood-recommendation siblings when the user actually wants a suggestion.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so the baseline is 4. There is nothing for the description to disambiguate, and it correctly avoids inventing input semantics.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource — infer the listener's current mood from recent listening — and frames it as an evidence-backed read, which cleanly separates it from the recommendation siblings (recommend_for_mood, recommend_from_playlist_for_mood). An agent can tell what this returns without opening the output schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives clear interaction guidance (lead with evidence, offer the inference as a correctable read) but never says when to call this instead of, or before, recommend_for_mood. The when-to-use case is only implied by the conversational framing.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

recommend_for_moodA

Recommend new songs that match how the listener actually feels right now.

Unlike recommend_from_song, the mood decides where candidates come from: seeds are drawn from the listener's OWN library nearest the target mood, then expanded through radio/related/artist signals and songs from YouTube's mood playlists. Results are still guaranteed absent from their library.

Describing the mood -- in priority order: vector The precise path, and the one to prefer. A dict with valence (-1..1, despairing->euphoric), energy (0..1, still->frantic), tension (0..1, resolved->anxious; this is what separates angry from excited) and depth (0..1, background ->lyric-forward). YOU should read the user's words and set these -- you understand "wistful but still wants to get things done" far better than any keyword list. feeling Their words verbatim, as a fallback when you'd rather not commit to numbers. Matched against a mood-word lexicon. context One of: Chill, Sleep, Focus, Commute, Feel good, Romance, Energize, Workout, Party, Gaming, Sad. If none are given, the mood is inferred from recent listening history.

arc shapes the sequence rather than returning a flat mood-matched set: mirror stay where they are and validate it (default) lift start where they are, rise gradually -- never jump straight to upbeat when someone is low, it reads as being told to cheer up settle descend to calm; an evening wind-down deepen go further in; sometimes you want to sit in it properly hold stay in a band with energy as a curve (workout: warmup/peak/cooldown)

genres optionally restricts the seeds to the listener's own genre playlists, e.g. ["Punjabi", "Hip-Hop & Rap"].

language / exclude_languages filter the RESULTS, e.g. language=["english"]. Strict by default: a candidate with no language label is dropped, because someone asking for English only wants a guarantee. The response says how many were dropped and why; pass allow_unlabelled_language=True to keep them.

bpm biases ranking toward a tempo (half- and double-time count as close). bpm_min/bpm_max bound it instead. Songs with no known BPM are KEPT and simply not scored on tempo -- Deezer has no tempo for much of the non-English catalogue, so dropping them would delete whole languages.

limit is a ceiling, not a guarantee. If fewer than limit songs genuinely fit the mood (rated, with a real fit -- not just an unrated placeholder score), the shortfall is NOT padded with weak filler to hit the number. Filler is capped at 25% of limit: asking for 100 with 7 genuine matches returns 32 (7 + 25), not 100. See match_quality in the result for the genuine/filler breakdown, and notes for the human-readable version.

If they also point at a specific playlist ("look at this playlist and recommend me songs for how I feel"), use recommend_from_playlist_for_mood instead -- it seeds from that playlist's own fitting tracks rather than from the whole library.

This is READ-ONLY. If they asked for a PLAYLIST rather than a list, this tool is step one of three: get the songs here, create the playlist from the returned videoIds with a playlist-management tool, then call refresh_library() so those tracks are excluded from later recommendations.

The result carries target (the mood aimed at), target_origin (where it came from), seeds (which of their songs it grew from), notes (caveats worth repeating to the user), match_quality (genuine vs. filler counts) and songs, each with its slot, mood fit and which signals surfaced it.

ParametersJSON Schema
NameRequiredDescriptionDefault
arcNomirror
bpmNo
limitNo
genresNo
vectorNo
bpm_maxNo
bpm_minNo
contextNo
feelingNo
languageNo
exclude_languagesNo
allow_unlabelled_languageNo

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description must carry the full behavioral burden and it does: declares READ-ONLY, guarantees results absent from library, discloses the strict language filter and the unlabelled-drop behavior, and explains the 25% filler cap with a concrete example (7 + 25 = 32). These are non-obvious traits an agent cannot infer elsewhere.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with purpose and organized under clear parameter headers, so it scans well despite its length. It is verbose and occasionally repetitive (the filler logic is restated in both `limit` and the result description), but nearly every sentence carries operational detail that would otherwise be missing.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present the description need not explain return values, yet it still previews `target`, `seeds`, `match_quality`, and `notes` to tell the agent what to surface to the user. Combined with the alternative routing and the playlist hand-off, an agent has everything needed to call it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% and the schema has no enums, so the description compensates fully: it defines vector's four numeric dimensions with ranges and semantics, enumerates all eleven `context` values, documents each `arc` mode, and explains bpm_min/max biasing, language filtering, allow_unlabelled_language, and limit's ceiling-not-guarantee semantics. All 12 params gain meaning beyond the bare schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource ('recommend new songs that match how the listener actually feels') and immediately contrasts its seed-selection mechanism against recommend_from_song. An agent can distinguish it from every sibling without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly names when to switch: 'If they also point at a specific playlist ... use recommend_from_playlist_for_mood instead.' It also clarifies the multi-step playlist workflow (get songs here, create playlist, call refresh_library) and that this tool is not for building playlists directly.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

recommend_from_playlistA

Recommend new songs based on an entire playlist.

Randomly samples up to seed_sample_size tracks from the playlist as seeds (the whole playlist if it's smaller), runs the same multi-signal candidate generation as recommend_from_song for each, and pools/ranks the results. Never returns a song already in Liked Music, already in the source playlist, or already in ANY other of the user's playlists.

The library exclusion set is cached for speed; newly liked songs are always honoured, but call refresh_library() after adding songs to a playlist by other means.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNo
playlist_idYes
seed_sample_sizeNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.3/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does so well: it discloses the sampling strategy (up to seed_sample_size random seeds), the pooling/ranking approach, and hard output guarantees (never returns songs in Liked Music, the source playlist, or any other playlist). It also flags a caching staleness risk and the refresh_library remedy, which is exactly the kind of behavioral caveat an agent needs before trusting results.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the one-line purpose, then layers mechanism, guarantees, and the caching caveat in short paragraphs; every sentence carries information. Slightly verbose in restating the multi-signal pipeline, but no filler sentences.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return-value explanation is unnecessary, and the description covers seeding, exclusions, and cache behavior. The main gap is the unexplained `limit` parameter and no mention of failure modes for an invalid playlist_id, but overall it is sufficient to invoke the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate for three undocumented parameters. It explains seed_sample_size concretely (random seed count, with whole-playlist fallback) and implies playlist_id's role, but never explains what `limit` (default 20) controls. Partial compensation only.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Names a specific verb and resource ('Recommend new songs based on an entire playlist') and explicitly differentiates its mechanism from the sibling recommend_from_song ('runs the same multi-signal candidate generation as recommend_from_song for each, and pools/ranks'). An agent can distinguish it from recommend_from_song and recommend_from_playlist_for_mood without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives clear context for when this tool applies (you have a whole playlist rather than one seed song) and an explicit follow-up call ('call refresh_library() after adding songs to a playlist by other means'), which routes the agent to a sibling when the cache is stale. It stops short of an explicit 'use recommend_from_song when you only have one track' exclusion, so it is strong but not fully enumerated.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

recommend_from_playlist_for_moodA

Recommend new songs from ONE playlist, shaped by how the listener feels.

For "I feel like this -- look at this playlist and find me songs". Use this over recommend_from_playlist whenever a mood is part of the ask, and over recommend_for_mood whenever a specific playlist is.

Unlike recommend_from_playlist, which samples a few tracks at random and ignores mood entirely: EVERY track in the playlist is read and scored for mood fit, and only genuine matches -- tracks whose own mood resolves and actually fits the target -- are used as seeds. An off-mood playlist therefore yields few seeds or none, which is reported rather than papered over by seeding from tracks that don't fit.

Seeding costs ~4 API calls per seed, so the best-fitting seeds are capped (default 20, override with seed_cap). seed_report in the result says how many tracks were considered, how many were genuine, and how many were capped away.

Mood arguments behave exactly as in recommend_for_mood (vector preferred, then feeling, then context; falls back to inferred mood). arc shapes the sequence the same way. Results are guaranteed absent from Liked Music, from this playlist, and from every other playlist -- and limit is a ceiling, not a guarantee: filler is capped at 25% of it, same as recommend_for_mood.

This is READ-ONLY -- it never creates a playlist or adds anything anywhere. To turn the result into a real playlist, pass the returned videoIds to a playlist-management tool, then call refresh_library() so the new tracks are excluded from later recommendations.

ParametersJSON Schema
NameRequiredDescriptionDefault
arcNomirror
bpmNo
limitNo
vectorNo
bpm_maxNo
bpm_minNo
contextNo
feelingNo
languageNo
seed_capNo
playlist_idYes
exclude_languagesNo
allow_unlabelled_languageNo

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full behavioral burden and does so richly: read-only guarantee, ~4 API calls per seed with a default cap of 20, seed_report contents (considered/genuine/capped), the fact that off-mood playlists legitimately yield few or no seeds rather than being papered over, exclusion guarantees, and the 25% filler ceiling on `limit`. This is exactly the cost/side-effect/auth context an agent needs.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the one-line purpose and the routing rule before any internals, and every paragraph adds a distinct fact (contrast, cost/seed behavior, mood args, guarantees, read-only follow-up). It is long, and a couple of clauses ('same as recommend_for_mood') are mild repetition, but no paragraph is filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present it correctly avoids re-describing return values while still flagging the meaningful `seed_report` field. For a 13-parameter, no-annotation, no-schema-description tool it covers behavior thoroughly, but the unaddressed bpm/language parameter family is a real completeness gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate and it only partially does. It clarifies vector>feeling>context precedence, arc's shaping role, seed_cap, and limit's ceiling semantics, but 13 parameters exist and bpm/bpm_min/bpm_max, language, exclude_languages, and allow_unlabelled_language are never addressed beyond their self-evident names.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource+scope: 'Recommend new songs from ONE playlist, shaped by how the listener feels.' It explicitly contrasts itself with both closest siblings (recommend_from_playlist and recommend_for_mood) and names the distinguishing dimension (mood presence, playlist specificity).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit routing rules: 'Use this over recommend_from_playlist whenever a mood is part of the ask, and over recommend_for_mood whenever a specific playlist is,' plus a concrete user-facing trigger ('I feel like this -- look at this playlist and find me songs'). It also explains the follow-up workflow (pass videoIds to a playlist tool, then call refresh_library()).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

recommend_from_songA

Recommend new songs similar to a seed song.

Seed the search either with a known video_id, or with song (a free-text title, optionally narrowed with artist) to have the seed resolved via search internally -- e.g. "10 songs that relate to Kryptonite by 3 Doors Down" needs no separate lookup first. Exactly one of video_id or song must be given.

Combines YouTube Music's radio, its separate "related" signal, and the seed artist's own catalog plus related artists' catalogs, then ranks by how many independent signals agreed on each candidate. Never returns the seed song itself, and never returns a song already in Liked Music or in ANY of the user's playlists.

By default candidates can come from OTHER artists too (radio/related signals surface stylistically similar tracks, not just the seed artist's own catalog) -- pass same_artist_only=True to keep only songs credited to the seed's own artist(s), e.g. for "recommend songs BY artist X similar to song Y" requests.

language / exclude_languages filter the RESULTS independently of the seed, which is the point: seeding from a Punjabi song with language=["english"] returns English songs similar to it. Strict by default -- candidates with no language label are dropped, since a Punjabi-seeded pool is mostly Punjabi and keeping unlabelled ones would hand back exactly what was excluded. Pass allow_unlabelled_language=True to relax that; the response always reports what was dropped.

When a language filter leaves too few results -- seeding from a Punjabi song and asking for English usually does -- the surviving songs are used as fresh seeds to reach more of that language in the same neighbourhood, since filtering alone can only return what happened to be in the seed's own pool. Set expand_across_language=False to skip that and get the short list.

match_seed_tempo=True biases results toward the seed's own BPM (half- and double-time count as close). bpm sets a tempo target directly, and bpm_min/bpm_max bound it. Songs with no known BPM are kept and simply not scored on tempo.

Returns {"songs": [...], "notes": [...], "filters": {...}} -- notes carry anything the user should hear about, such as results dropped for having no language label.

The library exclusion set is cached for speed; newly liked songs are always honoured, but call refresh_library() after adding songs to a playlist by other means.

ParametersJSON Schema
NameRequiredDescriptionDefault
bpmNo
songNo
limitNo
artistNo
bpm_maxNo
bpm_minNo
languageNo
video_idNo
max_per_artistNo
match_seed_tempoNo
same_artist_onlyNo
exclude_languagesNo
expand_across_languageNo
allow_unlabelled_languageNo

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.3/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does so: it discloses the multi-signal ranking strategy, that the seed song is never returned, exclusion of Liked Music and all playlists, the default cross-artist behaviour, language-filter strictness with allow_unlabelled_language escape hatch, and the response shape with notes. It even flags a cache-staleness behaviour and the refresh_library() remedy — exactly the kind of non-obvious trait annotations would normally supply.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose and the required-argument rule are front-loaded, and essentially every sentence adds a distinct behavioural fact, so length is largely justified. A few conversational asides ('which is the point:') and repeated restatements of the language-filter rationale could be trimmed without losing information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists so return values need not be re-explained, and the description nonetheless names the response keys and the role of `notes`. Combined with the mutual-exclusion rule, filter semantics, and the cache/refresh_library dependency, an agent has everything needed to call this 14-parameter tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% across 14 parameters, so the description is the only source of meaning, and it explains the semantics of most of them: video_id vs song mutual exclusion, artist narrowing, language/exclude_languages independence from the seed, allow_unlabelled_language, expand_across_language, same_artist_only, and match_seed_tempo/bpm/bpm_min/bpm_max including the half/double-time and missing-BPM cases. It leaves `limit` and `max_per_artist` (the default of 2) undocumented.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The first sentence gives a specific verb and resource ('Recommend new songs similar to a seed song'), which cleanly separates it from the playlist- and mood-seeded siblings by seeding resource. It never names those siblings explicitly, so an agent must infer the routing from the resource noun rather than being told.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Strong in-scope guidance: exactly one of video_id or song must be given, with a worked example ('10 songs that relate to Kryptonite by 3 Doors Down'); it also explains when same_artist_only, language filters, and expand_across_language apply. It does not, however, compare itself against the sibling tools (recommend_from_playlist, recommend_for_mood), so the agent gets no explicit alternative routing.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

record_feedbackB

Record what the listener thought of a recommendation.

reaction is one of: loved, saved, skipped, wrong_mood.

wrong_mood is the valuable one -- it says the song was fine but the mood read was off, which is a different failure from simply not liking it. Anything marked skipped or wrong_mood is never recommended again.

ParametersJSON Schema
NameRequiredDescriptionDefault
reactionYes
video_idYes

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

B3.4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden. It does disclose a meaningful system effect -- 'Anything marked skipped or wrong_mood is never recommended again' -- which is durable state change worth knowing. However, it omits idempotency, auth/permission needs, error behavior, and whether feedback can be changed after recording.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Short and front-loaded: purpose first, then the allowed values, then the consequence. Every sentence carries weight, though the sentimental framing of 'the valuable one' is mildly editorial rather than operational.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be described, and the reaction semantics are covered well. The one gap is the undocumented required `video_id` and no statement of the recording's persistence or reversibility, which leaves the agent guessing on a required field.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, so the description must compensate. It fully enumerates and interprets the `reaction` values, which the schema does not (no enum). But `video_id` is never explained -- the agent must infer where a video id comes from -- leaving half the parameters undocumented.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb+resource ('Record what the listener thought of a recommendation'), making it a feedback sink that is easily distinguished from the sibling recommender tools. It does not explicitly name siblings, but the purpose is unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Use is implied by the recommender sibling set (call this after a recommendation is consumed), and it hints at the value of one reaction type. There is no explicit when-to-use, when-not, or routing to alternatives such as explain_recommendation.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

refresh_libraryA

Rebuild the cached library exclusion set from scratch, right now.

Every recommendation tool excludes songs already in Liked Music or any of your playlists. That set is expensive to build (~20s), so it's cached and reused. Liking a song is picked up immediately regardless, but adding a song to some other playlist is only seen once the cache is rebuilt.

Call this after adding songs to a playlist by other means (e.g. a playlist-management tool) if you want the next recommendation to account for them without waiting out the cache TTL.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does so well: it discloses the ~20s build cost, that the result is cached and reused, and that the effect is only otherwise visible after a cache TTL. It also clarifies the incremental freshness rule (likes immediate, playlist adds deferred), which is non-obvious and directly affects whether the agent should call it.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The action is front-loaded in the first sentence and each subsequent sentence adds real information (cost, cache semantics, call condition). The cache-economics paragraph is slightly more expansive than strictly needed, but it is not wasted.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be described. For a zero-parameter maintenance tool, the description covers the why, the cost, the caching model, and the precise call condition, leaving no material gap for an agent deciding whether to invoke it.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

There are zero parameters, so the baseline is 4 and there is no parameter syntax for the description to clarify. Nothing is missing, but there is also no opportunity to add semantic value here.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Names a specific verb and resource ('Rebuild the cached library exclusion set') and immediately explains its relationship to the sibling recommendation tools, which all consume this set. An agent can distinguish this maintenance tool from recommend_* siblings without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

States the exact trigger ('Call this after adding songs to a playlist by other means') and explicitly contrasts it with the case where it is unnecessary, since liking a song is already picked up immediately. It also names the alternative workflow (a playlist-management tool) that creates the need.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

songs_by_artistA

Return actual songs by a specific artist -- a direct catalog pull, not a similarity recommendation like recommend_from_song/recommend_from_playlist.

Resolves artist (a name) to its YouTube Music channel and pulls its real song catalog, excluding anything already in Liked Music OR in ANY of the user's playlists (not just one, unlike recommend_from_playlist's single-seed-playlist exclusion). Read-only: never adds results anywhere.

This is a hard requirement, not best-effort -- if fewer than limit qualifying songs exist after exclusion, this returns however many were actually found rather than padding the list. Check found vs requested in the result to see whether it fell short.

Remix/feature variants of the same underlying song (e.g. a track and its "(feat. ...)" credit under a different videoId) count once, not once per variant -- see variants_collapsed in the result.

The library exclusion set is cached for speed; newly liked songs are always honoured, but call refresh_library() after adding songs to a playlist by other means.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNo
artistYes

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the burden. It discloses read-only behavior ('never adds results anywhere'), the exclusion set semantics (Liked Music OR any playlist), the hard requirement that it returns fewer than limit rather than padding, and the variant-collapsing behavior. It does not state auth/permission requirements or rate limits, so not a 5.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Multi-paragraph but front-loads the core purpose and sibling differentiation first, then layers in exclusion behavior, shortfall semantics, variant collapsing, and caching. Each paragraph earns its place, though some detail could be trimmed.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool has an output schema, so return-value explanation is largely unnecessary, yet the description references result fields ('found' vs 'requested', 'variants_collapsed') to clarify semantics. Covers exclusion behavior, caching caveat, and refresh_library guidance. Could still note artist-resolution failure behavior, but is largely complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate. It explains 'artist' is a name resolved to a YouTube Music channel and mentions 'limit' as a qualifying-songs count, adding real meaning. But it does not specify limit bounds, defaults effect, or exact artist-matching semantics, leaving gaps for a 0%-coverage schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource ('Return actual songs by a specific artist -- a direct catalog pull') and explicitly distinguishes itself from siblings by contrasting with similarity recommendations like recommend_from_song/recommend_from_playlist. The direct-catalog-pull framing makes it unmistakable.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly contrasts this tool with recommend_from_song and recommend_from_playlist, and clarifies the exclusion scope differs from recommend_from_playlist's single-seed-playlist exclusion. It also instructs when to call refresh_library(). Strong when-to-use guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 10 tool updatesv0.2.0
    • First observedexplain_recommendation
    • First observedindex_status
    • First observedread_my_mood
    • First observedrecommend_for_mood
    • First observedrecommend_from_playlist
    • First observedrecommend_from_playlist_for_mood
    • First observedrecommend_from_song
    • First observedrecord_feedback
    • First observedrefresh_library
    • First observedsongs_by_artist

TDQS

A4.2/5.0

Scored across 10 tools

Disambiguation4/5

Tool names and descriptions clearly delineate purposes: recommend_from_song seeds by a song, recommend_from_playlist seeds by playlist, recommend_for_mood uses mood, recommend_from_playlist_for_mood combines both. songs_by_artist and explain_recommendation are distinct. Minor potential confusion between recommend_from_playlist_for_mood and recommend_for_mood, but descriptions explicitly guide selection.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern (recommend_from_song, read_my_mood, record_feedback, etc.) with snake_case throughout, no deviations.

Tool Count5/5

10 tools provide a well-scoped set covering recommendation generation, library management, feedback, mood reading, explanation, and status—each earns its place without excess.

Completeness5/5

Comprehensive lifecycle coverage: multiple recommendation entry points (by song, playlist, mood, playlist+mood), library refresh, mood inference, feedback recording, explanation, and index status. No obvious gaps for a music recommendation system.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    An MCP server for managing YouTube Music playlists via Claude. Add and remove songs, create playlists, and ask Claude to suggest music — all through conversation.
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    A 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.
    1
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    An MCP server that enables AI applications to search YouTube music videos and manage playlists via the YouTube Data API v3, with OAuth support for playlist and subscription management.
    3
    -