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