Skip to main content
Glama
spotify-mcp-http.js14.9 kB
import express from "express"; import cors from "cors"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { z } from "zod"; import fetch from "node-fetch"; // GLOBAL credentials storage (shared across all requests) let globalSpotifyAuthInfo = { accessToken: "", refreshToken: "", clientId: "", clientSecret: "", }; // Helper function to create server instance function createSpotifyMcpServer() { const server = new McpServer({ name: "SpotifyServer", version: "1.0.0", capabilities: { tools: {}, }, }); // Refresh token when needed async function getValidAccessToken() { if ( !globalSpotifyAuthInfo.accessToken || !globalSpotifyAuthInfo.refreshToken ) { throw new Error( "No access token available. Please set credentials first using the set-spotify-credentials tool." ); } try { // Try using current token const response = await fetch("https://api.spotify.com/v1/me", { headers: { Authorization: `Bearer ${globalSpotifyAuthInfo.accessToken}`, }, }); // If token works, return it if (response.ok) { return globalSpotifyAuthInfo.accessToken; } console.log("Access token expired, refreshing..."); // If token doesn't work, refresh it const refreshResponse = await fetch( "https://accounts.spotify.com/api/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Authorization: "Basic " + Buffer.from( globalSpotifyAuthInfo.clientId + ":" + globalSpotifyAuthInfo.clientSecret ).toString("base64"), }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: globalSpotifyAuthInfo.refreshToken, }), } ); const data = await refreshResponse.json(); if (data.access_token) { console.log("Successfully refreshed access token"); globalSpotifyAuthInfo.accessToken = data.access_token; return globalSpotifyAuthInfo.accessToken; } throw new Error("Failed to refresh access token"); } catch (error) { throw new Error("Error with access token: " + error.message); } } // Set credentials tool server.tool( "set-spotify-credentials", { clientId: z.string().describe("The Spotify Client ID"), clientSecret: z.string().describe("The Spotify Client Secret"), accessToken: z.string().describe("The Spotify Access Token"), refreshToken: z.string().describe("The Spotify Refresh Token"), }, async ({ clientId, clientSecret, accessToken, refreshToken }) => { globalSpotifyAuthInfo.clientId = clientId; globalSpotifyAuthInfo.clientSecret = clientSecret; globalSpotifyAuthInfo.accessToken = accessToken; globalSpotifyAuthInfo.refreshToken = refreshToken; return { content: [ { type: "text", text: "Spotify credentials set successfully. You can now use other Spotify tools.", }, ], }; } ); // Check credentials tool server.tool("check-credentials-status", {}, async () => { if ( !globalSpotifyAuthInfo.accessToken || !globalSpotifyAuthInfo.refreshToken || !globalSpotifyAuthInfo.clientId || !globalSpotifyAuthInfo.clientSecret ) { return { content: [ { type: "text", text: "Spotify credentials are not set. Please use the set-spotify-credentials tool.", }, ], }; } try { const accessToken = await getValidAccessToken(); const response = await fetch("https://api.spotify.com/v1/me", { headers: { Authorization: `Bearer ${accessToken}`, }, }); if (response.ok) { const userData = await response.json(); return { content: [ { type: "text", text: `Spotify credentials are valid.\nLogged in as: ${ userData.display_name } (${userData.email || "email not available"})`, }, ], }; } else { return { content: [ { type: "text", text: `Spotify credentials may be invalid. Status code: ${response.status}`, }, ], isError: true, }; } } catch (error) { return { content: [ { type: "text", text: `Error checking credentials: ${error.message}`, }, ], isError: true, }; } }); // Search tracks server.tool( "search-tracks", { query: z.string().describe("Search query for tracks"), limit: z .number() .min(1) .max(50) .default(10) .describe("Number of results to return"), }, async ({ query, limit }) => { try { const accessToken = await getValidAccessToken(); const response = await fetch( `https://api.spotify.com/v1/search?q=${encodeURIComponent( query )}&type=track&limit=${limit}`, { headers: { Authorization: `Bearer ${accessToken}`, }, } ); const data = await response.json(); if (!response.ok) { return { content: [ { type: "text", text: `Error searching tracks: ${JSON.stringify(data)}`, }, ], isError: true, }; } const tracks = data.tracks.items.map((track) => ({ id: track.id, name: track.name, artist: track.artists.map((artist) => artist.name).join(", "), album: track.album.name, uri: track.uri, })); return { content: [ { type: "text", text: JSON.stringify(tracks, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Failed to search tracks: ${error.message}`, }, ], isError: true, }; } } ); // Get current user server.tool("get-current-user", {}, async () => { try { const accessToken = await getValidAccessToken(); const response = await fetch("https://api.spotify.com/v1/me", { headers: { Authorization: `Bearer ${accessToken}`, }, }); const data = await response.json(); if (!response.ok) { return { content: [ { type: "text", text: `Error getting user profile: ${JSON.stringify(data)}`, }, ], isError: true, }; } return { content: [ { type: "text", text: JSON.stringify( { id: data.id, name: data.display_name, email: data.email, country: data.country, }, null, 2 ), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Failed to get user profile: ${error.message}`, }, ], isError: true, }; } }); // Create playlist server.tool( "create-playlist", { name: z.string().describe("Name of the playlist"), description: z .string() .optional() .describe("Description of the playlist"), }, async ({ name, description = "" }) => { try { const accessToken = await getValidAccessToken(); // Get user ID const userResponse = await fetch("https://api.spotify.com/v1/me", { headers: { Authorization: `Bearer ${accessToken}`, }, }); const userData = await userResponse.json(); const userId = userData.id; // Create playlist const response = await fetch( `https://api.spotify.com/v1/users/${userId}/playlists`, { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ name, description, public: false, }), } ); const data = await response.json(); if (!response.ok) { return { content: [ { type: "text", text: `Error creating playlist: ${JSON.stringify(data)}`, }, ], isError: true, }; } return { content: [ { type: "text", text: `Playlist created successfully!\nName: ${data.name}\nID: ${data.id}\nURL: ${data.external_urls.spotify}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Failed to create playlist: ${error.message}`, }, ], isError: true, }; } } ); // Add tracks to playlist server.tool( "add-tracks-to-playlist", { playlistId: z.string().describe("The Spotify playlist ID"), trackUris: z .array(z.string()) .describe("Array of Spotify track URIs to add"), }, async ({ playlistId, trackUris }) => { try { const accessToken = await getValidAccessToken(); const response = await fetch( `https://api.spotify.com/v1/playlists/${playlistId}/tracks`, { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ uris: trackUris, }), } ); const data = await response.json(); if (!response.ok) { return { content: [ { type: "text", text: `Error adding tracks: ${JSON.stringify(data)}`, }, ], isError: true, }; } return { content: [ { type: "text", text: `Successfully added ${trackUris.length} track(s) to playlist!`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Failed to add tracks: ${error.message}`, }, ], isError: true, }; } } ); // Get recommendations server.tool( "get-recommendations", { seedTracks: z .array(z.string()) .max(5) .describe("Spotify track IDs to use as seeds (max 5)"), limit: z .number() .min(1) .max(100) .default(20) .describe("Number of recommendations to return"), }, async ({ seedTracks, limit }) => { try { const accessToken = await getValidAccessToken(); if (seedTracks.length === 0) { return { content: [ { type: "text", text: "Error: At least one seed track is required", }, ], isError: true, }; } const response = await fetch( `https://api.spotify.com/v1/recommendations?seed_tracks=${seedTracks.join( "," )}&limit=${limit}`, { headers: { Authorization: `Bearer ${accessToken}`, }, } ); const data = await response.json(); if (!response.ok) { return { content: [ { type: "text", text: `Error getting recommendations: ${JSON.stringify(data)}`, }, ], isError: true, }; } const tracks = data.tracks.map((track) => ({ id: track.id, name: track.name, artist: track.artists.map((artist) => artist.name).join(", "), album: track.album.name, uri: track.uri, })); return { content: [ { type: "text", text: JSON.stringify(tracks, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Failed to get recommendations: ${error.message}`, }, ], isError: true, }; } } ); return server; } // Create Express app const app = express(); app.use(express.json()); // CORS configuration app.use( cors({ origin: "*", // Configure this for production exposedHeaders: ["Mcp-Session-Id"], allowedHeaders: ["Content-Type", "mcp-session-id"], }) ); // Health check endpoint app.get("/health", (req, res) => { res.json({ status: "healthy", timestamp: new Date().toISOString() }); }); // MCP endpoint (stateless mode) app.post("/mcp", async (req, res) => { try { // Create new server instance for each request (stateless) const server = createSpotifyMcpServer(); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, // No session management }); // Clean up when request closes res.on("close", () => { console.log("Request closed, cleaning up"); transport.close(); server.close(); }); // Connect server to transport await server.connect(transport); // Handle the request await transport.handleRequest(req, res, req.body); } catch (error) { console.error("Error handling MCP request:", error); if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error", }, id: null, }); } } }); // Start the server const PORT = process.env.PORT || 8080; app.listen(PORT, () => { console.log(`Spotify MCP Server running on port ${PORT}`); console.log(`Health check: http://localhost:${PORT}/health`); console.log(`MCP endpoint: http://localhost:${PORT}/mcp`); console.log(`Mode: STATELESS`); });

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/hrishi0102/spotifyyy-mcp'

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