Skip to main content
Glama
kylestratis

Spotify Playlist MCP Server

by kylestratis

spotify_find_similar_tracks

Find similar tracks based on audio features, mood, energy, or genre to discover music, create playlists, or expand existing collections using customizable matching strategies.

Instructions

Find tracks similar to a track, artist, or playlist using audio analysis or genre matching.

Centerpiece of the similarity engine. Supports 8 strategies, custom weighting, and automated playlist creation. For curated playlists, music discovery, and mood-based mixes. Args: Source (one required): track_id, artist_id, or playlist_id Strategy (default: euclidean): euclidean, weighted (needs weights), cosine, manhattan, energy_match (workout), mood_match (relaxation), rhythm_match (running), genre_match (non-catalog scope only) Scope (default: catalog): catalog (recommendations API), playlist (needs scope_id), artist (needs scope_id), album (needs scope_id), saved_tracks Action (default: return_tracks): return_tracks, create_playlist (needs playlist_name), add_to_playlist (needs target_playlist_id) - limit: Results to return, 1-100 (default: 20) - min_similarity: Optional threshold, 0.0-1.0 - weights: Optional custom weights for 'weighted' strategy (e.g., {"energy": 5.0, "danceability": 5.0}) - response_format: 'markdown' or 'json' Returns: return_tracks: List with similarity scores (Markdown or JSON: {"strategy": "...", "scope": "...", "count": N, "tracks": [{track, similarity}]}) create_playlist: {"success": true, "action": "create_playlist", "playlist_id": "...", "playlist_name": "...", "playlist_url": "...", "tracks_added": N, "message": "..."} add_to_playlist: {"success": true, "action": "add_to_playlist", "playlist_id": "...", "tracks_added": N, "message": "..."} Examples: - "Find songs similar to this track" -> track_id, catalog scope - "Create workout playlist like this" -> track_id, energy_match, create_playlist - "Filter playlist by genre" -> track_id, genre_match, playlist scope - "Custom similarity for energy/dance" -> weighted strategy, custom weights Errors: Returns errors for missing source, missing scope_id, missing action params, genre_match with catalog, no genres, no matches, auth (401), permissions (403), rate limits (429).

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
paramsYes

Implementation Reference

  • server.py:658-667 (registration)
    Registration of the tool 'spotify_find_similar_tracks' using the @mcp.tool decorator.
    @mcp.tool( name="spotify_find_similar_tracks", annotations={ "title": "Find Similar Tracks Using Audio Features", "readOnlyHint": False, # Can create playlists "destructiveHint": False, "idempotentHint": False, "openWorldHint": True, }, )
  • The core handler function that orchestrates similarity search: validates inputs, fetches source features or genres, retrieves candidate tracks from specified scope, computes similarity scores using selected strategy, filters/sorts results, and executes actions (return, create playlist, or add to playlist). Delegates to helper functions for computations.
    async def spotify_find_similar_tracks(params: FindSimilarTracksInput) -> str: """Find tracks similar to a track, artist, or playlist using audio analysis or genre matching. Centerpiece of the similarity engine. Supports 8 strategies, custom weighting, and automated playlist creation. For curated playlists, music discovery, and mood-based mixes. Args: Source (one required): track_id, artist_id, or playlist_id Strategy (default: euclidean): euclidean, weighted (needs weights), cosine, manhattan, energy_match (workout), mood_match (relaxation), rhythm_match (running), genre_match (non-catalog scope only) Scope (default: catalog): catalog (recommendations API), playlist (needs scope_id), artist (needs scope_id), album (needs scope_id), saved_tracks Action (default: return_tracks): return_tracks, create_playlist (needs playlist_name), add_to_playlist (needs target_playlist_id) - limit: Results to return, 1-100 (default: 20) - min_similarity: Optional threshold, 0.0-1.0 - weights: Optional custom weights for 'weighted' strategy (e.g., {"energy": 5.0, "danceability": 5.0}) - response_format: 'markdown' or 'json' Returns: return_tracks: List with similarity scores (Markdown or JSON: {"strategy": "...", "scope": "...", "count": N, "tracks": [{track, similarity}]}) create_playlist: {"success": true, "action": "create_playlist", "playlist_id": "...", "playlist_name": "...", "playlist_url": "...", "tracks_added": N, "message": "..."} add_to_playlist: {"success": true, "action": "add_to_playlist", "playlist_id": "...", "tracks_added": N, "message": "..."} Examples: - "Find songs similar to this track" -> track_id, catalog scope - "Create workout playlist like this" -> track_id, energy_match, create_playlist - "Filter playlist by genre" -> track_id, genre_match, playlist scope - "Custom similarity for energy/dance" -> weighted strategy, custom weights Errors: Returns errors for missing source, missing scope_id, missing action params, genre_match with catalog, no genres, no matches, auth (401), permissions (403), rate limits (429). """ try: # Validate source if not params.track_id and not params.artist_id and not params.playlist_id: return "Error: At least one of track_id, artist_id, or playlist_id must be provided." # Validate scope_id for non-catalog scopes if params.scope != SearchScope.CATALOG and not params.scope_id: return f"Error: scope_id is required for scope '{params.scope.value}'" # Validate action parameters if ( params.action == SimilarityAction.CREATE_PLAYLIST and not params.playlist_name ): return "Error: playlist_name is required for 'create_playlist' action" if ( params.action == SimilarityAction.ADD_TO_PLAYLIST and not params.target_playlist_id ): return "Error: target_playlist_id is required for 'add_to_playlist' action" similar_tracks = [] # Handle GENRE_MATCH strategy separately (uses artist genres instead of audio features) if params.strategy == SimilarityStrategy.GENRE_MATCH: # Genre matching requires a specific scope (not catalog) if params.scope == SearchScope.CATALOG: return "Error: GENRE_MATCH strategy requires a specific scope (playlist, artist, album, or saved_tracks), not catalog" # Get source genres source_genres = await get_source_genres( params.track_id, params.artist_id, params.playlist_id ) if not source_genres: return "Error: No genres found for the source track/artist/playlist" # Get candidate tracks from specified scope candidate_tracks = await get_candidate_tracks( params.scope, params.scope_id, limit=500 ) # Calculate genre similarity for each track for track in candidate_tracks: # Get genres for this track target_genres = await get_track_genres(track) if target_genres: similarity = calculate_genre_similarity( source_genres, target_genres ) # Apply minimum similarity filter if ( params.min_similarity is None or similarity >= params.min_similarity ): similar_tracks.append( { "track": track, "similarity": similarity, "genres": target_genres, } ) else: # Audio feature-based matching # Get source audio features source_features = await get_source_features( params.track_id, params.artist_id, params.playlist_id ) if params.scope == SearchScope.CATALOG: # Use recommendations API for catalog search seed_params = {} if params.track_id: seed_params["seed_tracks"] = params.track_id elif params.artist_id: seed_params["seed_artists"] = params.artist_id # Use source features as targets for recommendations seed_params["limit"] = params.limit seed_params["target_acousticness"] = source_features.get("acousticness") seed_params["target_danceability"] = source_features.get("danceability") seed_params["target_energy"] = source_features.get("energy") seed_params["target_instrumentalness"] = source_features.get( "instrumentalness" ) seed_params["target_valence"] = source_features.get("valence") seed_params["target_tempo"] = source_features.get("tempo") data = await make_spotify_request("recommendations", params=seed_params) candidate_tracks = data.get("tracks", []) else: # Get candidate tracks from specified scope candidate_tracks = await get_candidate_tracks( params.scope, params.scope_id, limit=500 ) # Get audio features for candidate tracks candidate_ids = [track["id"] for track in candidate_tracks] features_map = await get_audio_features_for_tracks(candidate_ids) # Calculate similarity scores for track in candidate_tracks: track_id = track["id"] if track_id in features_map: target_features = features_map[track_id] similarity = calculate_similarity( source_features, target_features, params.strategy, params.weights, ) # Apply minimum similarity filter if ( params.min_similarity is None or similarity >= params.min_similarity ): similar_tracks.append( { "track": track, "similarity": similarity, "features": target_features, } ) # Sort by similarity (descending for similarity scores) similar_tracks.sort(key=lambda x: x["similarity"], reverse=True) # Limit results similar_tracks = similar_tracks[: params.limit] if not similar_tracks: return "No similar tracks found matching the criteria." # Execute action if params.action == SimilarityAction.CREATE_PLAYLIST: # Create new playlist with similar tracks playlist_data = await create_playlist_helper( name=params.playlist_name, description=f"Similar tracks found using {params.strategy.value} strategy", public=False, ) # Add tracks to playlist track_uris = [item["track"]["uri"] for item in similar_tracks] await add_tracks_to_playlist_helper( playlist_id=playlist_data["id"], track_uris=track_uris, ) return json.dumps( { "success": True, "action": "create_playlist", "playlist_id": playlist_data["id"], "playlist_name": playlist_data["name"], "playlist_url": playlist_data["external_urls"]["spotify"], "tracks_added": len(track_uris), "message": f"Created playlist '{params.playlist_name}' with {len(track_uris)} similar tracks", }, indent=2, ) elif params.action == SimilarityAction.ADD_TO_PLAYLIST: # Add tracks to existing playlist track_uris = [item["track"]["uri"] for item in similar_tracks] await add_tracks_to_playlist_helper( playlist_id=params.target_playlist_id, track_uris=track_uris, ) return json.dumps( { "success": True, "action": "add_to_playlist", "playlist_id": params.target_playlist_id, "tracks_added": len(track_uris), "message": f"Added {len(track_uris)} similar tracks to playlist", }, indent=2, ) else: # Return tracks if params.response_format == ResponseFormat.MARKDOWN: lines = ["# Similar Tracks\n"] lines.append(f"Strategy: {params.strategy.value}") lines.append(f"Scope: {params.scope.value}") lines.append(f"Found {len(similar_tracks)} similar tracks\n") for i, item in enumerate(similar_tracks, 1): track = item["track"] similarity = item["similarity"] lines.append(f"## {i}. {format_track_markdown(track)}") lines.append(f"- **Similarity Score**: {similarity:.3f}\n") return "\n".join(lines) else: # JSON format return json.dumps( { "strategy": params.strategy.value, "scope": params.scope.value, "count": len(similar_tracks), "tracks": [ { "track": item["track"], "similarity": item["similarity"], } for item in similar_tracks ], }, indent=2, ) except Exception as e: return handle_spotify_error(e)
  • Pydantic model defining the input schema for the spotify_find_similar_tracks tool, including source IDs, similarity strategy, search scope, limits, thresholds, actions for playlists, and output format with validation.
    class FindSimilarTracksInput(BaseModel): """Input model for finding similar tracks.""" model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True) # Source track/entity track_id: str | None = Field( default=None, description="Spotify track ID to find similar tracks for", ) artist_id: str | None = Field( default=None, description="Spotify artist ID to analyze their style", ) playlist_id: str | None = Field( default=None, description="Playlist ID to analyze (uses average features of first 20 tracks)", ) # Similarity configuration strategy: SimilarityStrategy = Field( default=SimilarityStrategy.EUCLIDEAN, description=( "Similarity algorithm: 'euclidean' (overall similarity), " "'weighted' (custom feature weights), 'cosine' (angular similarity), " "'manhattan' (city-block distance), 'energy_match' (energy/danceability focus), " "'mood_match' (valence/acousticness focus), 'rhythm_match' (tempo/time_signature focus), " "'genre_match' (artist genre matching)" ), ) weights: FeatureWeights | None = Field( default=None, description="Feature weights (only used with 'weighted' strategy)", ) # Search scope scope: SearchScope = Field( default=SearchScope.CATALOG, description=( "Search scope: 'catalog' (recommendations API), 'playlist' (within playlist), " "'artist' (artist's tracks), 'album' (within album), 'saved_tracks' (user's library)" ), ) scope_id: str | None = Field( default=None, description="ID for scope (playlist_id, artist_id, or album_id) - required for non-catalog scopes", ) # Result configuration limit: int = Field( default=20, description="Number of similar tracks to return", ge=1, le=100, ) min_similarity: float | None = Field( default=None, description="Minimum similarity score (0.0-1.0, lower = more similar for distance metrics)", ge=0.0, le=1.0, ) # Action configuration action: SimilarityAction = Field( default=SimilarityAction.RETURN_TRACKS, description=( "Action: 'return_tracks' (just return list), 'create_playlist' (create new playlist), " "'add_to_playlist' (add to existing playlist)" ), ) playlist_name: str | None = Field( default=None, description="Playlist name (required for 'create_playlist' action)", ) target_playlist_id: str | None = Field( default=None, description="Target playlist ID (required for 'add_to_playlist' action)", ) response_format: ResponseFormat = Field( default=ResponseFormat.MARKDOWN, description="Output format: 'markdown' or 'json'", ) @field_validator("track_id", "artist_id", "playlist_id") @classmethod def validate_source(cls, v: str | None, info) -> str | None: """Validate at least one source is provided.""" return v

Latest Blog Posts

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/kylestratis/spotify-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server