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

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

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
Behavior4/5

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

Annotations indicate readOnlyHint=false, openWorldHint=true, idempotentHint=false, destructiveHint=false. The description adds valuable behavioral context: it explains the tool's role as 'Centerpiece of the similarity engine,' details strategies and actions, lists error conditions (auth, permissions, rate limits), and describes return formats. This goes beyond annotations, though it doesn't fully explain idempotency or open-world implications.

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 description is well-structured with clear sections (purpose, args, returns, examples, errors) and uses bullet points for readability. It's appropriately detailed for a complex tool, though some sentences could be more concise (e.g., the first paragraph has minor redundancy). Overall, it earns its length with valuable 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?

Given the tool's complexity (multiple strategies, scopes, actions) and 0% schema description coverage, the description is highly complete: it explains purpose, parameters, return formats, examples, and error conditions. The output schema exists, so the description appropriately focuses on usage rather than repeating return structures. It provides all necessary context for effective tool invocation.

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?

With 0% schema description coverage, the description fully compensates by detailing all parameters: it lists source types, 8 strategies with explanations, 5 scopes with requirements, 3 actions with dependencies, and additional parameters like limit, min_similarity, weights, and response_format. It provides semantic meaning (e.g., 'energy_match (workout)') that the schema lacks.

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 clearly states the tool's purpose: 'Find tracks similar to a track, artist, or playlist using audio analysis or genre matching.' It specifies the verb ('find'), resources ('tracks'), and distinguishes from siblings by focusing on similarity matching rather than basic retrieval or playlist management.

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?

The description provides explicit usage guidance: 'For curated playlists, music discovery, and mood-based mixes.' It includes examples that show when to use specific configurations (e.g., 'Create workout playlist like this' for energy_match strategy) and mentions errors for misuse (e.g., 'genre_match with catalog'), helping the agent choose appropriate parameters.

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

Install Server

Other Tools

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