fix: read track from item["item"] key to match updated Spotify API response shape
Spotify's playlist_items endpoint now nests the track object under "item"
instead of "track" in each playlist entry. The old key is absent, causing
item.get("track") to return None for every entry and silently drop all tracks.
Also adds stderr debug traces at each component boundary (tool dispatch,
API page summary, per-item accept/skip) to make future silent-empty issues
diagnosable, and switches popularity to .get() since it is absent in the
new response shape.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,10 @@ from mcp.types import Tool, TextContent
|
|||||||
|
|
||||||
import spotify_client
|
import spotify_client
|
||||||
|
|
||||||
|
|
||||||
|
def _dbg(msg: str) -> None:
|
||||||
|
print(f"[spotify-mcp] {msg}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
server = Server("spotify-mcp")
|
server = Server("spotify-mcp")
|
||||||
|
|
||||||
|
|
||||||
@@ -112,6 +116,7 @@ async def handle_list_tools() -> list[Tool]:
|
|||||||
@server.call_tool()
|
@server.call_tool()
|
||||||
async def handle_call_tool(name: str, arguments: dict | None) -> list[TextContent]:
|
async def handle_call_tool(name: str, arguments: dict | None) -> list[TextContent]:
|
||||||
arguments = arguments or {}
|
arguments = arguments or {}
|
||||||
|
_dbg(f"tool={name!r} args={arguments!r}")
|
||||||
try:
|
try:
|
||||||
if name == "list_playlists":
|
if name == "list_playlists":
|
||||||
result = await asyncio.to_thread(spotify_client.list_playlists)
|
result = await asyncio.to_thread(spotify_client.list_playlists)
|
||||||
@@ -145,8 +150,11 @@ async def handle_call_tool(name: str, arguments: dict | None) -> list[TextConten
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown tool: {name}")
|
raise ValueError(f"Unknown tool: {name}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
_dbg(f"tool={name!r} raised {type(e).__name__}: {e}")
|
||||||
return [TextContent(type="text", text=f"Error: {e}")]
|
return [TextContent(type="text", text=f"Error: {e}")]
|
||||||
|
|
||||||
|
result_len = len(result) if isinstance(result, list) else "n/a"
|
||||||
|
_dbg(f"tool={name!r} returning {result_len} items")
|
||||||
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import spotipy
|
import spotipy
|
||||||
from spotipy.oauth2 import SpotifyOAuth
|
from spotipy.oauth2 import SpotifyOAuth
|
||||||
from spotipy.cache_handler import CacheFileHandler
|
from spotipy.cache_handler import CacheFileHandler
|
||||||
|
|
||||||
|
|
||||||
|
def _dbg(msg: str) -> None:
|
||||||
|
print(f"[spotify-mcp] {msg}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
_SCOPES = " ".join([
|
_SCOPES = " ".join([
|
||||||
@@ -72,25 +77,41 @@ def list_playlists() -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
def get_playlist_tracks(playlist_id: str) -> list[dict]:
|
def get_playlist_tracks(playlist_id: str) -> list[dict]:
|
||||||
|
_dbg(f"get_playlist_tracks called with playlist_id={playlist_id!r}")
|
||||||
sp = get_client()
|
sp = get_client()
|
||||||
results = []
|
results = []
|
||||||
|
page = 0
|
||||||
response = sp.playlist_items(playlist_id, additional_types=["track"])
|
response = sp.playlist_items(playlist_id, additional_types=["track"])
|
||||||
while response:
|
while response:
|
||||||
for item in response["items"]:
|
items = response.get("items") or []
|
||||||
track = item.get("track")
|
total = response.get("total")
|
||||||
if not track or track.get("type") != "track":
|
_dbg(f" page {page}: total={total}, items_in_page={len(items)}, next={bool(response.get('next'))}")
|
||||||
|
for idx, item in enumerate(items):
|
||||||
|
# Spotify API returns the track under "item" key (newer API) or "track" key (older)
|
||||||
|
track = item.get("item") or item.get("track")
|
||||||
|
if track is None:
|
||||||
|
_dbg(f" item[{idx}]: track=None (local/null track), skipping")
|
||||||
continue
|
continue
|
||||||
|
track_type = track.get("type")
|
||||||
|
track_name = track.get("name", "<no-name>")
|
||||||
|
track_uri = track.get("uri", "<no-uri>")
|
||||||
|
if track_type != "track":
|
||||||
|
_dbg(f" item[{idx}]: SKIPPED type={track_type!r} name={track_name!r} uri={track_uri!r}")
|
||||||
|
continue
|
||||||
|
_dbg(f" item[{idx}]: OK type={track_type!r} name={track_name!r}")
|
||||||
results.append({
|
results.append({
|
||||||
"name": track["name"],
|
"name": track["name"],
|
||||||
"artists": [a["name"] for a in track["artists"]],
|
"artists": [a["name"] for a in track["artists"]],
|
||||||
"album": track["album"]["name"],
|
"album": track["album"]["name"],
|
||||||
"duration_ms": track["duration_ms"],
|
"duration_ms": track["duration_ms"],
|
||||||
"duration": _format_duration(track["duration_ms"]),
|
"duration": _format_duration(track["duration_ms"]),
|
||||||
"popularity": track["popularity"],
|
"popularity": track.get("popularity"),
|
||||||
"uri": track["uri"],
|
"uri": track["uri"],
|
||||||
"added_at": item["added_at"],
|
"added_at": item["added_at"],
|
||||||
})
|
})
|
||||||
|
page += 1
|
||||||
response = sp.next(response) if response["next"] else None
|
response = sp.next(response) if response["next"] else None
|
||||||
|
_dbg(f"get_playlist_tracks returning {len(results)} tracks")
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user