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.
- iflow_mcp_jamiew_spotify_mcp-0.2.0.dist-info/METADATA +73 -0
- iflow_mcp_jamiew_spotify_mcp-0.2.0.dist-info/RECORD +12 -0
- iflow_mcp_jamiew_spotify_mcp-0.2.0.dist-info/WHEEL +5 -0
- iflow_mcp_jamiew_spotify_mcp-0.2.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_jamiew_spotify_mcp-0.2.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_jamiew_spotify_mcp-0.2.0.dist-info/top_level.txt +1 -0
- spotify_mcp/__init__.py +23 -0
- spotify_mcp/errors.py +276 -0
- spotify_mcp/fastmcp_server.py +1092 -0
- spotify_mcp/logging_utils.py +156 -0
- spotify_mcp/spotify_api.py +577 -0
- spotify_mcp/utils.py +240 -0
|
@@ -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,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
|
spotify_mcp/__init__.py
ADDED
|
@@ -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
|