Skip to main content
Glama

Spotify Streamable MCP Server

by iceener
auth.ts5.54 kB
import { config } from "../config/env.ts"; import { SpotifyTokenResponseCodec } from "../types/spotify.codecs.ts"; import { logger } from "../utils/logger.ts"; import { getCurrentSessionId, getCurrentSpotifyAccessToken, } from "./context.ts"; import { getSession } from "./session.ts"; import { createHttpClient } from "../services/http-client.ts"; import { apiBase } from "../utils/spotify.ts"; const http = createHttpClient({ baseHeaders: { "Content-Type": "application/json", "User-Agent": `mcp-spotify/${config.MCP_VERSION}`, }, rateLimit: { rps: 5, burst: 10 }, timeout: 10000, // Shorter timeout for validation retries: 0, // No retries for validation }); /** * Validates a Spotify access token by making a lightweight API call * @param accessToken The access token to validate * @returns true if token is valid, false if expired/invalid */ export async function validateSpotifyToken( accessToken: string ): Promise<boolean> { try { const base = apiBase(config.SPOTIFY_API_URL); const response = await http(new URL("me", base).toString(), { headers: { Authorization: `Bearer ${accessToken}` }, signal: AbortSignal.timeout(5000), // Quick validation }); // 200 OK means token is valid return response.status === 200; } catch (error) { await logger.warning("auth", { message: "Token validation failed", error: (error as Error).message, }); return false; } } export async function getUserBearer(): Promise<string | null> { // Linear-style: prefer per-request token passed via context from the Worker const fromContext = getCurrentSpotifyAccessToken(); if (fromContext && fromContext.trim()) { return fromContext; } const sessionId = getCurrentSessionId(); if (!sessionId) return null; const session = getSession(sessionId); const token = session?.spotify?.access_token ?? null; const expiresAt = session?.spotify?.expires_at ?? 0; const refreshToken = session?.spotify?.refresh_token; if (!token) return null; if (refreshToken && Date.now() > expiresAt - 30_000) { const tokenUrl = new URL( "/api/token", config.SPOTIFY_ACCOUNTS_URL ).toString(); const body = new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, }).toString(); // Minimal, bounded retry with jittered backoff const maxAttempts = 2; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { if (!config.SPOTIFY_CLIENT_ID || !config.SPOTIFY_CLIENT_SECRET) { await logger.warning("auth", { message: "Missing Spotify client credentials; cannot refresh access token", }); // If still valid, prefer returning the current token; otherwise fail return Date.now() <= expiresAt ? token : null; } const basic = Buffer.from( `${config.SPOTIFY_CLIENT_ID}:${config.SPOTIFY_CLIENT_SECRET}` ).toString("base64"); const resp = await fetch(tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", Authorization: `Basic ${basic}`, }, body, signal: AbortSignal.timeout(10_000), }); if (resp.ok) { const parsed = SpotifyTokenResponseCodec.safeParse(await resp.json()); if (!parsed.success) { await logger.warning("auth", { message: "Silent refresh returned invalid payload; using cached token", issues: parsed.error.issues.map((i) => ({ path: i.path, code: i.code, })), }); return token; } const data = parsed.data; const newAccess = data.access_token || token; const newRt = data.refresh_token ?? refreshToken; const newExp = Date.now() + Number(data.expires_in ?? 3600) * 1000; const scopes = String(data.scope || "") .split(" ") .filter(Boolean); if (session?.spotify) { session.spotify.access_token = newAccess; session.spotify.refresh_token = newRt; session.spotify.expires_at = newExp; session.spotify.scopes = scopes.length ? scopes : session.spotify.scopes; } return newAccess; } // Non-OK: retry only on transient statuses const status = resp.status; const shouldRetry = status >= 500 || status === 429; await logger.warning("auth", { message: "Silent refresh HTTP non-OK", status, statusText: resp.statusText, attempt, }); if (!shouldRetry || attempt === maxAttempts) { return Date.now() <= expiresAt ? token : null; } const backoffMs = 300 * attempt + Math.floor(Math.random() * 200); await new Promise((r) => setTimeout(r, backoffMs)); } catch (error) { await logger.warning("auth", { message: "Silent refresh attempt failed", error: (error as Error)?.message, attempt, }); if (attempt === maxAttempts) { return Date.now() <= expiresAt ? token : null; } const backoffMs = 300 * attempt + Math.floor(Math.random() * 200); await new Promise((r) => setTimeout(r, backoffMs)); } } } return token ?? null; }

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/iceener/spotify-streamable-mcp-server'

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