Skip to main content
Glama

Spotify MCP Server

by lauechlo
music_server_updated_2025.py54.9 kB
#!/usr/bin/env python3 """ Music MCP Server - Updated for 2025 Spotify API Changes AI @Princeton Workshop - October 31, 2025 This MCP server connects Claude to Spotify, allowing natural language music queries, playlist analysis, and song recommendations. WHAT CHANGED IN 2025: - audio-features endpoint now requires track IDs instead of URIs - Updated authentication flow - Improved error handling for batch requests """ import os import json import logging from typing import Any, Sequence from datetime import datetime from dotenv import load_dotenv import spotipy from spotipy.oauth2 import SpotifyOAuth # Load environment variables from .env file load_dotenv() from mcp.server import Server from mcp.types import ( Resource, Tool, TextContent, ImageContent, EmbeddedResource, LoggingLevel ) from pydantic import AnyUrl import mcp.server.stdio # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger("music-server") # Initialize Spotify client def get_spotify_client(): """Initialize and return authenticated Spotify client.""" scope = "user-library-read user-top-read playlist-read-private playlist-modify-public playlist-modify-private" return spotipy.Spotify(auth_manager=SpotifyOAuth( client_id=os.environ["SPOTIFY_CLIENT_ID"], client_secret=os.environ["SPOTIFY_CLIENT_SECRET"], redirect_uri="http://127.0.0.1:8888/callback", scope=scope )) # Initialize MCP server app = Server("music-server") sp = get_spotify_client() @app.list_resources() async def list_resources() -> list[Resource]: """List available music-related resources.""" return [ Resource( uri=AnyUrl("music://user/profile"), name="User Profile", mimeType="application/json", description="Current user's Spotify profile information" ), Resource( uri=AnyUrl("music://user/top-tracks"), name="Top Tracks", mimeType="application/json", description="User's most played tracks" ), Resource( uri=AnyUrl("music://user/top-artists"), name="Top Artists", mimeType="application/json", description="User's most listened to artists" ) ] @app.read_resource() async def read_resource(uri: AnyUrl) -> str: """Read and return music resource data.""" uri_str = str(uri) if uri_str == "music://user/profile": profile = sp.current_user() return json.dumps(profile, indent=2) elif uri_str == "music://user/top-tracks": tracks = sp.current_user_top_tracks(limit=20, time_range="medium_term") return json.dumps(tracks, indent=2) elif uri_str == "music://user/top-artists": artists = sp.current_user_top_artists(limit=20, time_range="medium_term") return json.dumps(artists, indent=2) else: raise ValueError(f"Unknown resource: {uri}") @app.list_tools() async def list_tools() -> list[Tool]: """List available music analysis tools.""" return [ Tool( name="search_tracks", description="Search for tracks on Spotify by name, artist, or keywords", inputSchema={ "type": "object", "properties": { "query": { "type": "string", "description": "Search query (song name, artist, keywords)" }, "limit": { "type": "number", "description": "Number of results to return (1-50)", "default": 10 } }, "required": ["query"] } ), Tool( name="get_recommendations", description="Get song recommendations based on seed tracks, artists, or genres", inputSchema={ "type": "object", "properties": { "seed_tracks": { "type": "array", "items": {"type": "string"}, "description": "Up to 5 track IDs" }, "seed_artists": { "type": "array", "items": {"type": "string"}, "description": "Up to 5 artist IDs" }, "seed_genres": { "type": "array", "items": {"type": "string"}, "description": "Up to 5 genre seeds" }, "limit": { "type": "number", "description": "Number of recommendations (1-100)", "default": 20 } } } ), Tool( name="analyze_playlist", description="Analyze a Spotify playlist to get insights about its musical characteristics", inputSchema={ "type": "object", "properties": { "playlist_id": { "type": "string", "description": "Spotify playlist ID" } }, "required": ["playlist_id"] } ), Tool( name="get_artist_info", description="Get detailed information about an artist including genres, popularity, and top tracks", inputSchema={ "type": "object", "properties": { "artist_id": { "type": "string", "description": "Spotify artist ID" } }, "required": ["artist_id"] } ), Tool( name="analyze_explicitness", description="Analyze explicit content in a collection of songs - find out how many songs have explicit lyrics", inputSchema={ "type": "object", "properties": { "songs": { "type": "array", "items": { "type": "object", "properties": { "song_name": {"type": "string"}, "artist_name": {"type": "string"} }, "required": ["song_name"] }, "description": "List of songs to check for explicit content" } }, "required": ["songs"] } ), Tool( name="analyze_collection_diversity", description="Analyze diversity in a music collection - unique artists, genre spread, era distribution, popularity range", inputSchema={ "type": "object", "properties": { "songs": { "type": "array", "items": { "type": "object", "properties": { "song_name": {"type": "string"}, "artist_name": {"type": "string"} }, "required": ["song_name"] }, "description": "List of songs to analyze for diversity" } }, "required": ["songs"] } ), Tool( name="get_top_artists_from_collection", description="Find the most frequent artists in a song collection and their contribution percentage", inputSchema={ "type": "object", "properties": { "songs": { "type": "array", "items": { "type": "object", "properties": { "song_name": {"type": "string"}, "artist_name": {"type": "string"} }, "required": ["song_name"] }, "description": "List of songs to analyze" }, "top_n": { "type": "number", "description": "Number of top artists to return", "default": 10 } }, "required": ["songs"] } ), Tool( name="analyze_genres_in_collection", description="Analyze genre distribution in a music collection - find dominant genres, genre diversity, and trends", inputSchema={ "type": "object", "properties": { "songs": { "type": "array", "items": { "type": "object", "properties": { "song_name": {"type": "string"}, "artist_name": {"type": "string"} }, "required": ["song_name"] }, "description": "List of songs to analyze for genres" } }, "required": ["songs"] } ), Tool( name="create_playlist", description="Create a new Spotify playlist from a collection of songs", inputSchema={ "type": "object", "properties": { "playlist_name": { "type": "string", "description": "Name for the new playlist" }, "songs": { "type": "array", "items": { "type": "object", "properties": { "song_name": {"type": "string"}, "artist_name": {"type": "string"} }, "required": ["song_name"] }, "description": "List of songs to add to the playlist" }, "description": { "type": "string", "description": "Optional description for the playlist" }, "public": { "type": "boolean", "description": "Whether the playlist should be public (default: false)", "default": False } }, "required": ["playlist_name", "songs"] } ), Tool( name="generate_balanced_playlist", description="Create a balanced playlist from a song collection based on genre, artist, and era distribution", inputSchema={ "type": "object", "properties": { "songs": { "type": "array", "items": { "type": "object", "properties": { "song_name": {"type": "string"}, "artist_name": {"type": "string"} }, "required": ["song_name"] }, "description": "Source collection of songs to balance from" }, "target_size": { "type": "number", "description": "Target number of songs for the balanced playlist (default: 30)", "default": 30 }, "balance_criteria": { "type": "string", "description": "What to balance by: 'genre', 'artist', or 'era' (default: 'genre')", "default": "genre" }, "playlist_name": { "type": "string", "description": "Name for the new balanced playlist (optional - if provided, creates the playlist)" } }, "required": ["songs"] } ), Tool( name="compare_to_my_taste", description="Compare a song collection to your actual Spotify listening history - find overlaps, differences, and get personalized insights", inputSchema={ "type": "object", "properties": { "songs": { "type": "array", "items": { "type": "object", "properties": { "song_name": {"type": "string"}, "artist_name": {"type": "string"} }, "required": ["song_name"] }, "description": "List of songs to compare to your taste" } }, "required": ["songs"] } ), Tool( name="find_whats_missing", description="Check which songs from a collection are NOT in your Spotify library - find songs you haven't saved yet", inputSchema={ "type": "object", "properties": { "songs": { "type": "array", "items": { "type": "object", "properties": { "song_name": {"type": "string"}, "artist_name": {"type": "string"} }, "required": ["song_name"] }, "description": "List of songs to check against your library" } }, "required": ["songs"] } ), ] @app.call_tool() async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]: """Execute music analysis tools.""" try: if name == "search_tracks": query = arguments["query"] limit = arguments.get("limit", 10) results = sp.search(q=query, type="track", limit=limit) tracks = results["tracks"]["items"] formatted_results = [] for track in tracks: formatted_results.append({ "name": track["name"], "artists": [artist["name"] for artist in track["artists"]], "album": track["album"]["name"], "id": track["id"], "uri": track["uri"], "popularity": track["popularity"], "preview_url": track.get("preview_url"), "external_url": track["external_urls"]["spotify"] }) return [TextContent( type="text", text=json.dumps(formatted_results, indent=2) )] elif name == "get_recommendations": seed_tracks = arguments.get("seed_tracks", []) seed_artists = arguments.get("seed_artists", []) seed_genres = arguments.get("seed_genres", []) limit = arguments.get("limit", 20) recommendations = sp.recommendations( seed_tracks=seed_tracks[:5], seed_artists=seed_artists[:5], seed_genres=seed_genres[:5], limit=limit ) formatted_recs = [] for track in recommendations["tracks"]: formatted_recs.append({ "name": track["name"], "artists": [artist["name"] for artist in track["artists"]], "album": track["album"]["name"], "id": track["id"], "uri": track["uri"], "popularity": track["popularity"], "external_url": track["external_urls"]["spotify"] }) return [TextContent( type="text", text=json.dumps(formatted_recs, indent=2) )] elif name == "analyze_playlist": playlist_id = arguments["playlist_id"] # Get playlist details playlist = sp.playlist(playlist_id) tracks = playlist["tracks"]["items"] # Collect track info track_info = [] total_popularity = 0 explicit_count = 0 for item in tracks: if item["track"]: track = item["track"] track_info.append({ "name": track["name"], "artists": [a["name"] for a in track["artists"]], "popularity": track["popularity"], "explicit": track["explicit"] }) total_popularity += track["popularity"] if track["explicit"]: explicit_count += 1 analysis = { "name": playlist["name"], "description": playlist["description"], "owner": playlist["owner"]["display_name"], "total_tracks": playlist["tracks"]["total"], "followers": playlist["followers"]["total"], "stats": { "average_popularity": round(total_popularity / len(track_info), 1) if track_info else 0, "explicit_songs": explicit_count, "explicit_percentage": round((explicit_count / len(track_info) * 100), 1) if track_info else 0 }, "external_url": playlist["external_urls"]["spotify"] } return [TextContent( type="text", text=json.dumps(analysis, indent=2) )] elif name == "get_artist_info": artist_id = arguments["artist_id"] # Get artist details artist = sp.artist(artist_id) # Get top tracks top_tracks = sp.artist_top_tracks(artist_id) info = { "name": artist["name"], "genres": artist["genres"], "popularity": artist["popularity"], "followers": artist["followers"]["total"], "top_tracks": [ { "name": track["name"], "album": track["album"]["name"], "popularity": track["popularity"], "id": track["id"] } for track in top_tracks["tracks"][:10] ], "external_url": artist["external_urls"]["spotify"] } return [TextContent( type="text", text=json.dumps(info, indent=2) )] elif name == "analyze_explicitness": songs = arguments["songs"] explicit_songs = [] clean_songs = [] errors = [] for song_data in songs: song_name = song_data["song_name"] artist_name = song_data.get("artist_name", "") # Build search query query = song_name if artist_name: query += f" artist:{artist_name}" # Search for the song search_results = sp.search(q=query, type="track", limit=1) tracks = search_results["tracks"]["items"] if not tracks: errors.append(f"Not found: {query}") continue track = tracks[0] song_info = { "name": track["name"], "artists": [a["name"] for a in track["artists"]], "explicit": track["explicit"], "popularity": track["popularity"] } if track["explicit"]: explicit_songs.append(song_info) else: clean_songs.append(song_info) total_songs = len(explicit_songs) + len(clean_songs) explicit_percentage = (len(explicit_songs) / total_songs * 100) if total_songs > 0 else 0 result = { "summary": { "total_songs_analyzed": total_songs, "explicit_songs_count": len(explicit_songs), "clean_songs_count": len(clean_songs), "explicit_percentage": round(explicit_percentage, 1), "rating": "Family-Friendly" if explicit_percentage == 0 else "Mostly Clean" if explicit_percentage < 25 else "Mixed Content" if explicit_percentage < 50 else "Mostly Explicit" if explicit_percentage < 75 else "Explicit", "errors": errors if errors else None }, "explicit_songs": explicit_songs, "clean_songs": clean_songs } return [TextContent( type="text", text=json.dumps(result, indent=2) )] elif name == "analyze_collection_diversity": songs = arguments["songs"] all_artists = [] all_genres = set() popularities = [] release_years = [] track_info = [] errors = [] for song_data in songs: song_name = song_data["song_name"] artist_name = song_data.get("artist_name", "") # Build search query query = song_name if artist_name: query += f" artist:{artist_name}" # Search for the song search_results = sp.search(q=query, type="track", limit=1) tracks = search_results["tracks"]["items"] if not tracks: errors.append(f"Not found: {query}") continue track = tracks[0] # Collect artist names for artist in track["artists"]: all_artists.append(artist["name"]) # Get artist genres try: artist_info = sp.artist(artist["id"]) all_genres.update(artist_info["genres"]) except: pass # Collect popularity popularities.append(track["popularity"]) # Get release year release_date = track["album"]["release_date"] year = None if release_date: year = int(release_date.split("-")[0]) release_years.append(year) track_info.append({ "name": track["name"], "artists": [a["name"] for a in track["artists"]], "popularity": track["popularity"], "release_year": year }) # Calculate diversity metrics unique_artists = len(set(all_artists)) total_artists = len(all_artists) artist_diversity = unique_artists / total_artists if total_artists > 0 else 0 unique_genres = len(all_genres) popularity_range = max(popularities) - min(popularities) if popularities else 0 avg_popularity = sum(popularities) / len(popularities) if popularities else 0 year_range = max(release_years) - min(release_years) if release_years else 0 # Determine diversity level if artist_diversity > 0.8 and unique_genres > 10: diversity_level = "Very Diverse" elif artist_diversity > 0.6 and unique_genres > 5: diversity_level = "Diverse" elif artist_diversity > 0.4: diversity_level = "Moderately Diverse" else: diversity_level = "Low Diversity" result = { "summary": { "diversity_level": diversity_level, "total_songs": len(track_info), "errors": errors if errors else None }, "artist_diversity": { "unique_artists": unique_artists, "total_artist_appearances": total_artists, "diversity_score": round(artist_diversity, 3), "interpretation": "High" if artist_diversity > 0.7 else "Medium" if artist_diversity > 0.4 else "Low" }, "genre_diversity": { "unique_genres": unique_genres, "genres": sorted(list(all_genres)), "interpretation": "Very Diverse" if unique_genres > 10 else "Diverse" if unique_genres > 5 else "Limited" if unique_genres > 2 else "Very Limited" }, "popularity_distribution": { "average_popularity": round(avg_popularity, 1), "range": popularity_range, "interpretation": "Mainstream" if avg_popularity > 70 else "Popular" if avg_popularity > 50 else "Mixed" if avg_popularity > 30 else "Underground/Niche" }, "era_distribution": { "year_range": year_range, "earliest": min(release_years) if release_years else None, "latest": max(release_years) if release_years else None, "interpretation": "Multi-era" if year_range > 20 else "Modern" if (min(release_years) > 2010 if release_years else False) else "Recent-focused" }, "tracks": track_info } return [TextContent( type="text", text=json.dumps(result, indent=2) )] elif name == "get_top_artists_from_collection": songs = arguments["songs"] top_n = arguments.get("top_n", 10) artist_count = {} artist_songs = {} errors = [] for song_data in songs: song_name = song_data["song_name"] artist_name = song_data.get("artist_name", "") # Build search query query = song_name if artist_name: query += f" artist:{artist_name}" # Search for the song search_results = sp.search(q=query, type="track", limit=1) tracks = search_results["tracks"]["items"] if not tracks: errors.append(f"Not found: {query}") continue track = tracks[0] # Count each artist for artist in track["artists"]: artist_name = artist["name"] artist_count[artist_name] = artist_count.get(artist_name, 0) + 1 if artist_name not in artist_songs: artist_songs[artist_name] = [] artist_songs[artist_name].append(track["name"]) # Sort by frequency sorted_artists = sorted( artist_count.items(), key=lambda x: x[1], reverse=True )[:top_n] total_songs = sum(artist_count.values()) top_artists = [] for artist_name, count in sorted_artists: percentage = (count / total_songs * 100) if total_songs > 0 else 0 top_artists.append({ "artist": artist_name, "song_count": count, "percentage": round(percentage, 1), "songs": artist_songs[artist_name] }) result = { "summary": { "total_songs_analyzed": len(songs) - len(errors), "unique_artists": len(artist_count), "top_artist": sorted_artists[0][0] if sorted_artists else None, "errors": errors if errors else None }, "top_artists": top_artists, "distribution_type": "Focused" if top_artists and top_artists[0]["percentage"] > 40 else "Balanced" if len(set(artist_count.values())) > len(artist_count) * 0.5 else "Varied" } return [TextContent( type="text", text=json.dumps(result, indent=2) )] elif name == "analyze_genres_in_collection": songs = arguments["songs"] genre_count = {} artist_genres_map = {} track_info = [] errors = [] for song_data in songs: song_name = song_data["song_name"] artist_name = song_data.get("artist_name", "") # Build search query query = song_name if artist_name: query += f" artist:{artist_name}" # Search for the song search_results = sp.search(q=query, type="track", limit=1) tracks = search_results["tracks"]["items"] if not tracks: errors.append(f"Not found: {query}") continue track = tracks[0] track_genres = [] # Get genres from all artists for artist in track["artists"]: artist_name_key = artist["name"] # Cache artist info to avoid duplicate API calls if artist_name_key not in artist_genres_map: try: artist_info = sp.artist(artist["id"]) artist_genres_map[artist_name_key] = artist_info["genres"] except: artist_genres_map[artist_name_key] = [] artist_genres = artist_genres_map[artist_name_key] track_genres.extend(artist_genres) # Count genres for genre in artist_genres: genre_count[genre] = genre_count.get(genre, 0) + 1 track_info.append({ "name": track["name"], "artists": [a["name"] for a in track["artists"]], "genres": list(set(track_genres)) if track_genres else ["Unknown"] }) # Sort genres by frequency sorted_genres = sorted( genre_count.items(), key=lambda x: x[1], reverse=True ) # Calculate genre diversity total_genre_tags = sum(genre_count.values()) unique_genres = len(genre_count) # Top genres with percentages top_genres = [] for genre, count in sorted_genres[:15]: percentage = (count / total_genre_tags * 100) if total_genre_tags > 0 else 0 top_genres.append({ "genre": genre, "count": count, "percentage": round(percentage, 1) }) # Determine dominant style if sorted_genres: top_genre = sorted_genres[0][0] if "pop" in top_genre: dominant_style = "Pop-oriented" elif "rock" in top_genre: dominant_style = "Rock-focused" elif "hip hop" in top_genre or "rap" in top_genre: dominant_style = "Hip-Hop/Rap" elif "electronic" in top_genre or "edm" in top_genre: dominant_style = "Electronic" elif "indie" in top_genre or "alternative" in top_genre: dominant_style = "Indie/Alternative" elif "r&b" in top_genre or "soul" in top_genre: dominant_style = "R&B/Soul" elif "country" in top_genre: dominant_style = "Country" elif "jazz" in top_genre: dominant_style = "Jazz" elif "classical" in top_genre: dominant_style = "Classical" else: dominant_style = top_genre.title() else: dominant_style = "Unknown" result = { "summary": { "total_songs_analyzed": len(track_info), "unique_genres": unique_genres, "dominant_style": dominant_style, "genre_diversity": "Very Diverse" if unique_genres > 20 else "Diverse" if unique_genres > 10 else "Moderately Diverse" if unique_genres > 5 else "Limited", "errors": errors if errors else None }, "top_genres": top_genres, "genre_distribution": { "total_genre_tags": total_genre_tags, "average_genres_per_song": round(total_genre_tags / len(track_info), 1) if track_info else 0 }, "tracks_with_genres": track_info } return [TextContent( type="text", text=json.dumps(result, indent=2) )] elif name == "create_playlist": playlist_name = arguments["playlist_name"] songs = arguments["songs"] description = arguments.get("description", "") public = arguments.get("public", False) # Get current user ID user = sp.current_user() user_id = user["id"] # Create the playlist playlist = sp.user_playlist_create( user=user_id, name=playlist_name, public=public, description=description ) # Search and collect track URIs track_uris = [] found_songs = [] not_found = [] for song_data in songs: song_name = song_data["song_name"] artist_name = song_data.get("artist_name", "") # Build search query query = song_name if artist_name: query += f" artist:{artist_name}" # Search for the song search_results = sp.search(q=query, type="track", limit=1) tracks = search_results["tracks"]["items"] if tracks: track = tracks[0] track_uris.append(track["uri"]) found_songs.append({ "name": track["name"], "artists": [a["name"] for a in track["artists"]] }) else: not_found.append(query) # Add tracks to playlist in batches of 100 (Spotify limit) for i in range(0, len(track_uris), 100): batch = track_uris[i:i+100] sp.playlist_add_items(playlist["id"], batch) result = { "success": True, "playlist": { "id": playlist["id"], "name": playlist["name"], "url": playlist["external_urls"]["spotify"], "public": public }, "summary": { "total_requested": len(songs), "songs_added": len(track_uris), "not_found": len(not_found) }, "added_songs": found_songs, "not_found_queries": not_found if not_found else None } return [TextContent( type="text", text=json.dumps(result, indent=2) )] elif name == "generate_balanced_playlist": songs = arguments["songs"] target_size = arguments.get("target_size", 30) balance_criteria = arguments.get("balance_criteria", "genre") playlist_name = arguments.get("playlist_name") # First, analyze the collection to understand distribution track_data = [] errors = [] for song_data in songs: song_name = song_data["song_name"] artist_name = song_data.get("artist_name", "") query = song_name if artist_name: query += f" artist:{artist_name}" search_results = sp.search(q=query, type="track", limit=1) tracks = search_results["tracks"]["items"] if not tracks: errors.append(f"Not found: {query}") continue track = tracks[0] # Get artist info for genres genres = [] for artist in track["artists"]: try: artist_info = sp.artist(artist["id"]) genres.extend(artist_info["genres"]) except: pass # Get release year release_date = track["album"]["release_date"] year = int(release_date.split("-")[0]) if release_date else None track_data.append({ "track": track, "genres": list(set(genres)), "year": year, "artists": [a["name"] for a in track["artists"]] }) # Balance based on criteria selected_tracks = [] if balance_criteria == "genre": # Group by genre genre_groups = {} for item in track_data: for genre in item["genres"] if item["genres"] else ["Unknown"]: if genre not in genre_groups: genre_groups[genre] = [] genre_groups[genre].append(item) # Select evenly from each genre import random genres_list = list(genre_groups.keys()) random.shuffle(genres_list) idx = 0 while len(selected_tracks) < min(target_size, len(track_data)): genre = genres_list[idx % len(genres_list)] if genre_groups[genre]: item = genre_groups[genre].pop(0) if item not in selected_tracks: selected_tracks.append(item) idx += 1 elif balance_criteria == "artist": # Ensure diversity of artists artist_count = {} import random shuffled = track_data.copy() random.shuffle(shuffled) for item in shuffled: if len(selected_tracks) >= target_size: break artist_key = tuple(sorted(item["artists"])) if artist_count.get(artist_key, 0) < 2: # Max 2 per artist selected_tracks.append(item) artist_count[artist_key] = artist_count.get(artist_key, 0) + 1 elif balance_criteria == "era": # Balance by decade decade_groups = {} for item in track_data: if item["year"]: decade = (item["year"] // 10) * 10 if decade not in decade_groups: decade_groups[decade] = [] decade_groups[decade].append(item) import random decades_list = sorted(decade_groups.keys()) idx = 0 while len(selected_tracks) < min(target_size, len(track_data)): if not decades_list: break decade = decades_list[idx % len(decades_list)] if decade_groups[decade]: item = decade_groups[decade].pop(0) selected_tracks.append(item) idx += 1 # Prepare result balanced_songs = [] for item in selected_tracks: track = item["track"] balanced_songs.append({ "name": track["name"], "artists": [a["name"] for a in track["artists"]], "genres": item["genres"], "year": item["year"], "id": track["id"], "uri": track["uri"] }) result = { "summary": { "balance_criteria": balance_criteria, "source_songs": len(track_data), "selected_songs": len(selected_tracks), "target_size": target_size }, "balanced_selection": balanced_songs } # Create playlist if name provided if playlist_name: user = sp.current_user() playlist = sp.user_playlist_create( user=user["id"], name=playlist_name, public=False, description=f"Balanced by {balance_criteria}" ) track_uris = [item["track"]["uri"] for item in selected_tracks] for i in range(0, len(track_uris), 100): batch = track_uris[i:i+100] sp.playlist_add_items(playlist["id"], batch) result["playlist_created"] = { "id": playlist["id"], "name": playlist["name"], "url": playlist["external_urls"]["spotify"] } return [TextContent( type="text", text=json.dumps(result, indent=2) )] elif name == "compare_to_my_taste": songs = arguments["songs"] # Get user's top tracks and artists top_tracks = sp.current_user_top_tracks(limit=50, time_range="medium_term") top_artists = sp.current_user_top_artists(limit=50, time_range="medium_term") # Extract user's favorite artists and genres user_artists = set([artist["name"].lower() for artist in top_artists["items"]]) user_genres = set() for artist in top_artists["items"]: user_genres.update(artist["genres"]) # Extract user's top track names for matching user_tracks = set([track["name"].lower() for track in top_tracks["items"]]) # Analyze the input collection matching_tracks = [] matching_artists = [] non_matching_tracks = [] collection_artists = set() collection_genres = set() errors = [] for song_data in songs: song_name = song_data["song_name"] artist_name = song_data.get("artist_name", "") # Build search query query = song_name if artist_name: query += f" artist:{artist_name}" # Search for the song search_results = sp.search(q=query, type="track", limit=1) tracks = search_results["tracks"]["items"] if not tracks: errors.append(f"Not found: {query}") continue track = tracks[0] track_artists = [a["name"] for a in track["artists"]] # Check if track is in user's top tracks is_favorite_track = track["name"].lower() in user_tracks # Check if artist is in user's top artists is_favorite_artist = any(a.lower() in user_artists for a in track_artists) # Get genres for this track track_genres = [] for artist in track["artists"]: try: artist_info = sp.artist(artist["id"]) track_genres.extend(artist_info["genres"]) except: pass collection_genres.update(track_genres) for artist in track_artists: collection_artists.add(artist.lower()) song_info = { "name": track["name"], "artists": track_artists, "is_favorite_track": is_favorite_track, "is_favorite_artist": is_favorite_artist, "genres": list(set(track_genres)) } if is_favorite_track or is_favorite_artist: if is_favorite_track: matching_tracks.append(song_info) if is_favorite_artist: matching_artists.append(song_info) else: non_matching_tracks.append(song_info) # Calculate overlaps artist_overlap = len(collection_artists.intersection(user_artists)) genre_overlap = len(collection_genres.intersection(user_genres)) # Determine taste alignment total_analyzed = len(matching_tracks) + len(matching_artists) + len(non_matching_tracks) match_percentage = ((len(matching_tracks) + len(matching_artists)) / total_analyzed * 100) if total_analyzed > 0 else 0 if match_percentage > 50: alignment = "Strong Match - This collection aligns well with your taste!" elif match_percentage > 25: alignment = "Moderate Match - Some overlap with your preferences" else: alignment = "Low Match - This collection explores different territory" # Find missing genres from user's taste missing_genres = list(user_genres - collection_genres)[:5] new_genres = list(collection_genres - user_genres)[:5] result = { "summary": { "alignment": alignment, "match_percentage": round(match_percentage, 1), "total_analyzed": total_analyzed, "errors": errors if errors else None }, "matches": { "favorite_tracks_count": len(matching_tracks), "favorite_artists_count": len(matching_artists), "favorite_tracks": matching_tracks[:5], "favorite_artists": matching_artists[:5] }, "overlaps": { "artist_overlap": f"{artist_overlap} artists", "genre_overlap": f"{genre_overlap} genres" }, "insights": { "missing_from_your_taste": missing_genres, "new_genres_in_collection": new_genres, "songs_to_explore": [ {"name": t["name"], "artists": t["artists"]} for t in non_matching_tracks[:5] ] } } return [TextContent( type="text", text=json.dumps(result, indent=2) )] elif name == "find_whats_missing": songs = arguments["songs"] # Get ALL user's saved tracks (may require pagination) saved_tracks_set = set() offset = 0 limit = 50 while True: saved = sp.current_user_saved_tracks(limit=limit, offset=offset) if not saved["items"]: break for item in saved["items"]: track = item["track"] # Create a unique identifier for the track saved_tracks_set.add(track["id"]) offset += limit if len(saved["items"]) < limit: break # No more tracks # Check which songs from the collection are missing missing_songs = [] already_saved = [] errors = [] for song_data in songs: song_name = song_data["song_name"] artist_name = song_data.get("artist_name", "") # Build search query query = song_name if artist_name: query += f" artist:{artist_name}" # Search for the song search_results = sp.search(q=query, type="track", limit=1) tracks = search_results["tracks"]["items"] if not tracks: errors.append(f"Not found: {query}") continue track = tracks[0] song_info = { "name": track["name"], "artists": [a["name"] for a in track["artists"]], "id": track["id"], "uri": track["uri"], "url": track["external_urls"]["spotify"], "popularity": track["popularity"] } # Check if it's in user's saved tracks if track["id"] in saved_tracks_set: already_saved.append(song_info) else: missing_songs.append(song_info) result = { "summary": { "total_songs_checked": len(songs) - len(errors), "missing_from_library": len(missing_songs), "already_saved": len(already_saved), "missing_percentage": round((len(missing_songs) / (len(missing_songs) + len(already_saved)) * 100), 1) if (len(missing_songs) + len(already_saved)) > 0 else 0, "errors": errors if errors else None }, "missing_songs": missing_songs, "already_saved_songs": already_saved } return [TextContent( type="text", text=json.dumps(result, indent=2) )] else: raise ValueError(f"Unknown tool: {name}") except Exception as e: logger.error(f"Error executing tool {name}: {str(e)}") return [TextContent( type="text", text=f"Error: {str(e)}" )] async def main(): """Run the MCP server.""" async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await app.run( read_stream, write_stream, app.create_initialization_options() ) if __name__ == "__main__": import asyncio asyncio.run(main())

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/lauechlo/spotify-mpc-server'

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