#
tokens: 23572/50000 11/11 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .DS_Store
├── .gitignore
├── build
│   ├── spotify-mcp-sse.js
│   └── spotify-mcp.js
├── Dockerfile
├── mcp
│   ├── spotify-mcp-http.js
│   ├── spotify-mcp-oauth-http.js
│   ├── spotify-mcp-sse.ts
│   └── spotify-mcp.ts
├── package.json
├── Readme.md
├── smithery.yaml
├── spotify-auth.js
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | node_modules
2 | secrets.json
3 | package-lock.json
4 | .env
```

--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Spotify MCP Server
  2 | 
  3 | A simple Model Context Protocol (MCP) server that lets you interact with Spotify through Claude. This server enables Claude to search for songs, create playlists, get recommendations, and more using your Spotify account.
  4 | 
  5 | ## Features
  6 | 
  7 | - Search for tracks on Spotify
  8 | - View your Spotify profile
  9 | - Create playlists
 10 | - Add tracks to playlists
 11 | - Get personalized music recommendations
 12 | 
 13 | ## Tools Available
 14 | 
 15 | | Tool Name                  | Description                                              |
 16 | | -------------------------- | -------------------------------------------------------- |
 17 | | `set-spotify-credentials`  | Set your Spotify authentication credentials              |
 18 | | `check-credentials-status` | Check if your credentials are valid and who is logged in |
 19 | | `search-tracks`            | Search for tracks by name, artist, or keywords           |
 20 | | `get-current-user`         | Get your Spotify profile information                     |
 21 | | `create-playlist`          | Create a new playlist on your account                    |
 22 | | `add-tracks-to-playlist`   | Add tracks to an existing playlist                       |
 23 | | `get-recommendations`      | Get recommendations based on seed tracks                 |
 24 | 
 25 | ## Setup Instructions
 26 | 
 27 | ## Plug and Play - HTTP Spotify MCP Server
 28 | 
 29 | 1. Go to [Claude AI](https://claude.ai/)
 30 | 2. Click on Add Connectors
 31 | 3. Click on Manage Connectors
 32 | 4. Add custom connector
 33 | 5. Add the https server link
 34 | 6. Mail [email protected] to whitelist your spotify ID/mail
 35 | 7. That's it. Your claude is ready to use Spotify. You will be prompted to sign-in using spotify OAuth.
 36 | 
 37 | ## Run Spotify MCP locally
 38 | 
 39 | ### 1. Prerequisites
 40 | 
 41 | - Node.js v16 or higher
 42 | - npm
 43 | - A Spotify account
 44 | - A registered Spotify Developer application
 45 | 
 46 | ### 2. Create a Spotify Developer App
 47 | 
 48 | 1. Go to [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/)
 49 | 2. Log in with your Spotify account
 50 | 3. Click "Create an App"
 51 | 4. Fill in the app name and description
 52 | 5. Add `http://localhost:8888/callback` as a Redirect URI
 53 | 6. Note your Client ID and Client Secret
 54 | 
 55 | ### 3. Install the Project
 56 | 
 57 | ```bash
 58 | # Clone or download the project first
 59 | cd spotify-mcp-server
 60 | 
 61 | # Install dependencies
 62 | npm install
 63 | ```
 64 | 
 65 | ### 4. Get Your Spotify Tokens
 66 | 
 67 | Edit the `spotify-auth.js` file to include your Client ID and Client Secret:
 68 | 
 69 | ```javascript
 70 | // Replace these with your Spotify app credentials
 71 | const CLIENT_ID = "your_client_id_here";
 72 | const CLIENT_SECRET = "your_client_secret_here";
 73 | ```
 74 | 
 75 | Then run the authentication script:
 76 | 
 77 | ```bash
 78 | node spotify-auth.js
 79 | ```
 80 | 
 81 | This will:
 82 | 
 83 | 1. Open a URL in your browser
 84 | 2. Prompt you to log in to Spotify
 85 | 3. Ask for your permission to access your account
 86 | 4. Save the tokens to `secrets.json`
 87 | 
 88 | ### 5. Build the MCP Server
 89 | 
 90 | ```bash
 91 | npm run build
 92 | ```
 93 | 
 94 | ### 6. Configure Claude Desktop
 95 | 
 96 | Edit your Claude Desktop configuration file:
 97 | 
 98 | - On macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
 99 | - On Windows: `%APPDATA%\Claude\claude_desktop_config.json`
100 | 
101 | Add the following configuration:
102 | 
103 | ```json
104 | {
105 |   "mcpServers": {
106 |     "spotify": {
107 |       "command": "node",
108 |       "args": ["/full/path/to/spotify-mcp-server/build/spotify-mcp-server.js"]
109 |     }
110 |   }
111 | }
112 | ```
113 | 
114 | Replace `/full/path/to/spotify-mcp-server` with the actual path to your project directory.
115 | 
116 | ### 7. Restart Claude Desktop
117 | 
118 | Close and reopen Claude Desktop to load the new configuration.
119 | 
120 | ## Usage
121 | 
122 | When you start a conversation with Claude, you'll first need to set your Spotify credentials:
123 | 
124 | 1. Look at your `secrets.json` file to get your credentials
125 | 2. Use the `set-spotify-credentials` tool to authenticate
126 | 3. Then use any of the other Spotify tools
127 | 
128 | ## Example Prompts
129 | 
130 | ### Setting Up Credentials
131 | 
132 | ```
133 | I want to connect to my Spotify account. Here are my credentials from secrets.json:
134 | 
135 | Tool: set-spotify-credentials
136 | Parameters:
137 | {
138 |   "clientId": "your_client_id",
139 |   "clientSecret": "your_client_secret",
140 |   "accessToken": "your_access_token",
141 |   "refreshToken": "your_refresh_token"
142 | }
143 | ```
144 | 
145 | ### Basic Commands
146 | 
147 | Check your account:
148 | 
149 | ```
150 | Can you check who I'm logged in as on Spotify?
151 | 
152 | Tool: get-current-user
153 | Parameters: {}
154 | ```
155 | 
156 | Search for tracks:
157 | 
158 | ```
159 | Search for songs by Weekend
160 | 
161 | Tool: search-tracks
162 | Parameters:
163 | {
164 |   "query": "Taylor Swift",
165 |   "limit": 5
166 | }
167 | ```
168 | 
169 | Create a playlist:
170 | 
171 | ```
172 | Create a new playlist called "My Pretty pretty girlfriend"
173 | 
174 | Tool: create-playlist
175 | Parameters:
176 | {
177 |   "name": "My Pretty pretty girlfriend",
178 |   "description": "For my girlfriend. Created with Claude and the Spotify MCP server"
179 | }
180 | ```
181 | 
182 | ### Multi-Step Tasks
183 | 
184 | Creating a playlist with songs:
185 | 
186 | ```
187 | I want to create a workout playlist with energetic songs. First, search for some high-energy songs. Then create a playlist called "Workout Mix" and add those songs to it.
188 | ```
189 | 
190 | Getting recommendations based on favorites:
191 | 
192 | ```
193 | I like the song "Blinding Lights" by The Weeknd. Can you search for it, then find similar songs, and create a playlist with those recommendations?
194 | ```
195 | 
196 | ## Troubleshooting
197 | 
198 | - **Error: No access token available**: You need to set your credentials first using the `set-spotify-credentials` tool
199 | - **Authentication failures**: Your tokens may have expired. Run the auth script again to get fresh tokens
200 | - **Invalid credentials**: Double check that you're using the correct Client ID and Client Secret
201 | 
202 | ## Notes
203 | 
204 | - The server stores credentials in memory only
205 | - You'll need to set credentials each time you start a new conversation
206 | - If Claude Desktop restarts, you'll need to set credentials again
207 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./mcp",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": ["*.ts", "mcp/spotify-mcp.ts", "mcp/spotify-mcp-sse.ts"],
14 |   "exclude": ["node_modules"]
15 | }
16 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM node:lts-alpine
 3 | 
 4 | WORKDIR /app
 5 | 
 6 | # Copy package.json and lock file if available
 7 | COPY package.json package-lock.json* ./
 8 | 
 9 | # Install dependencies without running scripts
10 | RUN npm install --ignore-scripts
11 | 
12 | # Copy source files
13 | COPY . .
14 | 
15 | # Build the TypeScript code
16 | RUN npm run build
17 | 
18 | CMD [ "node", "build/spotify-mcp-server.js" ]
19 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "spotify-mcp-server",
 3 |   "version": "1.0.0",
 4 |   "description": "MCP server for Spotify API integration",
 5 |   "type": "module",
 6 |   "main": "build/spotify-mcp.js",
 7 |   "scripts": {
 8 |     "auth": "node spotify-auth.js",
 9 |     "build": "tsc",
10 |     "start": "node build/spotify-mcp-http.js"
11 |   },
12 |   "dependencies": {
13 |     "@modelcontextprotocol/sdk": "^1.16.0",
14 |     "@types/cors": "^2.8.18",
15 |     "@types/express": "^5.0.1",
16 |     "axios": "^1.6.2",
17 |     "cors": "^2.8.5",
18 |     "express": "^4.21.2",
19 |     "node-fetch": "^3.3.2",
20 |     "zod": "^3.24.2"
21 |   },
22 |   "devDependencies": {
23 |     "@types/node": "^22.14.0",
24 |     "typescript": "^5.8.3"
25 |   }
26 | }
27 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required: []
 9 |     properties:
10 |       clientId:
11 |         type: string
12 |         description: Spotify Client ID
13 |       clientSecret:
14 |         type: string
15 |         description: Spotify Client Secret
16 |       accessToken:
17 |         type: string
18 |         description: Spotify Access Token
19 |       refreshToken:
20 |         type: string
21 |         description: Spotify Refresh Token
22 |   commandFunction:
23 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
24 |     |-
25 |     (config) => {
26 |       const env = {};
27 |       if (config.clientId) { env.SPOTIFY_CLIENT_ID = config.clientId; }
28 |       if (config.clientSecret) { env.SPOTIFY_CLIENT_SECRET = config.clientSecret; }
29 |       if (config.accessToken) { env.SPOTIFY_ACCESS_TOKEN = config.accessToken; }
30 |       if (config.refreshToken) { env.SPOTIFY_REFRESH_TOKEN = config.refreshToken; }
31 |       return {
32 |         command: "node",
33 |         args: ["build/spotify-mcp-server.js"],
34 |         env: env
35 |       };
36 |     }
37 |   exampleConfig:
38 |     clientId: your-spotify-client-id
39 |     clientSecret: your-spotify-client-secret
40 |     accessToken: your-initial-access-token
41 |     refreshToken: your-initial-refresh-token
42 | 
```

--------------------------------------------------------------------------------
/spotify-auth.js:
--------------------------------------------------------------------------------

```javascript
 1 | import express from "express";
 2 | import axios from "axios";
 3 | import fs from "fs";
 4 | 
 5 | // Replace these with your Spotify app credentials
 6 | const CLIENT_ID = process.env.SPOTIFY_CLIENT_ID;
 7 | const CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET;
 8 | const REDIRECT_URI = process.env.SPOTIFY_REDIRECT_URI;
 9 | 
10 | const app = express();
11 | const PORT = 8888;
12 | 
13 | app.get("/login", (req, res) => {
14 |   res.redirect(
15 |     `https://accounts.spotify.com/authorize?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${REDIRECT_URI}&scope=user-read-private%20user-read-email%20playlist-read-private%20playlist-modify-private%20playlist-modify-public`
16 |   );
17 | });
18 | 
19 | app.get("/callback", async (req, res) => {
20 |   const code = req.query.code;
21 | 
22 |   try {
23 |     const tokenResponse = await axios({
24 |       method: "post",
25 |       url: "https://accounts.spotify.com/api/token",
26 |       params: {
27 |         code: code,
28 |         redirect_uri: REDIRECT_URI,
29 |         grant_type: "authorization_code",
30 |       },
31 |       headers: {
32 |         Authorization:
33 |           "Basic " +
34 |           Buffer.from(CLIENT_ID + ":" + CLIENT_SECRET).toString("base64"),
35 |         "Content-Type": "application/x-www-form-urlencoded",
36 |       },
37 |     });
38 | 
39 |     // Save tokens to secrets.json file
40 |     const tokens = {
41 |       clientId: CLIENT_ID,
42 |       clientSecret: CLIENT_SECRET,
43 |       accessToken: tokenResponse.data.access_token,
44 |       refreshToken: tokenResponse.data.refresh_token,
45 |     };
46 | 
47 |     fs.writeFileSync("secrets.json", JSON.stringify(tokens, null, 2));
48 | 
49 |     res.send("Authentication successful! Tokens saved to secrets.json");
50 | 
51 |     setTimeout(() => {
52 |       server.close();
53 |       console.log(
54 |         "Tokens saved to secrets.json. You can now close this window."
55 |       );
56 |     }, 3000);
57 |   } catch (error) {
58 |     res.send("Error during authentication: " + error.message);
59 |   }
60 | });
61 | 
62 | const server = app.listen(PORT, () => {
63 |   console.log(
64 |     `Please open http://localhost:${PORT}/login in your browser to authenticate`
65 |   );
66 | });
67 | 
```

--------------------------------------------------------------------------------
/mcp/spotify-mcp.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  3 | import { z } from "zod";
  4 | import fetch from "node-fetch";
  5 | 
  6 | const server = new McpServer({
  7 |   name: "SpotifyServer",
  8 |   version: "1.0.0",
  9 |   capabilities: {
 10 |     tools: {},
 11 |   },
 12 | });
 13 | 
 14 | let spotifyAuthInfo = {
 15 |   accessToken: "",
 16 |   refreshToken: "",
 17 |   clientId: "",
 18 |   clientSecret: "",
 19 | };
 20 | 
 21 | // Refresh token when needed
 22 | async function getValidAccessToken() {
 23 |   if (!spotifyAuthInfo.accessToken || !spotifyAuthInfo.refreshToken) {
 24 |     throw new Error(
 25 |       "No access token available. Please set credentials first using the set-spotify-credentials tool."
 26 |     );
 27 |   }
 28 | 
 29 |   try {
 30 |     // Try using current token
 31 |     const response = await fetch("https://api.spotify.com/v1/me", {
 32 |       headers: {
 33 |         Authorization: `Bearer ${spotifyAuthInfo.accessToken}`,
 34 |       },
 35 |     });
 36 | 
 37 |     // If token works, return it
 38 |     if (response.ok) {
 39 |       return spotifyAuthInfo.accessToken;
 40 |     }
 41 | 
 42 |     console.error("Access token expired, refreshing...");
 43 | 
 44 |     // If token doesn't work, refresh it
 45 |     const refreshResponse = await fetch(
 46 |       "https://accounts.spotify.com/api/token",
 47 |       {
 48 |         method: "POST",
 49 |         headers: {
 50 |           "Content-Type": "application/x-www-form-urlencoded",
 51 |           Authorization:
 52 |             "Basic " +
 53 |             Buffer.from(
 54 |               spotifyAuthInfo.clientId + ":" + spotifyAuthInfo.clientSecret
 55 |             ).toString("base64"),
 56 |         },
 57 |         body: new URLSearchParams({
 58 |           grant_type: "refresh_token",
 59 |           refresh_token: spotifyAuthInfo.refreshToken,
 60 |         }),
 61 |       }
 62 |     );
 63 | 
 64 |     const data = (await refreshResponse.json()) as any;
 65 | 
 66 |     if (data.access_token) {
 67 |       console.error("Successfully refreshed access token");
 68 |       spotifyAuthInfo.accessToken = data.access_token;
 69 |       return spotifyAuthInfo.accessToken;
 70 |     }
 71 | 
 72 |     throw new Error("Failed to refresh access token");
 73 |   } catch (error) {
 74 |     throw new Error(
 75 |       "Error with access token: " +
 76 |         (error instanceof Error ? error.message : String(error))
 77 |     );
 78 |   }
 79 | }
 80 | 
 81 | // Set credentials tool
 82 | server.tool(
 83 |   "set-spotify-credentials",
 84 |   {
 85 |     clientId: z.string().describe("The Spotify Client ID"),
 86 |     clientSecret: z.string().describe("The Spotify Client Secret"),
 87 |     accessToken: z.string().describe("The Spotify Access Token"),
 88 |     refreshToken: z.string().describe("The Spotify Refresh Token"),
 89 |   },
 90 |   async ({ clientId, clientSecret, accessToken, refreshToken }) => {
 91 |     spotifyAuthInfo.clientId = clientId;
 92 |     spotifyAuthInfo.clientSecret = clientSecret;
 93 |     spotifyAuthInfo.accessToken = accessToken;
 94 |     spotifyAuthInfo.refreshToken = refreshToken;
 95 | 
 96 |     return {
 97 |       content: [
 98 |         {
 99 |           type: "text",
100 |           text: "Spotify credentials set successfully. You can now use other Spotify tools.",
101 |         },
102 |       ],
103 |     };
104 |   }
105 | );
106 | 
107 | // Check credentials tool
108 | server.tool("check-credentials-status", {}, async () => {
109 |   if (
110 |     !spotifyAuthInfo.accessToken ||
111 |     !spotifyAuthInfo.refreshToken ||
112 |     !spotifyAuthInfo.clientId ||
113 |     !spotifyAuthInfo.clientSecret
114 |   ) {
115 |     return {
116 |       content: [
117 |         {
118 |           type: "text",
119 |           text: "Spotify credentials are not set. Please use the set-spotify-credentials tool.",
120 |         },
121 |       ],
122 |     };
123 |   }
124 | 
125 |   try {
126 |     const accessToken = await getValidAccessToken();
127 | 
128 |     const response = await fetch("https://api.spotify.com/v1/me", {
129 |       headers: {
130 |         Authorization: `Bearer ${accessToken}`,
131 |       },
132 |     });
133 | 
134 |     if (response.ok) {
135 |       const userData = (await response.json()) as any;
136 |       return {
137 |         content: [
138 |           {
139 |             type: "text",
140 |             text: `Spotify credentials are valid.\nLogged in as: ${
141 |               userData.display_name
142 |             } (${userData.email || "email not available"})`,
143 |           },
144 |         ],
145 |       };
146 |     } else {
147 |       return {
148 |         content: [
149 |           {
150 |             type: "text",
151 |             text: `Spotify credentials may be invalid. Status code: ${response.status}`,
152 |           },
153 |         ],
154 |         isError: true,
155 |       };
156 |     }
157 |   } catch (error) {
158 |     return {
159 |       content: [
160 |         {
161 |           type: "text",
162 |           text: `Error checking credentials: ${
163 |             error instanceof Error ? error.message : String(error)
164 |           }`,
165 |         },
166 |       ],
167 |       isError: true,
168 |     };
169 |   }
170 | });
171 | 
172 | // Search tracks
173 | server.tool(
174 |   "search-tracks",
175 |   {
176 |     query: z.string().describe("Search query for tracks"),
177 |     limit: z
178 |       .number()
179 |       .min(1)
180 |       .max(50)
181 |       .default(10)
182 |       .describe("Number of results to return"),
183 |   },
184 |   async ({ query, limit }) => {
185 |     try {
186 |       const accessToken = await getValidAccessToken();
187 | 
188 |       const response = await fetch(
189 |         `https://api.spotify.com/v1/search?q=${encodeURIComponent(
190 |           query
191 |         )}&type=track&limit=${limit}`,
192 |         {
193 |           headers: {
194 |             Authorization: `Bearer ${accessToken}`,
195 |           },
196 |         }
197 |       );
198 | 
199 |       const data = (await response.json()) as any;
200 | 
201 |       if (!response.ok) {
202 |         return {
203 |           content: [
204 |             {
205 |               type: "text",
206 |               text: `Error searching tracks: ${JSON.stringify(data)}`,
207 |             },
208 |           ],
209 |           isError: true,
210 |         };
211 |       }
212 | 
213 |       const tracks = data.tracks.items.map((track: any) => ({
214 |         id: track.id,
215 |         name: track.name,
216 |         artist: track.artists.map((artist: any) => artist.name).join(", "),
217 |         album: track.album.name,
218 |         uri: track.uri,
219 |       }));
220 | 
221 |       return {
222 |         content: [
223 |           {
224 |             type: "text",
225 |             text: JSON.stringify(tracks, null, 2),
226 |           },
227 |         ],
228 |       };
229 |     } catch (error) {
230 |       return {
231 |         content: [
232 |           {
233 |             type: "text",
234 |             text: `Failed to search tracks: ${
235 |               error instanceof Error ? error.message : String(error)
236 |             }`,
237 |           },
238 |         ],
239 |         isError: true,
240 |       };
241 |     }
242 |   }
243 | );
244 | 
245 | // Get current user
246 | server.tool("get-current-user", {}, async () => {
247 |   try {
248 |     const accessToken = await getValidAccessToken();
249 | 
250 |     const response = await fetch("https://api.spotify.com/v1/me", {
251 |       headers: {
252 |         Authorization: `Bearer ${accessToken}`,
253 |       },
254 |     });
255 | 
256 |     const data = (await response.json()) as any;
257 | 
258 |     if (!response.ok) {
259 |       return {
260 |         content: [
261 |           {
262 |             type: "text",
263 |             text: `Error getting user profile: ${JSON.stringify(data)}`,
264 |           },
265 |         ],
266 |         isError: true,
267 |       };
268 |     }
269 | 
270 |     return {
271 |       content: [
272 |         {
273 |           type: "text",
274 |           text: JSON.stringify(
275 |             {
276 |               id: data.id,
277 |               name: data.display_name,
278 |               email: data.email,
279 |               country: data.country,
280 |             },
281 |             null,
282 |             2
283 |           ),
284 |         },
285 |       ],
286 |     };
287 |   } catch (error) {
288 |     return {
289 |       content: [
290 |         {
291 |           type: "text",
292 |           text: `Failed to get user profile: ${
293 |             error instanceof Error ? error.message : String(error)
294 |           }`,
295 |         },
296 |       ],
297 |       isError: true,
298 |     };
299 |   }
300 | });
301 | 
302 | // Create playlist
303 | server.tool(
304 |   "create-playlist",
305 |   {
306 |     name: z.string().describe("Name of the playlist"),
307 |     description: z.string().optional().describe("Description of the playlist"),
308 |   },
309 |   async ({ name, description = "" }) => {
310 |     try {
311 |       const accessToken = await getValidAccessToken();
312 | 
313 |       // Get user ID
314 |       const userResponse = await fetch("https://api.spotify.com/v1/me", {
315 |         headers: {
316 |           Authorization: `Bearer ${accessToken}`,
317 |         },
318 |       });
319 | 
320 |       const userData = (await userResponse.json()) as any;
321 |       const userId = userData.id;
322 | 
323 |       // Create playlist
324 |       const response = await fetch(
325 |         `https://api.spotify.com/v1/users/${userId}/playlists`,
326 |         {
327 |           method: "POST",
328 |           headers: {
329 |             Authorization: `Bearer ${accessToken}`,
330 |             "Content-Type": "application/json",
331 |           },
332 |           body: JSON.stringify({
333 |             name,
334 |             description,
335 |             public: false,
336 |           }),
337 |         }
338 |       );
339 | 
340 |       const data = (await response.json()) as any;
341 | 
342 |       if (!response.ok) {
343 |         return {
344 |           content: [
345 |             {
346 |               type: "text",
347 |               text: `Error creating playlist: ${JSON.stringify(data)}`,
348 |             },
349 |           ],
350 |           isError: true,
351 |         };
352 |       }
353 | 
354 |       return {
355 |         content: [
356 |           {
357 |             type: "text",
358 |             text: `Playlist created successfully!\nName: ${data.name}\nID: ${data.id}\nURL: ${data.external_urls.spotify}`,
359 |           },
360 |         ],
361 |       };
362 |     } catch (error) {
363 |       return {
364 |         content: [
365 |           {
366 |             type: "text",
367 |             text: `Failed to create playlist: ${
368 |               error instanceof Error ? error.message : String(error)
369 |             }`,
370 |           },
371 |         ],
372 |         isError: true,
373 |       };
374 |     }
375 |   }
376 | );
377 | 
378 | // Add tracks to playlist
379 | server.tool(
380 |   "add-tracks-to-playlist",
381 |   {
382 |     playlistId: z.string().describe("The Spotify playlist ID"),
383 |     trackUris: z
384 |       .array(z.string())
385 |       .describe("Array of Spotify track URIs to add"),
386 |   },
387 |   async ({ playlistId, trackUris }) => {
388 |     try {
389 |       const accessToken = await getValidAccessToken();
390 | 
391 |       const response = await fetch(
392 |         `https://api.spotify.com/v1/playlists/${playlistId}/tracks`,
393 |         {
394 |           method: "POST",
395 |           headers: {
396 |             Authorization: `Bearer ${accessToken}`,
397 |             "Content-Type": "application/json",
398 |           },
399 |           body: JSON.stringify({
400 |             uris: trackUris,
401 |           }),
402 |         }
403 |       );
404 | 
405 |       const data = (await response.json()) as any;
406 | 
407 |       if (!response.ok) {
408 |         return {
409 |           content: [
410 |             {
411 |               type: "text",
412 |               text: `Error adding tracks: ${JSON.stringify(data)}`,
413 |             },
414 |           ],
415 |           isError: true,
416 |         };
417 |       }
418 | 
419 |       return {
420 |         content: [
421 |           {
422 |             type: "text",
423 |             text: `Successfully added ${trackUris.length} track(s) to playlist!`,
424 |           },
425 |         ],
426 |       };
427 |     } catch (error) {
428 |       return {
429 |         content: [
430 |           {
431 |             type: "text",
432 |             text: `Failed to add tracks: ${
433 |               error instanceof Error ? error.message : String(error)
434 |             }`,
435 |           },
436 |         ],
437 |         isError: true,
438 |       };
439 |     }
440 |   }
441 | );
442 | 
443 | // Get recommendations
444 | server.tool(
445 |   "get-recommendations",
446 |   {
447 |     seedTracks: z
448 |       .array(z.string())
449 |       .max(5)
450 |       .describe("Spotify track IDs to use as seeds (max 5)"),
451 |     limit: z
452 |       .number()
453 |       .min(1)
454 |       .max(100)
455 |       .default(20)
456 |       .describe("Number of recommendations to return"),
457 |   },
458 |   async ({ seedTracks, limit }) => {
459 |     try {
460 |       const accessToken = await getValidAccessToken();
461 | 
462 |       if (seedTracks.length === 0) {
463 |         return {
464 |           content: [
465 |             {
466 |               type: "text",
467 |               text: "Error: At least one seed track is required",
468 |             },
469 |           ],
470 |           isError: true,
471 |         };
472 |       }
473 | 
474 |       const response = await fetch(
475 |         `https://api.spotify.com/v1/recommendations?seed_tracks=${seedTracks.join(
476 |           ","
477 |         )}&limit=${limit}`,
478 |         {
479 |           headers: {
480 |             Authorization: `Bearer ${accessToken}`,
481 |           },
482 |         }
483 |       );
484 | 
485 |       const data = (await response.json()) as any;
486 | 
487 |       if (!response.ok) {
488 |         return {
489 |           content: [
490 |             {
491 |               type: "text",
492 |               text: `Error getting recommendations: ${JSON.stringify(data)}`,
493 |             },
494 |           ],
495 |           isError: true,
496 |         };
497 |       }
498 | 
499 |       const tracks = data.tracks.map((track: any) => ({
500 |         id: track.id,
501 |         name: track.name,
502 |         artist: track.artists.map((artist: any) => artist.name).join(", "),
503 |         album: track.album.name,
504 |         uri: track.uri,
505 |       }));
506 | 
507 |       return {
508 |         content: [
509 |           {
510 |             type: "text",
511 |             text: JSON.stringify(tracks, null, 2),
512 |           },
513 |         ],
514 |       };
515 |     } catch (error) {
516 |       return {
517 |         content: [
518 |           {
519 |             type: "text",
520 |             text: `Failed to get recommendations: ${
521 |               error instanceof Error ? error.message : String(error)
522 |             }`,
523 |           },
524 |         ],
525 |         isError: true,
526 |       };
527 |     }
528 |   }
529 | );
530 | 
531 | // Start the server
532 | async function main() {
533 |   const transport = new StdioServerTransport();
534 |   await server.connect(transport);
535 |   console.error("Spotify MCP Server running on stdio");
536 |   console.error(
537 |     "No credentials are pre-loaded. Users must set credentials with set-spotify-credentials tool."
538 |   );
539 | }
540 | 
541 | main().catch((error) => {
542 |   console.error("Fatal error:", error);
543 |   process.exit(1);
544 | });
545 | 
```

--------------------------------------------------------------------------------
/mcp/spotify-mcp-sse.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  2 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
  3 | import { z } from "zod";
  4 | import fetch from "node-fetch";
  5 | import express from "express";
  6 | import cors from "cors";
  7 | 
  8 | const app = express();
  9 | app.use(
 10 |   cors({
 11 |     origin: "*",
 12 |     methods: ["GET", "POST"],
 13 |     allowedHeaders: ["Content-Type"],
 14 |   })
 15 | );
 16 | 
 17 | const server = new McpServer({
 18 |   name: "SpotifyServer",
 19 |   version: "1.0.0",
 20 |   capabilities: {
 21 |     tools: {},
 22 |   },
 23 | });
 24 | 
 25 | const transports = {};
 26 | 
 27 | let spotifyAuthInfo = {
 28 |   accessToken: "",
 29 |   refreshToken: "",
 30 |   clientId: "",
 31 |   clientSecret: "",
 32 | };
 33 | 
 34 | // Refresh token when needed
 35 | async function getValidAccessToken() {
 36 |   if (!spotifyAuthInfo.accessToken || !spotifyAuthInfo.refreshToken) {
 37 |     throw new Error(
 38 |       "No access token available. Please set credentials first using the set-spotify-credentials tool."
 39 |     );
 40 |   }
 41 | 
 42 |   try {
 43 |     // Try using current token
 44 |     const response = await fetch("https://api.spotify.com/v1/me", {
 45 |       headers: {
 46 |         Authorization: `Bearer ${spotifyAuthInfo.accessToken}`,
 47 |       },
 48 |     });
 49 | 
 50 |     // If token works, return it
 51 |     if (response.ok) {
 52 |       return spotifyAuthInfo.accessToken;
 53 |     }
 54 | 
 55 |     console.error("Access token expired, refreshing...");
 56 | 
 57 |     // If token doesn't work, refresh it
 58 |     const refreshResponse = await fetch(
 59 |       "https://accounts.spotify.com/api/token",
 60 |       {
 61 |         method: "POST",
 62 |         headers: {
 63 |           "Content-Type": "application/x-www-form-urlencoded",
 64 |           Authorization:
 65 |             "Basic " +
 66 |             Buffer.from(
 67 |               spotifyAuthInfo.clientId + ":" + spotifyAuthInfo.clientSecret
 68 |             ).toString("base64"),
 69 |         },
 70 |         body: new URLSearchParams({
 71 |           grant_type: "refresh_token",
 72 |           refresh_token: spotifyAuthInfo.refreshToken,
 73 |         }),
 74 |       }
 75 |     );
 76 | 
 77 |     const data = (await refreshResponse.json()) as any;
 78 | 
 79 |     if (data.access_token) {
 80 |       console.error("Successfully refreshed access token");
 81 |       spotifyAuthInfo.accessToken = data.access_token;
 82 |       return spotifyAuthInfo.accessToken;
 83 |     }
 84 | 
 85 |     throw new Error("Failed to refresh access token");
 86 |   } catch (error) {
 87 |     throw new Error(
 88 |       "Error with access token: " +
 89 |         (error instanceof Error ? error.message : String(error))
 90 |     );
 91 |   }
 92 | }
 93 | 
 94 | // Set credentials tool
 95 | server.tool(
 96 |   "set-spotify-credentials",
 97 |   {
 98 |     clientId: z.string().describe("The Spotify Client ID"),
 99 |     clientSecret: z.string().describe("The Spotify Client Secret"),
100 |     accessToken: z.string().describe("The Spotify Access Token"),
101 |     refreshToken: z.string().describe("The Spotify Refresh Token"),
102 |   },
103 |   async ({ clientId, clientSecret, accessToken, refreshToken }) => {
104 |     spotifyAuthInfo.clientId = clientId;
105 |     spotifyAuthInfo.clientSecret = clientSecret;
106 |     spotifyAuthInfo.accessToken = accessToken;
107 |     spotifyAuthInfo.refreshToken = refreshToken;
108 | 
109 |     return {
110 |       content: [
111 |         {
112 |           type: "text",
113 |           text: "Spotify credentials set successfully. You can now use other Spotify tools.",
114 |         },
115 |       ],
116 |     };
117 |   }
118 | );
119 | 
120 | // Check credentials tool
121 | server.tool("check-credentials-status", {}, async () => {
122 |   if (
123 |     !spotifyAuthInfo.accessToken ||
124 |     !spotifyAuthInfo.refreshToken ||
125 |     !spotifyAuthInfo.clientId ||
126 |     !spotifyAuthInfo.clientSecret
127 |   ) {
128 |     return {
129 |       content: [
130 |         {
131 |           type: "text",
132 |           text: "Spotify credentials are not set. Please use the set-spotify-credentials tool.",
133 |         },
134 |       ],
135 |     };
136 |   }
137 | 
138 |   try {
139 |     const accessToken = await getValidAccessToken();
140 | 
141 |     const response = await fetch("https://api.spotify.com/v1/me", {
142 |       headers: {
143 |         Authorization: `Bearer ${accessToken}`,
144 |       },
145 |     });
146 | 
147 |     if (response.ok) {
148 |       const userData = (await response.json()) as any;
149 |       return {
150 |         content: [
151 |           {
152 |             type: "text",
153 |             text: `Spotify credentials are valid.\nLogged in as: ${
154 |               userData.display_name
155 |             } (${userData.email || "email not available"})`,
156 |           },
157 |         ],
158 |       };
159 |     } else {
160 |       return {
161 |         content: [
162 |           {
163 |             type: "text",
164 |             text: `Spotify credentials may be invalid. Status code: ${response.status}`,
165 |           },
166 |         ],
167 |         isError: true,
168 |       };
169 |     }
170 |   } catch (error) {
171 |     return {
172 |       content: [
173 |         {
174 |           type: "text",
175 |           text: `Error checking credentials: ${
176 |             error instanceof Error ? error.message : String(error)
177 |           }`,
178 |         },
179 |       ],
180 |       isError: true,
181 |     };
182 |   }
183 | });
184 | 
185 | // Search tracks
186 | server.tool(
187 |   "search-tracks",
188 |   {
189 |     query: z.string().describe("Search query for tracks"),
190 |     limit: z
191 |       .number()
192 |       .min(1)
193 |       .max(50)
194 |       .default(10)
195 |       .describe("Number of results to return"),
196 |   },
197 |   async ({ query, limit }) => {
198 |     try {
199 |       const accessToken = await getValidAccessToken();
200 | 
201 |       const response = await fetch(
202 |         `https://api.spotify.com/v1/search?q=${encodeURIComponent(
203 |           query
204 |         )}&type=track&limit=${limit}`,
205 |         {
206 |           headers: {
207 |             Authorization: `Bearer ${accessToken}`,
208 |           },
209 |         }
210 |       );
211 | 
212 |       const data = (await response.json()) as any;
213 | 
214 |       if (!response.ok) {
215 |         return {
216 |           content: [
217 |             {
218 |               type: "text",
219 |               text: `Error searching tracks: ${JSON.stringify(data)}`,
220 |             },
221 |           ],
222 |           isError: true,
223 |         };
224 |       }
225 | 
226 |       const tracks = data.tracks.items.map((track: any) => ({
227 |         id: track.id,
228 |         name: track.name,
229 |         artist: track.artists.map((artist: any) => artist.name).join(", "),
230 |         album: track.album.name,
231 |         uri: track.uri,
232 |       }));
233 | 
234 |       return {
235 |         content: [
236 |           {
237 |             type: "text",
238 |             text: JSON.stringify(tracks, null, 2),
239 |           },
240 |         ],
241 |       };
242 |     } catch (error) {
243 |       return {
244 |         content: [
245 |           {
246 |             type: "text",
247 |             text: `Failed to search tracks: ${
248 |               error instanceof Error ? error.message : String(error)
249 |             }`,
250 |           },
251 |         ],
252 |         isError: true,
253 |       };
254 |     }
255 |   }
256 | );
257 | 
258 | // Get current user
259 | server.tool("get-current-user", {}, async () => {
260 |   try {
261 |     const accessToken = await getValidAccessToken();
262 | 
263 |     const response = await fetch("https://api.spotify.com/v1/me", {
264 |       headers: {
265 |         Authorization: `Bearer ${accessToken}`,
266 |       },
267 |     });
268 | 
269 |     const data = (await response.json()) as any;
270 | 
271 |     if (!response.ok) {
272 |       return {
273 |         content: [
274 |           {
275 |             type: "text",
276 |             text: `Error getting user profile: ${JSON.stringify(data)}`,
277 |           },
278 |         ],
279 |         isError: true,
280 |       };
281 |     }
282 | 
283 |     return {
284 |       content: [
285 |         {
286 |           type: "text",
287 |           text: JSON.stringify(
288 |             {
289 |               id: data.id,
290 |               name: data.display_name,
291 |               email: data.email,
292 |               country: data.country,
293 |             },
294 |             null,
295 |             2
296 |           ),
297 |         },
298 |       ],
299 |     };
300 |   } catch (error) {
301 |     return {
302 |       content: [
303 |         {
304 |           type: "text",
305 |           text: `Failed to get user profile: ${
306 |             error instanceof Error ? error.message : String(error)
307 |           }`,
308 |         },
309 |       ],
310 |       isError: true,
311 |     };
312 |   }
313 | });
314 | 
315 | // Create playlist
316 | server.tool(
317 |   "create-playlist",
318 |   {
319 |     name: z.string().describe("Name of the playlist"),
320 |     description: z.string().optional().describe("Description of the playlist"),
321 |   },
322 |   async ({ name, description = "" }) => {
323 |     try {
324 |       const accessToken = await getValidAccessToken();
325 | 
326 |       // Get user ID
327 |       const userResponse = await fetch("https://api.spotify.com/v1/me", {
328 |         headers: {
329 |           Authorization: `Bearer ${accessToken}`,
330 |         },
331 |       });
332 | 
333 |       const userData = (await userResponse.json()) as any;
334 |       const userId = userData.id;
335 | 
336 |       // Create playlist
337 |       const response = await fetch(
338 |         `https://api.spotify.com/v1/users/${userId}/playlists`,
339 |         {
340 |           method: "POST",
341 |           headers: {
342 |             Authorization: `Bearer ${accessToken}`,
343 |             "Content-Type": "application/json",
344 |           },
345 |           body: JSON.stringify({
346 |             name,
347 |             description,
348 |             public: false,
349 |           }),
350 |         }
351 |       );
352 | 
353 |       const data = (await response.json()) as any;
354 | 
355 |       if (!response.ok) {
356 |         return {
357 |           content: [
358 |             {
359 |               type: "text",
360 |               text: `Error creating playlist: ${JSON.stringify(data)}`,
361 |             },
362 |           ],
363 |           isError: true,
364 |         };
365 |       }
366 | 
367 |       return {
368 |         content: [
369 |           {
370 |             type: "text",
371 |             text: `Playlist created successfully!\nName: ${data.name}\nID: ${data.id}\nURL: ${data.external_urls.spotify}`,
372 |           },
373 |         ],
374 |       };
375 |     } catch (error) {
376 |       return {
377 |         content: [
378 |           {
379 |             type: "text",
380 |             text: `Failed to create playlist: ${
381 |               error instanceof Error ? error.message : String(error)
382 |             }`,
383 |           },
384 |         ],
385 |         isError: true,
386 |       };
387 |     }
388 |   }
389 | );
390 | 
391 | // Add tracks to playlist
392 | server.tool(
393 |   "add-tracks-to-playlist",
394 |   {
395 |     playlistId: z.string().describe("The Spotify playlist ID"),
396 |     trackUris: z
397 |       .array(z.string())
398 |       .describe("Array of Spotify track URIs to add"),
399 |   },
400 |   async ({ playlistId, trackUris }) => {
401 |     try {
402 |       const accessToken = await getValidAccessToken();
403 | 
404 |       const response = await fetch(
405 |         `https://api.spotify.com/v1/playlists/${playlistId}/tracks`,
406 |         {
407 |           method: "POST",
408 |           headers: {
409 |             Authorization: `Bearer ${accessToken}`,
410 |             "Content-Type": "application/json",
411 |           },
412 |           body: JSON.stringify({
413 |             uris: trackUris,
414 |           }),
415 |         }
416 |       );
417 | 
418 |       const data = (await response.json()) as any;
419 | 
420 |       if (!response.ok) {
421 |         return {
422 |           content: [
423 |             {
424 |               type: "text",
425 |               text: `Error adding tracks: ${JSON.stringify(data)}`,
426 |             },
427 |           ],
428 |           isError: true,
429 |         };
430 |       }
431 | 
432 |       return {
433 |         content: [
434 |           {
435 |             type: "text",
436 |             text: `Successfully added ${trackUris.length} track(s) to playlist!`,
437 |           },
438 |         ],
439 |       };
440 |     } catch (error) {
441 |       return {
442 |         content: [
443 |           {
444 |             type: "text",
445 |             text: `Failed to add tracks: ${
446 |               error instanceof Error ? error.message : String(error)
447 |             }`,
448 |           },
449 |         ],
450 |         isError: true,
451 |       };
452 |     }
453 |   }
454 | );
455 | 
456 | // Get recommendations
457 | server.tool(
458 |   "get-recommendations",
459 |   {
460 |     seedTracks: z
461 |       .array(z.string())
462 |       .max(5)
463 |       .describe("Spotify track IDs to use as seeds (max 5)"),
464 |     limit: z
465 |       .number()
466 |       .min(1)
467 |       .max(100)
468 |       .default(20)
469 |       .describe("Number of recommendations to return"),
470 |   },
471 |   async ({ seedTracks, limit }) => {
472 |     try {
473 |       const accessToken = await getValidAccessToken();
474 | 
475 |       if (seedTracks.length === 0) {
476 |         return {
477 |           content: [
478 |             {
479 |               type: "text",
480 |               text: "Error: At least one seed track is required",
481 |             },
482 |           ],
483 |           isError: true,
484 |         };
485 |       }
486 | 
487 |       const response = await fetch(
488 |         `https://api.spotify.com/v1/recommendations?seed_tracks=${seedTracks.join(
489 |           ","
490 |         )}&limit=${limit}`,
491 |         {
492 |           headers: {
493 |             Authorization: `Bearer ${accessToken}`,
494 |           },
495 |         }
496 |       );
497 | 
498 |       const data = (await response.json()) as any;
499 | 
500 |       if (!response.ok) {
501 |         return {
502 |           content: [
503 |             {
504 |               type: "text",
505 |               text: `Error getting recommendations: ${JSON.stringify(data)}`,
506 |             },
507 |           ],
508 |           isError: true,
509 |         };
510 |       }
511 | 
512 |       const tracks = data.tracks.map((track: any) => ({
513 |         id: track.id,
514 |         name: track.name,
515 |         artist: track.artists.map((artist: any) => artist.name).join(", "),
516 |         album: track.album.name,
517 |         uri: track.uri,
518 |       }));
519 | 
520 |       return {
521 |         content: [
522 |           {
523 |             type: "text",
524 |             text: JSON.stringify(tracks, null, 2),
525 |           },
526 |         ],
527 |       };
528 |     } catch (error) {
529 |       return {
530 |         content: [
531 |           {
532 |             type: "text",
533 |             text: `Failed to get recommendations: ${
534 |               error instanceof Error ? error.message : String(error)
535 |             }`,
536 |           },
537 |         ],
538 |         isError: true,
539 |       };
540 |     }
541 |   }
542 | );
543 | 
544 | let transport: SSEServerTransport | null = null;
545 | 
546 | app.get("/sse", (req, res) => {
547 |   transport = new SSEServerTransport("/messages", res);
548 |   server.connect(transport);
549 | });
550 | 
551 | app.post("/messages", (req, res) => {
552 |   if (transport) {
553 |     transport.handlePostMessage(req, res);
554 |   } else {
555 |     res.status(503).send("No active transport");
556 |   }
557 | });
558 | 
559 | app.listen(3001, () => {
560 |   console.log("Listening on port 3001");
561 | });
562 | 
```

--------------------------------------------------------------------------------
/mcp/spotify-mcp-http.js:
--------------------------------------------------------------------------------

```javascript
  1 | import express from "express";
  2 | import cors from "cors";
  3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
  5 | import { z } from "zod";
  6 | import fetch from "node-fetch";
  7 | 
  8 | // GLOBAL credentials storage (shared across all requests)
  9 | let globalSpotifyAuthInfo = {
 10 |   accessToken: "",
 11 |   refreshToken: "",
 12 |   clientId: "",
 13 |   clientSecret: "",
 14 | };
 15 | 
 16 | // Helper function to create server instance
 17 | function createSpotifyMcpServer() {
 18 |   const server = new McpServer({
 19 |     name: "SpotifyServer",
 20 |     version: "1.0.0",
 21 |     capabilities: {
 22 |       tools: {},
 23 |     },
 24 |   });
 25 | 
 26 |   // Refresh token when needed
 27 |   async function getValidAccessToken() {
 28 |     if (
 29 |       !globalSpotifyAuthInfo.accessToken ||
 30 |       !globalSpotifyAuthInfo.refreshToken
 31 |     ) {
 32 |       throw new Error(
 33 |         "No access token available. Please set credentials first using the set-spotify-credentials tool."
 34 |       );
 35 |     }
 36 | 
 37 |     try {
 38 |       // Try using current token
 39 |       const response = await fetch("https://api.spotify.com/v1/me", {
 40 |         headers: {
 41 |           Authorization: `Bearer ${globalSpotifyAuthInfo.accessToken}`,
 42 |         },
 43 |       });
 44 | 
 45 |       // If token works, return it
 46 |       if (response.ok) {
 47 |         return globalSpotifyAuthInfo.accessToken;
 48 |       }
 49 | 
 50 |       console.log("Access token expired, refreshing...");
 51 | 
 52 |       // If token doesn't work, refresh it
 53 |       const refreshResponse = await fetch(
 54 |         "https://accounts.spotify.com/api/token",
 55 |         {
 56 |           method: "POST",
 57 |           headers: {
 58 |             "Content-Type": "application/x-www-form-urlencoded",
 59 |             Authorization:
 60 |               "Basic " +
 61 |               Buffer.from(
 62 |                 globalSpotifyAuthInfo.clientId +
 63 |                   ":" +
 64 |                   globalSpotifyAuthInfo.clientSecret
 65 |               ).toString("base64"),
 66 |           },
 67 |           body: new URLSearchParams({
 68 |             grant_type: "refresh_token",
 69 |             refresh_token: globalSpotifyAuthInfo.refreshToken,
 70 |           }),
 71 |         }
 72 |       );
 73 | 
 74 |       const data = await refreshResponse.json();
 75 | 
 76 |       if (data.access_token) {
 77 |         console.log("Successfully refreshed access token");
 78 |         globalSpotifyAuthInfo.accessToken = data.access_token;
 79 |         return globalSpotifyAuthInfo.accessToken;
 80 |       }
 81 | 
 82 |       throw new Error("Failed to refresh access token");
 83 |     } catch (error) {
 84 |       throw new Error("Error with access token: " + error.message);
 85 |     }
 86 |   }
 87 | 
 88 |   // Set credentials tool
 89 |   server.tool(
 90 |     "set-spotify-credentials",
 91 |     {
 92 |       clientId: z.string().describe("The Spotify Client ID"),
 93 |       clientSecret: z.string().describe("The Spotify Client Secret"),
 94 |       accessToken: z.string().describe("The Spotify Access Token"),
 95 |       refreshToken: z.string().describe("The Spotify Refresh Token"),
 96 |     },
 97 |     async ({ clientId, clientSecret, accessToken, refreshToken }) => {
 98 |       globalSpotifyAuthInfo.clientId = clientId;
 99 |       globalSpotifyAuthInfo.clientSecret = clientSecret;
100 |       globalSpotifyAuthInfo.accessToken = accessToken;
101 |       globalSpotifyAuthInfo.refreshToken = refreshToken;
102 | 
103 |       return {
104 |         content: [
105 |           {
106 |             type: "text",
107 |             text: "Spotify credentials set successfully. You can now use other Spotify tools.",
108 |           },
109 |         ],
110 |       };
111 |     }
112 |   );
113 | 
114 |   // Check credentials tool
115 |   server.tool("check-credentials-status", {}, async () => {
116 |     if (
117 |       !globalSpotifyAuthInfo.accessToken ||
118 |       !globalSpotifyAuthInfo.refreshToken ||
119 |       !globalSpotifyAuthInfo.clientId ||
120 |       !globalSpotifyAuthInfo.clientSecret
121 |     ) {
122 |       return {
123 |         content: [
124 |           {
125 |             type: "text",
126 |             text: "Spotify credentials are not set. Please use the set-spotify-credentials tool.",
127 |           },
128 |         ],
129 |       };
130 |     }
131 | 
132 |     try {
133 |       const accessToken = await getValidAccessToken();
134 | 
135 |       const response = await fetch("https://api.spotify.com/v1/me", {
136 |         headers: {
137 |           Authorization: `Bearer ${accessToken}`,
138 |         },
139 |       });
140 | 
141 |       if (response.ok) {
142 |         const userData = await response.json();
143 |         return {
144 |           content: [
145 |             {
146 |               type: "text",
147 |               text: `Spotify credentials are valid.\nLogged in as: ${
148 |                 userData.display_name
149 |               } (${userData.email || "email not available"})`,
150 |             },
151 |           ],
152 |         };
153 |       } else {
154 |         return {
155 |           content: [
156 |             {
157 |               type: "text",
158 |               text: `Spotify credentials may be invalid. Status code: ${response.status}`,
159 |             },
160 |           ],
161 |           isError: true,
162 |         };
163 |       }
164 |     } catch (error) {
165 |       return {
166 |         content: [
167 |           {
168 |             type: "text",
169 |             text: `Error checking credentials: ${error.message}`,
170 |           },
171 |         ],
172 |         isError: true,
173 |       };
174 |     }
175 |   });
176 | 
177 |   // Search tracks
178 |   server.tool(
179 |     "search-tracks",
180 |     {
181 |       query: z.string().describe("Search query for tracks"),
182 |       limit: z
183 |         .number()
184 |         .min(1)
185 |         .max(50)
186 |         .default(10)
187 |         .describe("Number of results to return"),
188 |     },
189 |     async ({ query, limit }) => {
190 |       try {
191 |         const accessToken = await getValidAccessToken();
192 | 
193 |         const response = await fetch(
194 |           `https://api.spotify.com/v1/search?q=${encodeURIComponent(
195 |             query
196 |           )}&type=track&limit=${limit}`,
197 |           {
198 |             headers: {
199 |               Authorization: `Bearer ${accessToken}`,
200 |             },
201 |           }
202 |         );
203 | 
204 |         const data = await response.json();
205 | 
206 |         if (!response.ok) {
207 |           return {
208 |             content: [
209 |               {
210 |                 type: "text",
211 |                 text: `Error searching tracks: ${JSON.stringify(data)}`,
212 |               },
213 |             ],
214 |             isError: true,
215 |           };
216 |         }
217 | 
218 |         const tracks = data.tracks.items.map((track) => ({
219 |           id: track.id,
220 |           name: track.name,
221 |           artist: track.artists.map((artist) => artist.name).join(", "),
222 |           album: track.album.name,
223 |           uri: track.uri,
224 |         }));
225 | 
226 |         return {
227 |           content: [
228 |             {
229 |               type: "text",
230 |               text: JSON.stringify(tracks, null, 2),
231 |             },
232 |           ],
233 |         };
234 |       } catch (error) {
235 |         return {
236 |           content: [
237 |             {
238 |               type: "text",
239 |               text: `Failed to search tracks: ${error.message}`,
240 |             },
241 |           ],
242 |           isError: true,
243 |         };
244 |       }
245 |     }
246 |   );
247 | 
248 |   // Get current user
249 |   server.tool("get-current-user", {}, async () => {
250 |     try {
251 |       const accessToken = await getValidAccessToken();
252 | 
253 |       const response = await fetch("https://api.spotify.com/v1/me", {
254 |         headers: {
255 |           Authorization: `Bearer ${accessToken}`,
256 |         },
257 |       });
258 | 
259 |       const data = await response.json();
260 | 
261 |       if (!response.ok) {
262 |         return {
263 |           content: [
264 |             {
265 |               type: "text",
266 |               text: `Error getting user profile: ${JSON.stringify(data)}`,
267 |             },
268 |           ],
269 |           isError: true,
270 |         };
271 |       }
272 | 
273 |       return {
274 |         content: [
275 |           {
276 |             type: "text",
277 |             text: JSON.stringify(
278 |               {
279 |                 id: data.id,
280 |                 name: data.display_name,
281 |                 email: data.email,
282 |                 country: data.country,
283 |               },
284 |               null,
285 |               2
286 |             ),
287 |           },
288 |         ],
289 |       };
290 |     } catch (error) {
291 |       return {
292 |         content: [
293 |           {
294 |             type: "text",
295 |             text: `Failed to get user profile: ${error.message}`,
296 |           },
297 |         ],
298 |         isError: true,
299 |       };
300 |     }
301 |   });
302 | 
303 |   // Create playlist
304 |   server.tool(
305 |     "create-playlist",
306 |     {
307 |       name: z.string().describe("Name of the playlist"),
308 |       description: z
309 |         .string()
310 |         .optional()
311 |         .describe("Description of the playlist"),
312 |     },
313 |     async ({ name, description = "" }) => {
314 |       try {
315 |         const accessToken = await getValidAccessToken();
316 | 
317 |         // Get user ID
318 |         const userResponse = await fetch("https://api.spotify.com/v1/me", {
319 |           headers: {
320 |             Authorization: `Bearer ${accessToken}`,
321 |           },
322 |         });
323 | 
324 |         const userData = await userResponse.json();
325 |         const userId = userData.id;
326 | 
327 |         // Create playlist
328 |         const response = await fetch(
329 |           `https://api.spotify.com/v1/users/${userId}/playlists`,
330 |           {
331 |             method: "POST",
332 |             headers: {
333 |               Authorization: `Bearer ${accessToken}`,
334 |               "Content-Type": "application/json",
335 |             },
336 |             body: JSON.stringify({
337 |               name,
338 |               description,
339 |               public: false,
340 |             }),
341 |           }
342 |         );
343 | 
344 |         const data = await response.json();
345 | 
346 |         if (!response.ok) {
347 |           return {
348 |             content: [
349 |               {
350 |                 type: "text",
351 |                 text: `Error creating playlist: ${JSON.stringify(data)}`,
352 |               },
353 |             ],
354 |             isError: true,
355 |           };
356 |         }
357 | 
358 |         return {
359 |           content: [
360 |             {
361 |               type: "text",
362 |               text: `Playlist created successfully!\nName: ${data.name}\nID: ${data.id}\nURL: ${data.external_urls.spotify}`,
363 |             },
364 |           ],
365 |         };
366 |       } catch (error) {
367 |         return {
368 |           content: [
369 |             {
370 |               type: "text",
371 |               text: `Failed to create playlist: ${error.message}`,
372 |             },
373 |           ],
374 |           isError: true,
375 |         };
376 |       }
377 |     }
378 |   );
379 | 
380 |   // Add tracks to playlist
381 |   server.tool(
382 |     "add-tracks-to-playlist",
383 |     {
384 |       playlistId: z.string().describe("The Spotify playlist ID"),
385 |       trackUris: z
386 |         .array(z.string())
387 |         .describe("Array of Spotify track URIs to add"),
388 |     },
389 |     async ({ playlistId, trackUris }) => {
390 |       try {
391 |         const accessToken = await getValidAccessToken();
392 | 
393 |         const response = await fetch(
394 |           `https://api.spotify.com/v1/playlists/${playlistId}/tracks`,
395 |           {
396 |             method: "POST",
397 |             headers: {
398 |               Authorization: `Bearer ${accessToken}`,
399 |               "Content-Type": "application/json",
400 |             },
401 |             body: JSON.stringify({
402 |               uris: trackUris,
403 |             }),
404 |           }
405 |         );
406 | 
407 |         const data = await response.json();
408 | 
409 |         if (!response.ok) {
410 |           return {
411 |             content: [
412 |               {
413 |                 type: "text",
414 |                 text: `Error adding tracks: ${JSON.stringify(data)}`,
415 |               },
416 |             ],
417 |             isError: true,
418 |           };
419 |         }
420 | 
421 |         return {
422 |           content: [
423 |             {
424 |               type: "text",
425 |               text: `Successfully added ${trackUris.length} track(s) to playlist!`,
426 |             },
427 |           ],
428 |         };
429 |       } catch (error) {
430 |         return {
431 |           content: [
432 |             {
433 |               type: "text",
434 |               text: `Failed to add tracks: ${error.message}`,
435 |             },
436 |           ],
437 |           isError: true,
438 |         };
439 |       }
440 |     }
441 |   );
442 | 
443 |   // Get recommendations
444 |   server.tool(
445 |     "get-recommendations",
446 |     {
447 |       seedTracks: z
448 |         .array(z.string())
449 |         .max(5)
450 |         .describe("Spotify track IDs to use as seeds (max 5)"),
451 |       limit: z
452 |         .number()
453 |         .min(1)
454 |         .max(100)
455 |         .default(20)
456 |         .describe("Number of recommendations to return"),
457 |     },
458 |     async ({ seedTracks, limit }) => {
459 |       try {
460 |         const accessToken = await getValidAccessToken();
461 | 
462 |         if (seedTracks.length === 0) {
463 |           return {
464 |             content: [
465 |               {
466 |                 type: "text",
467 |                 text: "Error: At least one seed track is required",
468 |               },
469 |             ],
470 |             isError: true,
471 |           };
472 |         }
473 | 
474 |         const response = await fetch(
475 |           `https://api.spotify.com/v1/recommendations?seed_tracks=${seedTracks.join(
476 |             ","
477 |           )}&limit=${limit}`,
478 |           {
479 |             headers: {
480 |               Authorization: `Bearer ${accessToken}`,
481 |             },
482 |           }
483 |         );
484 | 
485 |         const data = await response.json();
486 | 
487 |         if (!response.ok) {
488 |           return {
489 |             content: [
490 |               {
491 |                 type: "text",
492 |                 text: `Error getting recommendations: ${JSON.stringify(data)}`,
493 |               },
494 |             ],
495 |             isError: true,
496 |           };
497 |         }
498 | 
499 |         const tracks = data.tracks.map((track) => ({
500 |           id: track.id,
501 |           name: track.name,
502 |           artist: track.artists.map((artist) => artist.name).join(", "),
503 |           album: track.album.name,
504 |           uri: track.uri,
505 |         }));
506 | 
507 |         return {
508 |           content: [
509 |             {
510 |               type: "text",
511 |               text: JSON.stringify(tracks, null, 2),
512 |             },
513 |           ],
514 |         };
515 |       } catch (error) {
516 |         return {
517 |           content: [
518 |             {
519 |               type: "text",
520 |               text: `Failed to get recommendations: ${error.message}`,
521 |             },
522 |           ],
523 |           isError: true,
524 |         };
525 |       }
526 |     }
527 |   );
528 | 
529 |   return server;
530 | }
531 | 
532 | // Create Express app
533 | const app = express();
534 | app.use(express.json());
535 | 
536 | // CORS configuration
537 | app.use(
538 |   cors({
539 |     origin: "*", // Configure this for production
540 |     exposedHeaders: ["Mcp-Session-Id"],
541 |     allowedHeaders: ["Content-Type", "mcp-session-id"],
542 |   })
543 | );
544 | 
545 | // Health check endpoint
546 | app.get("/health", (req, res) => {
547 |   res.json({ status: "healthy", timestamp: new Date().toISOString() });
548 | });
549 | 
550 | // MCP endpoint (stateless mode)
551 | app.post("/mcp", async (req, res) => {
552 |   try {
553 |     // Create new server instance for each request (stateless)
554 |     const server = createSpotifyMcpServer();
555 |     const transport = new StreamableHTTPServerTransport({
556 |       sessionIdGenerator: undefined, // No session management
557 |     });
558 | 
559 |     // Clean up when request closes
560 |     res.on("close", () => {
561 |       console.log("Request closed, cleaning up");
562 |       transport.close();
563 |       server.close();
564 |     });
565 | 
566 |     // Connect server to transport
567 |     await server.connect(transport);
568 | 
569 |     // Handle the request
570 |     await transport.handleRequest(req, res, req.body);
571 |   } catch (error) {
572 |     console.error("Error handling MCP request:", error);
573 |     if (!res.headersSent) {
574 |       res.status(500).json({
575 |         jsonrpc: "2.0",
576 |         error: {
577 |           code: -32603,
578 |           message: "Internal server error",
579 |         },
580 |         id: null,
581 |       });
582 |     }
583 |   }
584 | });
585 | 
586 | // Start the server
587 | const PORT = process.env.PORT || 8080;
588 | app.listen(PORT, () => {
589 |   console.log(`Spotify MCP Server running on port ${PORT}`);
590 |   console.log(`Health check: http://localhost:${PORT}/health`);
591 |   console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
592 |   console.log(`Mode: STATELESS`);
593 | });
594 | 
```

--------------------------------------------------------------------------------
/mcp/spotify-mcp-oauth-http.js:
--------------------------------------------------------------------------------

```javascript
  1 | import express from "express";
  2 | import cors from "cors";
  3 | import { randomUUID } from "node:crypto";
  4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  5 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
  6 | import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
  7 | import { z } from "zod";
  8 | import fetch from "node-fetch";
  9 | 
 10 | // Environment variables
 11 | const SPOTIFY_CLIENT_ID = process.env.SPOTIFY_CLIENT_ID;
 12 | const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET;
 13 | const SPOTIFY_REDIRECT_URI =
 14 |   process.env.SPOTIFY_REDIRECT_URI || "http://localhost:8080/callback/spotify";
 15 | 
 16 | if (!SPOTIFY_CLIENT_ID || !SPOTIFY_CLIENT_SECRET) {
 17 |   console.error(
 18 |     "Missing required environment variables: SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET"
 19 |   );
 20 |   process.exit(1);
 21 | }
 22 | 
 23 | console.log("🎵 Spotify Config:");
 24 | console.log("- Client ID:", SPOTIFY_CLIENT_ID);
 25 | console.log("- Redirect URI:", SPOTIFY_REDIRECT_URI);
 26 | 
 27 | // Session storage
 28 | const transports = new Map();
 29 | const sessionTokens = new Map();
 30 | const sessionActivity = new Map(); // Track last activity per session
 31 | 
 32 | // Session cleanup configuration
 33 | const INACTIVITY_TIMEOUT = 8 * 60 * 1000; // 8 minutes
 34 | const CLEANUP_INTERVAL = 3 * 60 * 1000; // 3 minutes
 35 | 
 36 | // Simple cleanup function
 37 | function cleanupSession(sessionId) {
 38 |   console.log(`🧹 Cleaning up session: ${sessionId}`);
 39 | 
 40 |   const transport = transports.get(sessionId);
 41 |   if (transport) {
 42 |     try {
 43 |       transport.close();
 44 |     } catch (error) {
 45 |       console.warn(`Warning closing transport:`, error.message);
 46 |     }
 47 |     transports.delete(sessionId);
 48 |   }
 49 | 
 50 |   sessionTokens.delete(sessionId);
 51 |   sessionActivity.delete(sessionId);
 52 | }
 53 | 
 54 | // Update session activity
 55 | function updateSessionActivity(sessionId) {
 56 |   sessionActivity.set(sessionId, Date.now());
 57 | }
 58 | 
 59 | // Cleanup inactive sessions
 60 | function cleanupInactiveSessions() {
 61 |   const now = Date.now();
 62 |   const inactiveSessions = [];
 63 | 
 64 |   for (const [sessionId, lastActivity] of sessionActivity.entries()) {
 65 |     if (now - lastActivity > INACTIVITY_TIMEOUT) {
 66 |       inactiveSessions.push(sessionId);
 67 |     }
 68 |   }
 69 | 
 70 |   if (inactiveSessions.length > 0) {
 71 |     console.log(`🧹 Cleaning up ${inactiveSessions.length} inactive sessions`);
 72 |     inactiveSessions.forEach(cleanupSession);
 73 |   }
 74 | }
 75 | 
 76 | // Start cleanup interval
 77 | setInterval(() => {
 78 |   try {
 79 |     cleanupInactiveSessions();
 80 |   } catch (error) {
 81 |     console.error('Error during inactive session cleanup:', error);
 82 |   }
 83 | }, CLEANUP_INTERVAL);
 84 | 
 85 | // Helper functions
 86 | function isTokenExpired(tokens) {
 87 |   if (!tokens.expiresAt) return true;
 88 |   return Date.now() >= tokens.expiresAt;
 89 | }
 90 | 
 91 | function getSpotifyAuthUrl(sessionId) {
 92 |   const params = new URLSearchParams({
 93 |     response_type: "code",
 94 |     client_id: SPOTIFY_CLIENT_ID,
 95 |     scope:
 96 |       "user-read-private user-read-email playlist-read-private playlist-modify-private playlist-modify-public",
 97 |     redirect_uri: SPOTIFY_REDIRECT_URI,
 98 |     state: sessionId,
 99 |   });
100 |   return `https://accounts.spotify.com/authorize?${params.toString()}`;
101 | }
102 | 
103 | async function exchangeCodeForTokens(code) {
104 |   const response = await fetch("https://accounts.spotify.com/api/token", {
105 |     method: "POST",
106 |     headers: {
107 |       "Content-Type": "application/x-www-form-urlencoded",
108 |       Authorization:
109 |         "Basic " +
110 |         Buffer.from(SPOTIFY_CLIENT_ID + ":" + SPOTIFY_CLIENT_SECRET).toString(
111 |           "base64"
112 |         ),
113 |     },
114 |     body: new URLSearchParams({
115 |       grant_type: "authorization_code",
116 |       code: code,
117 |       redirect_uri: SPOTIFY_REDIRECT_URI,
118 |     }),
119 |   });
120 | 
121 |   const data = await response.json();
122 | 
123 |   if (!response.ok) {
124 |     throw new Error(
125 |       `Token exchange failed: ${data.error_description || data.error}`
126 |     );
127 |   }
128 | 
129 |   return {
130 |     accessToken: data.access_token,
131 |     refreshToken: data.refresh_token,
132 |     expiresAt: Date.now() + data.expires_in * 1000,
133 |   };
134 | }
135 | 
136 | async function refreshSpotifyToken(tokens) {
137 |   const response = await fetch("https://accounts.spotify.com/api/token", {
138 |     method: "POST",
139 |     headers: {
140 |       "Content-Type": "application/x-www-form-urlencoded",
141 |       Authorization:
142 |         "Basic " +
143 |         Buffer.from(SPOTIFY_CLIENT_ID + ":" + SPOTIFY_CLIENT_SECRET).toString(
144 |           "base64"
145 |         ),
146 |     },
147 |     body: new URLSearchParams({
148 |       grant_type: "refresh_token",
149 |       refresh_token: tokens.refreshToken,
150 |     }),
151 |   });
152 | 
153 |   const data = await response.json();
154 | 
155 |   if (!response.ok) {
156 |     throw new Error(
157 |       `Token refresh failed: ${data.error_description || data.error}`
158 |     );
159 |   }
160 | 
161 |   return {
162 |     accessToken: data.access_token,
163 |     refreshToken: data.refresh_token || tokens.refreshToken,
164 |     expiresAt: Date.now() + data.expires_in * 1000,
165 |   };
166 | }
167 | 
168 | async function getValidAccessToken(sessionId) {
169 |   let tokens = sessionTokens.get(sessionId);
170 | 
171 |   if (!tokens) {
172 |     return null;
173 |   }
174 | 
175 |   if (isTokenExpired(tokens)) {
176 |     try {
177 |       tokens = await refreshSpotifyToken(tokens);
178 |       sessionTokens.set(sessionId, tokens);
179 |     } catch (error) {
180 |       console.log(
181 |         `❌ Token refresh failed for session ${sessionId}:`,
182 |         error.message
183 |       );
184 |       sessionTokens.delete(sessionId);
185 |       return null;
186 |     }
187 |   }
188 | 
189 |   return tokens.accessToken;
190 | }
191 | 
192 | async function handleSpotifyTool(sessionId, apiCall) {
193 |   const accessToken = await getValidAccessToken(sessionId);
194 | 
195 |   if (!accessToken) {
196 |     console.log(`🔐 Auth required for session: ${sessionId}`);
197 |     const baseUrl =
198 |       process.env.SPOTIFY_REDIRECT_URI?.replace("/callback/spotify", "") ||
199 |       "http://localhost:8080";
200 |     const authUrl = `${baseUrl}/auth?session=${sessionId}`;
201 | 
202 |     return {
203 |       content: [
204 |         {
205 |           type: "text",
206 |           text: `🎵 **Spotify Authentication Required**
207 | 
208 | To use Spotify features, please visit:
209 | 
210 | ${authUrl}
211 | 
212 | This will redirect you to connect your Spotify account. After authentication, return here and try your request again.`,
213 |         },
214 |       ],
215 |     };
216 |   }
217 | 
218 |   try {
219 |     console.log(`✅ Executing Spotify API call for session: ${sessionId}`);
220 |     return await apiCall(accessToken);
221 |   } catch (error) {
222 |     if (error.response?.status === 401) {
223 |       console.log(`❌ Token expired for session: ${sessionId}`);
224 |       sessionTokens.delete(sessionId);
225 |       const baseUrl =
226 |         process.env.SPOTIFY_REDIRECT_URI?.replace("/callback/spotify", "") ||
227 |         "http://localhost:8080";
228 |       const authUrl = `${baseUrl}/auth?session=${sessionId}`;
229 |       return {
230 |         content: [
231 |           {
232 |             type: "text",
233 |             text: `🔐 **Spotify Authentication Expired**
234 | 
235 | Your Spotify session has expired. Please visit:
236 | 
237 | ${authUrl}
238 | 
239 | After completing authentication, return here and try your request again.`,
240 |           },
241 |         ],
242 |         isError: true,
243 |       };
244 |     }
245 | 
246 |     console.log(
247 |       `❌ Spotify API error for session ${sessionId}:`,
248 |       error.message
249 |     );
250 |     return {
251 |       content: [
252 |         {
253 |           type: "text",
254 |           text: `❌ **Spotify API Error**
255 | 
256 | ${error.message}`,
257 |         },
258 |       ],
259 |       isError: true,
260 |     };
261 |   }
262 | }
263 | 
264 | // Create MCP server instance with session context
265 | function createSpotifyMcpServer(sessionId) {
266 |   const server = new McpServer({
267 |     name: "SpotifyServer",
268 |     version: "1.0.0",
269 |   });
270 | 
271 |   // Search tracks
272 |   server.registerTool(
273 |     "search-tracks",
274 |     {
275 |       title: "Search Spotify Tracks",
276 |       description: "Search for tracks on Spotify",
277 |       inputSchema: {
278 |         query: z.string().describe("Search query for tracks"),
279 |         limit: z
280 |           .number()
281 |           .min(1)
282 |           .max(50)
283 |           .default(10)
284 |           .describe("Number of results to return"),
285 |       },
286 |     },
287 |     async ({ query, limit }) => {
288 |       return await handleSpotifyTool(sessionId, async (accessToken) => {
289 |         const response = await fetch(
290 |           `https://api.spotify.com/v1/search?q=${encodeURIComponent(
291 |             query
292 |           )}&type=track&limit=${limit}`,
293 |           {
294 |             headers: {
295 |               Authorization: `Bearer ${accessToken}`,
296 |             },
297 |           }
298 |         );
299 | 
300 |         const data = await response.json();
301 | 
302 |         if (!response.ok) {
303 |           throw new Error(
304 |             `Spotify API error: ${data.error?.message || "Unknown error"}`
305 |           );
306 |         }
307 | 
308 |         const tracks = data.tracks.items.map((track) => ({
309 |           id: track.id,
310 |           name: track.name,
311 |           artist: track.artists.map((artist) => artist.name).join(", "),
312 |           album: track.album.name,
313 |           uri: track.uri,
314 |         }));
315 | 
316 |         return {
317 |           content: [
318 |             {
319 |               type: "text",
320 |               text: JSON.stringify(tracks, null, 2),
321 |             },
322 |           ],
323 |         };
324 |       });
325 |     }
326 |   );
327 | 
328 |   // Get current user
329 |   server.registerTool(
330 |     "get-current-user",
331 |     {
332 |       title: "Get Current User",
333 |       description: "Get current Spotify user information",
334 |     },
335 |     async () => {
336 |       return await handleSpotifyTool(sessionId, async (accessToken) => {
337 |         const response = await fetch("https://api.spotify.com/v1/me", {
338 |           headers: {
339 |             Authorization: `Bearer ${accessToken}`,
340 |           },
341 |         });
342 | 
343 |         const data = await response.json();
344 | 
345 |         if (!response.ok) {
346 |           throw new Error(
347 |             `Spotify API error: ${data.error?.message || "Unknown error"}`
348 |           );
349 |         }
350 | 
351 |         return {
352 |           content: [
353 |             {
354 |               type: "text",
355 |               text: JSON.stringify(
356 |                 {
357 |                   id: data.id,
358 |                   name: data.display_name,
359 |                   email: data.email,
360 |                   country: data.country,
361 |                   followers: data.followers?.total || 0,
362 |                 },
363 |                 null,
364 |                 2
365 |               ),
366 |             },
367 |           ],
368 |         };
369 |       });
370 |     }
371 |   );
372 | 
373 |   // Create playlist
374 |   server.registerTool(
375 |     "create-playlist",
376 |     {
377 |       title: "Create Playlist",
378 |       description: "Create a new playlist on Spotify",
379 |       inputSchema: {
380 |         name: z.string().describe("Name of the playlist"),
381 |         description: z
382 |           .string()
383 |           .optional()
384 |           .describe("Description of the playlist"),
385 |         public: z
386 |           .boolean()
387 |           .default(false)
388 |           .describe("Whether playlist should be public"),
389 |       },
390 |     },
391 |     async ({ name, description = "", public: isPublic }) => {
392 |       return await handleSpotifyTool(sessionId, async (accessToken) => {
393 |         // Get user ID first
394 |         const userResponse = await fetch("https://api.spotify.com/v1/me", {
395 |           headers: {
396 |             Authorization: `Bearer ${accessToken}`,
397 |           },
398 |         });
399 | 
400 |         const userData = await userResponse.json();
401 |         const userId = userData.id;
402 | 
403 |         // Create playlist
404 |         const response = await fetch(
405 |           `https://api.spotify.com/v1/users/${userId}/playlists`,
406 |           {
407 |             method: "POST",
408 |             headers: {
409 |               Authorization: `Bearer ${accessToken}`,
410 |               "Content-Type": "application/json",
411 |             },
412 |             body: JSON.stringify({
413 |               name,
414 |               description,
415 |               public: isPublic,
416 |             }),
417 |           }
418 |         );
419 | 
420 |         const data = await response.json();
421 | 
422 |         if (!response.ok) {
423 |           throw new Error(
424 |             `Spotify API error: ${data.error?.message || "Unknown error"}`
425 |           );
426 |         }
427 | 
428 |         return {
429 |           content: [
430 |             {
431 |               type: "text",
432 |               text: `✅ Playlist created successfully!\n\n**${data.name}**\nID: ${data.id}\n🔗 [Open in Spotify](${data.external_urls.spotify})`,
433 |             },
434 |           ],
435 |         };
436 |       });
437 |     }
438 |   );
439 | 
440 |   // Add tracks to playlist
441 |   server.registerTool(
442 |     "add-tracks-to-playlist",
443 |     {
444 |       title: "Add Tracks to Playlist",
445 |       description: "Add tracks to an existing playlist",
446 |       inputSchema: {
447 |         playlistId: z.string().describe("The Spotify playlist ID"),
448 |         trackUris: z
449 |           .array(z.string())
450 |           .describe("Array of Spotify track URIs to add"),
451 |       },
452 |     },
453 |     async ({ playlistId, trackUris }) => {
454 |       return await handleSpotifyTool(sessionId, async (accessToken) => {
455 |         const response = await fetch(
456 |           `https://api.spotify.com/v1/playlists/${playlistId}/tracks`,
457 |           {
458 |             method: "POST",
459 |             headers: {
460 |               Authorization: `Bearer ${accessToken}`,
461 |               "Content-Type": "application/json",
462 |             },
463 |             body: JSON.stringify({
464 |               uris: trackUris,
465 |             }),
466 |           }
467 |         );
468 | 
469 |         const data = await response.json();
470 | 
471 |         if (!response.ok) {
472 |           throw new Error(
473 |             `Spotify API error: ${data.error?.message || "Unknown error"}`
474 |           );
475 |         }
476 | 
477 |         return {
478 |           content: [
479 |             {
480 |               type: "text",
481 |               text: `✅ Successfully added ${trackUris.length} track(s) to playlist!`,
482 |             },
483 |           ],
484 |         };
485 |       });
486 |     }
487 |   );
488 | 
489 |   // Get recommendations
490 |   server.registerTool(
491 |     "get-recommendations",
492 |     {
493 |       title: "Get Recommendations",
494 |       description: "Get music recommendations based on seed tracks",
495 |       inputSchema: {
496 |         seedTracks: z
497 |           .array(z.string())
498 |           .max(5)
499 |           .describe("Spotify track IDs to use as seeds (max 5)"),
500 |         limit: z
501 |           .number()
502 |           .min(1)
503 |           .max(100)
504 |           .default(20)
505 |           .describe("Number of recommendations to return"),
506 |       },
507 |     },
508 |     async ({ seedTracks, limit }) => {
509 |       return await handleSpotifyTool(sessionId, async (accessToken) => {
510 |         if (seedTracks.length === 0) {
511 |           throw new Error("At least one seed track is required");
512 |         }
513 | 
514 |         const response = await fetch(
515 |           `https://api.spotify.com/v1/recommendations?seed_tracks=${seedTracks.join(
516 |             ","
517 |           )}&limit=${limit}`,
518 |           {
519 |             headers: {
520 |               Authorization: `Bearer ${accessToken}`,
521 |             },
522 |           }
523 |         );
524 | 
525 |         const data = await response.json();
526 | 
527 |         if (!response.ok) {
528 |           throw new Error(
529 |             `Spotify API error: ${data.error?.message || "Unknown error"}`
530 |           );
531 |         }
532 | 
533 |         const tracks = data.tracks.map((track) => ({
534 |           id: track.id,
535 |           name: track.name,
536 |           artist: track.artists.map((artist) => artist.name).join(", "),
537 |           album: track.album.name,
538 |           uri: track.uri,
539 |         }));
540 | 
541 |         return {
542 |           content: [
543 |             {
544 |               type: "text",
545 |               text: JSON.stringify(tracks, null, 2),
546 |             },
547 |           ],
548 |         };
549 |       });
550 |     }
551 |   );
552 | 
553 |   return server;
554 | }
555 | 
556 | // Express app setup
557 | const app = express();
558 | app.use(express.json());
559 | 
560 | // CORS configuration
561 | app.use(
562 |   cors({
563 |     origin: "*",
564 |     exposedHeaders: ["Mcp-Session-Id"],
565 |     allowedHeaders: ["Content-Type", "mcp-session-id"],
566 |   })
567 | );
568 | 
569 | // Health check
570 | app.get("/health", (req, res) => {
571 |   res.json({
572 |     status: "healthy",
573 |     timestamp: new Date().toISOString(),
574 |     activeSessions: transports.size,
575 |   });
576 | });
577 | 
578 | // Simple auth landing page
579 | app.get("/auth", (req, res) => {
580 |   const sessionId = req.query.session;
581 | 
582 |   if (!sessionId) {
583 |     res.status(400).send("Missing session ID");
584 |     return;
585 |   }
586 | 
587 |   const authUrl = getSpotifyAuthUrl(sessionId);
588 | 
589 |   res.send(`
590 |     <html>
591 |       <head>
592 |         <title>Connect Spotify to Claude</title>
593 |         <style>
594 |           body {
595 |             font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
596 |             text-align: center;
597 |             padding: 50px;
598 |             background: linear-gradient(135deg, #667eea, #764ba2);
599 |             color: white;
600 |             margin: 0;
601 |           }
602 |           .container {
603 |             background: rgba(0,0,0,0.1);
604 |             padding: 40px;
605 |             border-radius: 20px;
606 |             backdrop-filter: blur(10px);
607 |             display: inline-block;
608 |             max-width: 500px;
609 |           }
610 |           .spotify-icon { font-size: 60px; margin-bottom: 20px; }
611 |           h1 { margin: 20px 0; font-size: 28px; }
612 |           p { font-size: 18px; line-height: 1.5; opacity: 0.9; }
613 |           .auth-button {
614 |             display: inline-block;
615 |             background: #1db954;
616 |             color: white;
617 |             padding: 15px 30px;
618 |             border-radius: 50px;
619 |             text-decoration: none;
620 |             font-size: 18px;
621 |             font-weight: bold;
622 |             margin: 20px 0;
623 |             transition: all 0.3s ease;
624 |           }
625 |           .auth-button:hover {
626 |             background: #1ed760;
627 |             transform: translateY(-2px);
628 |           }
629 |         </style>
630 |       </head>
631 |       <body>
632 |         <div class="container">
633 |           <div class="spotify-icon">🎵</div>
634 |           <h1>Connect Spotify to Claude</h1>
635 |           <p>Click the button below to connect your Spotify account and enable music features in Claude.</p>
636 | 
637 |           <a href="${authUrl}" class="auth-button">Connect Spotify Account</a>
638 | 
639 |           <p style="font-size: 14px; margin-top: 30px;">
640 |             After connecting, return to Claude to use Spotify features.
641 |           </p>
642 |         </div>
643 |       </body>
644 |     </html>
645 |   `);
646 | });
647 | 
648 | // Spotify OAuth callback
649 | app.get("/callback/spotify", async (req, res) => {
650 |   const { code, state: sessionId, error } = req.query;
651 | 
652 |   if (error) {
653 |     console.log(`❌ OAuth error: ${error}`);
654 |     res.status(400).send(`Authentication error: ${error}`);
655 |     return;
656 |   }
657 | 
658 |   if (!code || !sessionId) {
659 |     console.log(`❌ OAuth callback missing code or sessionId`);
660 |     res.status(400).send("Missing authorization code or session ID");
661 |     return;
662 |   }
663 | 
664 |   console.log(`🔄 Processing OAuth callback for session: ${sessionId}`);
665 | 
666 |   try {
667 |     const tokens = await exchangeCodeForTokens(code);
668 |     sessionTokens.set(sessionId, tokens);
669 |     console.log(`✅ OAuth successful for session: ${sessionId}`);
670 | 
671 |     res.send(`
672 |       <html>
673 |         <head>
674 |           <title>Spotify Connected Successfully</title>
675 |           <style>
676 |             body {
677 |               font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
678 |               text-align: center;
679 |               padding: 50px;
680 |               background: linear-gradient(135deg, #1db954, #1ed760);
681 |               color: white;
682 |               margin: 0;
683 |             }
684 |             .container {
685 |               background: rgba(0,0,0,0.1);
686 |               padding: 40px;
687 |               border-radius: 20px;
688 |               backdrop-filter: blur(10px);
689 |               display: inline-block;
690 |               max-width: 500px;
691 |             }
692 |             .success-icon { font-size: 60px; margin-bottom: 20px; }
693 |             h1 { margin: 20px 0; font-size: 28px; }
694 |             p { font-size: 18px; line-height: 1.5; opacity: 0.9; }
695 |             .instruction {
696 |               background: rgba(255,255,255,0.1);
697 |               padding: 20px;
698 |               border-radius: 10px;
699 |               margin-top: 30px;
700 |               border: 1px solid rgba(255,255,255,0.2);
701 |             }
702 |           </style>
703 |         </head>
704 |         <body>
705 |           <div class="container">
706 |             <div class="success-icon">🎵</div>
707 |             <h1>Successfully Connected to Spotify!</h1>
708 |             <p>Your Spotify account is now linked to Claude.</p>
709 | 
710 |             <div class="instruction">
711 |               <strong>Next Steps:</strong><br>
712 |               1. Return to your Claude conversation<br>
713 |               2. Try your Spotify request again<br>
714 |               3. This window can be closed
715 |             </div>
716 |           </div>
717 |         </body>
718 |       </html>
719 |     `);
720 |   } catch (error) {
721 |     console.error(
722 |       `❌ Token exchange failed for session ${sessionId}:`,
723 |       error.message
724 |     );
725 |     res.status(500).send(`Authentication error: ${error.message}`);
726 |   }
727 | });
728 | 
729 | // MCP endpoint with session management
730 | app.post("/mcp", async (req, res) => {
731 |   try {
732 |     const sessionId = req.headers["mcp-session-id"];
733 |     let transport;
734 | 
735 |     if (sessionId && transports.has(sessionId)) {
736 |       // Reuse existing transport and update activity
737 |       transport = transports.get(sessionId);
738 |       updateSessionActivity(sessionId);
739 |     } else if (!sessionId && isInitializeRequest(req.body)) {
740 |       // New initialization request
741 |       const newSessionId = randomUUID();
742 | 
743 |       transport = new StreamableHTTPServerTransport({
744 |         sessionIdGenerator: () => newSessionId,
745 |         onsessioninitialized: (sessionId) => {
746 |           console.log(`🎯 New MCP session initialized: ${sessionId}`);
747 |           updateSessionActivity(sessionId); // Track initial activity
748 |         },
749 |       });
750 | 
751 |       // Clean up transport when closed
752 |       transport.onclose = () => {
753 |         if (transport.sessionId) {
754 |           console.log(`🔌 MCP session closed: ${transport.sessionId}`);
755 |           cleanupSession(transport.sessionId);
756 |         }
757 |       };
758 | 
759 |       // Create and connect server with sessionId
760 |       const server = createSpotifyMcpServer(newSessionId);
761 |       await server.connect(transport);
762 | 
763 |       // Store transport
764 |       transports.set(newSessionId, transport);
765 |     } else {
766 |       // Invalid request
767 |       res.status(400).json({
768 |         jsonrpc: "2.0",
769 |         error: {
770 |           code: -32000,
771 |           message: "Bad Request: No valid session ID provided",
772 |         },
773 |         id: null,
774 |       });
775 |       return;
776 |     }
777 | 
778 |     // Handle the request
779 |     await transport.handleRequest(req, res, req.body);
780 |   } catch (error) {
781 |     console.error(`❌ MCP request error:`, error.message);
782 |     if (!res.headersSent) {
783 |       res.status(500).json({
784 |         jsonrpc: "2.0",
785 |         error: {
786 |           code: -32603,
787 |           message: "Internal server error",
788 |         },
789 |         id: null,
790 |       });
791 |     }
792 |   }
793 | });
794 | 
795 | // Handle GET requests for server-to-client notifications via SSE
796 | app.get("/mcp", async (req, res) => {
797 |   const sessionId = req.headers["mcp-session-id"];
798 |   if (!sessionId || !transports.has(sessionId)) {
799 |     res.status(400).send("Invalid or missing session ID");
800 |     return;
801 |   }
802 | 
803 |   const transport = transports.get(sessionId);
804 |   updateSessionActivity(sessionId); // Update activity for SSE requests too
805 |   await transport.handleRequest(req, res);
806 | });
807 | 
808 | // Handle DELETE requests for session termination
809 | app.delete("/mcp", async (req, res) => {
810 |   const sessionId = req.headers["mcp-session-id"];
811 |   if (!sessionId || !transports.has(sessionId)) {
812 |     res.status(400).send("Invalid or missing session ID");
813 |     return;
814 |   }
815 | 
816 |   const transport = transports.get(sessionId);
817 |   await transport.handleRequest(req, res);
818 | 
819 |   // Clean up on explicit delete
820 |   cleanupSession(sessionId);
821 | });
822 | 
823 | // Start server
824 | const PORT = process.env.PORT || 8080;
825 | app.listen(PORT, () => {
826 |   console.log(`🚀 Spotify OAuth MCP Server running on port ${PORT}`);
827 |   console.log(`🔗 Health check: http://localhost:${PORT}/health`);
828 |   console.log(`🔗 MCP endpoint: http://localhost:${PORT}/mcp`);
829 |   console.log(`🔗 OAuth callback: http://localhost:${PORT}/callback/spotify`);
830 |   console.log(`📊 Mode: STATEFUL with OAuth`);
831 |   console.log(
832 |     `⏰ Session cleanup: ${INACTIVITY_TIMEOUT / 60000} min inactivity timeout`
833 |   );
834 | });
835 | 
```