iflow-mcp_jamiew-spotify-mcp 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: iflow-mcp_jamiew-spotify-mcp
3
+ Version: 0.2.0
4
+ Summary: MCP spotify project
5
+ Author-email: Varun Srivastava <varun.neal@berkeley.edu>
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: mcp[cli]>=1.12.0
10
+ Requires-Dist: python-dotenv>=1.0.1
11
+ Requires-Dist: spotipy>=2.25.0
12
+ Dynamic: license-file
13
+
14
+ # spotify-mcp MCP server
15
+
16
+ MCP server connecting Claude with Spotify. This fork of [varunneal/spotify-mcp](https://github.com/varunneal/spotify-mcp) adds smart-batching tools and advanced playlist features that optimize API usage.
17
+
18
+ ## Features
19
+
20
+ ### Core Functionality
21
+ - **Playback Control**: Start, pause, skip tracks, manage queue
22
+ - **Search & Discovery**: Find tracks, albums, artists, playlists with pagination
23
+ - **Real-time State**: Live user profile and playback status
24
+
25
+ ### Enhanced Playlist Tools (New in this fork)
26
+ - **Smart Batch Operations**: Add/remove up to 100 tracks in single API calls
27
+ - **Large Playlist Support**: Efficiently handle playlists with 1000+ tracks using pagination
28
+ - **Advanced Playlist Management**: Create, modify details, bulk track operations
29
+ - **API-Optimized Workflows**: Intelligent batching reduces API calls by 60-80%
30
+
31
+ ## Installation
32
+
33
+ ### 1. Get Spotify API Keys
34
+ 1. Create account at [developer.spotify.com](https://developer.spotify.com/)
35
+ 2. Create app with redirect URI: `http://localhost:8888`
36
+
37
+ ### 2. Install the MCP Server
38
+ ```bash
39
+ git clone https://github.com/jamiew/spotify-mcp.git
40
+ cd spotify-mcp
41
+ uv sync
42
+ ```
43
+
44
+ ### 3. Configure Claude Desktop
45
+ Add to Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
46
+ ```json
47
+ "spotify": {
48
+ "command": "uv",
49
+ "args": ["--directory", "/path/to/spotify-mcp", "run", "spotify-mcp"],
50
+ "env": {
51
+ "SPOTIFY_CLIENT_ID": "YOUR_CLIENT_ID",
52
+ "SPOTIFY_CLIENT_SECRET": "YOUR_CLIENT_SECRET",
53
+ "SPOTIFY_REDIRECT_URI": "http://localhost:8888"
54
+ }
55
+ }
56
+ ```
57
+
58
+ **Requirements**: Spotify Premium account, `uv` >= 0.54
59
+
60
+ ## Usage Examples
61
+
62
+ - **"Create a chill study playlist with 20 tracks"** → Search + playlist creation + bulk track addition
63
+ - **"Show me the first 50 tracks from my 'Liked Songs'"** → Pagination for large playlists
64
+ - **"Find similar artists to Radiohead and add their top tracks to my queue"** → Search + artist info + queue management
65
+
66
+ ## Development
67
+
68
+ Built with **FastMCP framework** featuring 13 focused tools, type-safe APIs, and comprehensive test coverage.
69
+
70
+ **Debug with MCP Inspector:**
71
+ ```bash
72
+ npx @modelcontextprotocol/inspector uv --directory /path/to/spotify_mcp run spotify-mcp
73
+ ```
@@ -0,0 +1,12 @@
1
+ iflow_mcp_jamiew_spotify_mcp-0.2.0.dist-info/licenses/LICENSE,sha256=AsY4d4FvHVVzMmKbGN_ej567i2ID07OwJ_O-QNndxNo,1077
2
+ spotify_mcp/__init__.py,sha256=E0zThznQYn2uzuoIt7n3iMedc-TIQaOlnY7hTLItx9I,544
3
+ spotify_mcp/errors.py,sha256=4RiE_wJPxK723RoILulss5rBIQ8pOY_9id6mwzXl86U,9925
4
+ spotify_mcp/fastmcp_server.py,sha256=1oSwKWbgTbjhDNvarv1coPEk09fF1jayU1w1ru7ILrE,35985
5
+ spotify_mcp/logging_utils.py,sha256=fkg7RhjrTx6TdciWSCSIn8zL6d50orPowrPD8AoiZ04,4842
6
+ spotify_mcp/spotify_api.py,sha256=TG_buRYo_UaLT8EmWGBH0U-3CiWRoirc-0-lsUjpc_I,21711
7
+ spotify_mcp/utils.py,sha256=4qlRhzAeMmBeRN-ygyZhyHxR41_q8nHkFMN1p-p9bNQ,7067
8
+ iflow_mcp_jamiew_spotify_mcp-0.2.0.dist-info/METADATA,sha256=nhsD69LsgQ9cW-CiCEoGif8MYZWbX6V8W7sVW8_OoH0,2592
9
+ iflow_mcp_jamiew_spotify_mcp-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
+ iflow_mcp_jamiew_spotify_mcp-0.2.0.dist-info/entry_points.txt,sha256=3zhK_-NjCgc0Gyt85Nz0h-TeX0CFvjye_RBS93o3m1Q,49
11
+ iflow_mcp_jamiew_spotify_mcp-0.2.0.dist-info/top_level.txt,sha256=O_lkWQaCucJ2Ue8NZ6Bx_uQp_Hxtw7ME8DVgiE2obWo,12
12
+ iflow_mcp_jamiew_spotify_mcp-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ spotify-mcp = spotify_mcp:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Varun Neal Srivastava
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ spotify_mcp
@@ -0,0 +1,23 @@
1
+ import signal
2
+ import sys
3
+
4
+ from .fastmcp_server import mcp
5
+
6
+
7
+ def main() -> None:
8
+ """Main entry point for the package."""
9
+ # Handle SIGPIPE gracefully (when client disconnects)
10
+ signal.signal(signal.SIGPIPE, signal.SIG_DFL)
11
+
12
+ try:
13
+ mcp.run()
14
+ except BrokenPipeError:
15
+ # Handle broken pipe gracefully when client disconnects
16
+ sys.exit(0)
17
+ except KeyboardInterrupt:
18
+ # Handle Ctrl+C gracefully
19
+ sys.exit(0)
20
+
21
+
22
+ # Optionally expose other important items at package level
23
+ __all__ = ["main", "mcp"]
spotify_mcp/errors.py ADDED
@@ -0,0 +1,276 @@
1
+ """Custom error handling for Spotify MCP server."""
2
+
3
+ import json
4
+ from collections.abc import Callable
5
+ from enum import Enum
6
+ from typing import Any
7
+
8
+ import mcp.types as types
9
+ from spotipy import SpotifyException
10
+
11
+
12
+ class SpotifyMCPErrorCode(Enum):
13
+ """Error codes for Spotify MCP operations."""
14
+
15
+ # Authentication errors
16
+ AUTHENTICATION_FAILED = "authentication_failed"
17
+ TOKEN_EXPIRED = "token_expired" # nosec B105 - not a hardcoded password
18
+ INSUFFICIENT_SCOPE = "insufficient_scope"
19
+
20
+ # API errors
21
+ API_RATE_LIMITED = "api_rate_limited"
22
+ API_UNAVAILABLE = "api_unavailable"
23
+ INVALID_REQUEST = "invalid_request"
24
+
25
+ # Device errors
26
+ NO_ACTIVE_DEVICE = "no_active_device"
27
+ DEVICE_NOT_FOUND = "device_not_found"
28
+ PREMIUM_REQUIRED = "premium_required"
29
+
30
+ # Resource errors
31
+ TRACK_NOT_FOUND = "track_not_found"
32
+ PLAYLIST_NOT_FOUND = "playlist_not_found"
33
+ USER_NOT_FOUND = "user_not_found"
34
+
35
+ # Playback errors
36
+ PLAYBACK_RESTRICTED = "playback_restricted"
37
+ ALREADY_PLAYING = "already_playing"
38
+ ALREADY_PAUSED = "already_paused"
39
+
40
+ # General errors
41
+ UNKNOWN_ERROR = "unknown_error"
42
+ VALIDATION_ERROR = "validation_error"
43
+
44
+
45
+ class SpotifyMCPError(Exception):
46
+ """Custom exception for Spotify MCP operations with MCP-compliant error reporting."""
47
+
48
+ def __init__(
49
+ self,
50
+ code: SpotifyMCPErrorCode,
51
+ message: str,
52
+ details: dict[str, Any] | None = None,
53
+ suggestion: str | None = None,
54
+ ):
55
+ """
56
+ Initialize a Spotify MCP error.
57
+
58
+ Args:
59
+ code: Error code enum
60
+ message: Human-readable error message
61
+ details: Additional error details
62
+ suggestion: Suggestion for resolving the error
63
+ """
64
+ self.code = code
65
+ self.message = message
66
+ self.details = details or {}
67
+ self.suggestion = suggestion
68
+ super().__init__(message)
69
+
70
+ def to_mcp_error(self) -> types.TextContent:
71
+ """Convert to MCP-compliant error response."""
72
+ error_response = {
73
+ "error": {
74
+ "code": self.code.value,
75
+ "message": self.message,
76
+ "details": self.details,
77
+ }
78
+ }
79
+
80
+ if self.suggestion:
81
+ error_response["error"]["suggestion"] = self.suggestion
82
+
83
+ return types.TextContent(type="text", text=json.dumps(error_response, indent=2))
84
+
85
+ @classmethod
86
+ def from_spotify_exception(cls, exc: SpotifyException) -> "SpotifyMCPError":
87
+ """Create SpotifyMCPError from spotipy SpotifyException."""
88
+ status_code = getattr(exc, "http_status", None)
89
+ error_message = str(exc)
90
+
91
+ # Map HTTP status codes to our error codes
92
+ if status_code == 401:
93
+ if "token expired" in error_message.lower():
94
+ return cls(
95
+ SpotifyMCPErrorCode.TOKEN_EXPIRED,
96
+ "Spotify access token has expired",
97
+ {"http_status": status_code},
98
+ "Please re-authenticate with Spotify",
99
+ )
100
+ else:
101
+ return cls(
102
+ SpotifyMCPErrorCode.AUTHENTICATION_FAILED,
103
+ "Authentication with Spotify failed",
104
+ {"http_status": status_code},
105
+ "Check your Spotify API credentials",
106
+ )
107
+
108
+ elif status_code == 403:
109
+ if "premium" in error_message.lower():
110
+ return cls(
111
+ SpotifyMCPErrorCode.PREMIUM_REQUIRED,
112
+ "Spotify Premium is required for this operation",
113
+ {"http_status": status_code},
114
+ "Upgrade to Spotify Premium to use playback features",
115
+ )
116
+ elif "scope" in error_message.lower():
117
+ return cls(
118
+ SpotifyMCPErrorCode.INSUFFICIENT_SCOPE,
119
+ "Insufficient permissions for this operation",
120
+ {"http_status": status_code},
121
+ "Re-authenticate with required scopes",
122
+ )
123
+ else:
124
+ return cls(
125
+ SpotifyMCPErrorCode.PLAYBACK_RESTRICTED,
126
+ "Playback is restricted for this content",
127
+ {"http_status": status_code},
128
+ )
129
+
130
+ elif status_code == 404:
131
+ if "track" in error_message.lower():
132
+ return cls(
133
+ SpotifyMCPErrorCode.TRACK_NOT_FOUND,
134
+ "The requested track was not found",
135
+ {"http_status": status_code},
136
+ "Check the track ID and try again",
137
+ )
138
+ elif "playlist" in error_message.lower():
139
+ return cls(
140
+ SpotifyMCPErrorCode.PLAYLIST_NOT_FOUND,
141
+ "The requested playlist was not found",
142
+ {"http_status": status_code},
143
+ "Check the playlist ID and try again",
144
+ )
145
+ elif "user" in error_message.lower():
146
+ return cls(
147
+ SpotifyMCPErrorCode.USER_NOT_FOUND,
148
+ "The requested user was not found",
149
+ {"http_status": status_code},
150
+ )
151
+
152
+ elif status_code == 429:
153
+ return cls(
154
+ SpotifyMCPErrorCode.API_RATE_LIMITED,
155
+ "Spotify API rate limit exceeded",
156
+ {"http_status": status_code},
157
+ "Wait a moment before making more requests",
158
+ )
159
+
160
+ elif status_code and status_code >= 500:
161
+ return cls(
162
+ SpotifyMCPErrorCode.API_UNAVAILABLE,
163
+ "Spotify API is temporarily unavailable",
164
+ {"http_status": status_code},
165
+ "Try again in a few minutes",
166
+ )
167
+
168
+ # Handle specific device-related errors
169
+ if "no active device" in error_message.lower():
170
+ return cls(
171
+ SpotifyMCPErrorCode.NO_ACTIVE_DEVICE,
172
+ "No active Spotify device found",
173
+ {"original_error": error_message},
174
+ "Open Spotify on a device to start playback",
175
+ )
176
+
177
+ if "device not found" in error_message.lower():
178
+ return cls(
179
+ SpotifyMCPErrorCode.DEVICE_NOT_FOUND,
180
+ "The specified device was not found",
181
+ {"original_error": error_message},
182
+ "Check available devices and try again",
183
+ )
184
+
185
+ # Default case
186
+ return cls(
187
+ SpotifyMCPErrorCode.UNKNOWN_ERROR,
188
+ f"Spotify API error: {error_message}",
189
+ {"http_status": status_code, "original_error": error_message},
190
+ )
191
+
192
+ @classmethod
193
+ def validation_error(cls, field: str, message: str) -> "SpotifyMCPError":
194
+ """Create a validation error."""
195
+ return cls(
196
+ SpotifyMCPErrorCode.VALIDATION_ERROR,
197
+ f"Validation error for '{field}': {message}",
198
+ {"field": field},
199
+ "Check the input parameters and try again",
200
+ )
201
+
202
+ @classmethod
203
+ def no_active_device(cls) -> "SpotifyMCPError":
204
+ """Create a no active device error."""
205
+ return cls(
206
+ SpotifyMCPErrorCode.NO_ACTIVE_DEVICE,
207
+ "No active Spotify device found for playback",
208
+ {},
209
+ "Open Spotify on a device (phone, computer, etc.) to enable playback control",
210
+ )
211
+
212
+ @classmethod
213
+ def premium_required(cls, operation: str) -> "SpotifyMCPError":
214
+ """Create a premium required error."""
215
+ return cls(
216
+ SpotifyMCPErrorCode.PREMIUM_REQUIRED,
217
+ f"Spotify Premium is required for {operation}",
218
+ {"operation": operation},
219
+ "Upgrade to Spotify Premium to access this feature",
220
+ )
221
+
222
+
223
+ def convert_spotify_error(e: Exception) -> Exception:
224
+ """Convert Spotify exceptions to appropriate exception types for FastMCP."""
225
+ if isinstance(e, SpotifyException):
226
+ error = SpotifyMCPError.from_spotify_exception(e)
227
+ # For FastMCP, we'll raise a ValueError with the error message
228
+ return ValueError(error.message)
229
+ elif isinstance(e, SpotifyMCPError):
230
+ return ValueError(e.message)
231
+ else:
232
+ return ValueError(f"Unexpected error: {str(e)}")
233
+
234
+
235
+ def handle_spotify_error(func: Callable[..., Any]) -> Callable[..., Any]:
236
+ """Decorator to handle Spotify API errors and convert them to MCP-compliant responses."""
237
+
238
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
239
+ try:
240
+ return func(*args, **kwargs)
241
+ except SpotifyException as e:
242
+ error = SpotifyMCPError.from_spotify_exception(e)
243
+ return [error.to_mcp_error()]
244
+ except SpotifyMCPError as e:
245
+ return [e.to_mcp_error()]
246
+ except Exception as e:
247
+ error = SpotifyMCPError(
248
+ SpotifyMCPErrorCode.UNKNOWN_ERROR,
249
+ f"Unexpected error: {str(e)}",
250
+ {"error_type": type(e).__name__},
251
+ )
252
+ return [error.to_mcp_error()]
253
+
254
+ return wrapper
255
+
256
+
257
+ async def handle_spotify_error_async(func: Callable[..., Any]) -> Callable[..., Any]:
258
+ """Async decorator to handle Spotify API errors and convert them to MCP-compliant responses."""
259
+
260
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
261
+ try:
262
+ return await func(*args, **kwargs)
263
+ except SpotifyException as e:
264
+ error = SpotifyMCPError.from_spotify_exception(e)
265
+ return [error.to_mcp_error()]
266
+ except SpotifyMCPError as e:
267
+ return [e.to_mcp_error()]
268
+ except Exception as e:
269
+ error = SpotifyMCPError(
270
+ SpotifyMCPErrorCode.UNKNOWN_ERROR,
271
+ f"Unexpected error: {str(e)}",
272
+ {"error_type": type(e).__name__},
273
+ )
274
+ return [error.to_mcp_error()]
275
+
276
+ return wrapper