Skip to main content
Glama

Spotify Streamable MCP Server

by iceener
worker.ts26.6 kB
/* Minimal MCP server over Streamable HTTP for Cloudflare Workers Parity with linear Worker: 401 challenges, PKCE OAuth, RS mapping, and tools. */ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { Router } from 'itty-router'; import { type ZodTypeAny, z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { deleteCode, deleteTransaction, getRecordByRsRefreshToken, getSpotifyTokensByRsAccessToken, getTransaction, getTxnIdByCode, saveCode, saveTransaction, setAuthStoreEnv, storeRsTokenMapping, updateSpotifyTokensByRsRefreshToken, } from './auth/store.ts'; import { serverMetadata } from './config/metadata.ts'; import { runWithRequestContext } from './core/context.ts'; import { ensureSession } from './core/session.ts'; import { registerTools } from './tools/index.ts'; const MCP_ENDPOINT_PATH = '/mcp'; function getProtocolVersion(): string { const v = (globalThis as unknown as { process?: { env?: Record<string, unknown> } }) ?.process?.env?.MCP_PROTOCOL_VERSION as string | undefined; return v || '2025-06-18'; } type ToolRecord = { name: string; title?: string; description?: string; inputSchema: Record<string, unknown>; handler: (args: Record<string, unknown>) => Promise<{ content?: Array<unknown>; structuredContent?: unknown; isError?: boolean; }>; }; const tools: Record<string, ToolRecord> = {}; type RegisterSchema = { description?: string; inputSchema: Record<string, unknown> | ZodTypeAny; annotations?: { title?: string }; }; type RegisterHandler = (args: unknown) => Promise<unknown>; const adapter: { registerTool: ( name: string, schema: RegisterSchema, handler: RegisterHandler, ) => void; } = { registerTool(name, schema, handler) { function toJsonSchema(input: unknown): Record<string, unknown> { try { if ( input && typeof input === 'object' && ('$schema' in (input as Record<string, unknown>) || 'type' in (input as Record<string, unknown>)) ) { return input as Record<string, unknown>; } const isZod = input && typeof input === 'object' && input !== null && '_def' in (input as Record<string, unknown>); if (isZod) { const json = zodToJsonSchema(input as ZodTypeAny, { $refStrategy: 'none', }); return json as unknown as Record<string, unknown>; } if (input && typeof input === 'object') { const values = Object.values(input as Record<string, unknown>); const looksLikeShape = values.length > 0 && values.every( (v) => v && typeof v === 'object' && '_def' in (v as Record<string, unknown>), ); if (looksLikeShape) { const obj = z.object(input as Record<string, ZodTypeAny>); const json = zodToJsonSchema(obj, { $refStrategy: 'none' }); return json as unknown as Record<string, unknown>; } } } catch {} return (input ?? {}) as Record<string, unknown>; } const wrappedHandler: ToolRecord['handler'] = async (args) => { const result = await handler(args); return result as { content?: Array<unknown>; structuredContent?: unknown; isError?: boolean; }; }; tools[name] = { name, title: schema.annotations?.title, description: schema.description, inputSchema: toJsonSchema(schema.inputSchema), handler: wrappedHandler, }; }, }; registerTools(adapter as unknown as McpServer); function ok(id: string | number, result: unknown): Response { return new Response(JSON.stringify({ jsonrpc: '2.0', id, result }), { headers: { 'content-type': 'application/json; charset=utf-8' }, }); } function error( id: string | number | undefined, code: number, message: string, ): Response { return new Response( JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }), { headers: { 'content-type': 'application/json; charset=utf-8' }, }, ); } function withCors(resp: Response): Response { const headers = new Headers(resp.headers); headers.set('Access-Control-Allow-Origin', '*'); headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); headers.set( 'Access-Control-Allow-Headers', 'Content-Type, Accept, Authorization, MCP-Protocol-Version, Mcp-Session-Id', ); headers.set('Access-Control-Expose-Headers', 'WWW-Authenticate, Mcp-Session-Id'); headers.set('Access-Control-Max-Age', '86400'); return new Response(resp.body, { status: resp.status, headers }); } function isAllowedRedirectUri(uri: string): boolean { try { const env = (globalThis as unknown as { process?: { env?: Record<string, unknown> } }) ?.process?.env ?? {}; const allowAll = String(env.OAUTH_REDIRECT_ALLOW_ALL || 'false').toLowerCase() === 'true'; if (allowAll) return true; const allowRaw = String(env.OAUTH_REDIRECT_ALLOWLIST || ''); const allowed = new Set( allowRaw .split(',') .map((v) => v.trim()) .filter(Boolean) .concat([String(env.OAUTH_REDIRECT_URI || 'alice://oauth/callback')]), ); const u = new URL(uri); const isDev = String(env.NODE_ENV || 'development') === 'development'; if (isDev) { const loopback = new Set(['localhost', '127.0.0.1', '::1']); if (loopback.has(u.hostname)) return true; } return ( allowed.has(`${u.protocol}//${u.host}${u.pathname}`) || allowed.has(u.toString()) ); } catch { return false; } } const router = Router(); router.options(MCP_ENDPOINT_PATH, async () => withCors(new Response(null, { status: 204 })), ); router.post(MCP_ENDPOINT_PATH, async (request: Request) => { const headerRecord: Record<string, string> = {}; request.headers.forEach((value, key) => { headerRecord[String(key).toLowerCase()] = String(value); }); const incomingSid = request.headers.get('Mcp-Session-Id'); const sid = incomingSid && incomingSid.trim() ? incomingSid : crypto.randomUUID(); try { ensureSession(sid); } catch {} const challenge = (origin: string): Response => { const resourceMd = `${origin}/.well-known/oauth-protected-resource?sid=${encodeURIComponent( sid, )}`; const resp = new Response( JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Unauthorized' }, id: null, }), { status: 401 }, ); resp.headers.set( 'WWW-Authenticate', `Bearer realm="MCP", authorization_uri="${resourceMd}"`, ); resp.headers.set('Mcp-Session-Id', sid); return withCors(resp); }; const env = (globalThis as unknown as { process?: { env?: Record<string, unknown> } })?.process ?.env ?? {}; const authEnabled = String(env.AUTH_ENABLED || 'false').toLowerCase() === 'true'; const requireRs = String(env.AUTH_REQUIRE_RS || 'false').toLowerCase() === 'true'; const allowProviderBearer = String( (env as Record<string, unknown>).AUTH_ALLOW_DIRECT_BEARER || (env as Record<string, unknown>).AUTH_ALLOW_LINEAR_BEARER || 'false', ) .toString() .toLowerCase() === 'true'; const authHeaderIn = headerRecord.authorization; const apiKeyHeader = headerRecord['x-api-key'] || headerRecord['x-auth-token']; if (authEnabled && !authHeaderIn && !apiKeyHeader) { const origin = new URL(request.url).origin; return challenge(origin); } let rsMapped = false; let bearer: string | undefined; if (authHeaderIn) { const m = authHeaderIn.match(/^\s*Bearer\s+(.+)$/i); bearer = m?.[1]; if (bearer) { try { const mapped = await getSpotifyTokensByRsAccessToken(bearer); if (mapped?.access_token) { headerRecord.authorization = `Bearer ${mapped.access_token}`; rsMapped = true; } } catch {} } } if (authEnabled && requireRs && bearer && !rsMapped && !allowProviderBearer) { const origin = new URL(request.url).origin; return challenge(origin); } return runWithRequestContext( { sessionId: sid, spotifyAccessToken: rsMapped && headerRecord.authorization ? headerRecord.authorization.replace(/^\s*Bearer\s+/i, '') : undefined, }, async () => { const raw = await request.text(); const payload = (raw ? JSON.parse(raw) : {}) as { jsonrpc?: string; id?: string | number; method?: string; params?: Record<string, unknown>; }; if (payload?.jsonrpc !== '2.0' || typeof payload.method !== 'string') { return withCors(new Response('Bad Request', { status: 400 })); } const { id, method, params } = payload; if (!('id' in payload) || typeof id === 'undefined') { return withCors(new Response(null, { status: 202 })); } if (method === 'initialize') { return withCors( ok(id, { protocolVersion: getProtocolVersion(), capabilities: { tools: { listChanged: true } }, serverInfo: { name: serverMetadata.title, title: serverMetadata.title, version: (env.MCP_VERSION as string) || '0.1.0', }, instructions: serverMetadata.instructions, }), ); } if (method === 'tools/list') { const list = Object.values(tools).map((t) => ({ name: t.name, title: t.title, description: t.description, inputSchema: t.inputSchema, })); return withCors(ok(id, { tools: list })); } if (method === 'resources/list') { return withCors(ok(id, { resources: [] })); } if (method === 'prompts/list') { return withCors(ok(id, { prompts: [] })); } if (method === 'tools/call') { const nameValue = (params as Record<string, unknown> | undefined)?.name; const name = typeof nameValue === 'string' ? nameValue : undefined; const argsValue = (params as Record<string, unknown> | undefined)?.arguments; const args = typeof argsValue === 'object' && argsValue !== null && !Array.isArray(argsValue) ? (argsValue as Record<string, unknown>) : ({} as Record<string, unknown>); if (!name || !tools[name]) { return withCors(error(id, -32602, `Unknown tool: ${String(name)}`)); } try { const tool = tools[name]; const result = await tool?.handler(args); return withCors(ok(id, result)); } catch (e) { return withCors( ok(id, { isError: true, content: [{ type: 'text', text: `Tool failed: ${(e as Error).message}` }], }), ); } } return withCors(error(id, -32601, `Method not found: ${method}`)); }, ); }); router.get(MCP_ENDPOINT_PATH, async () => withCors(new Response('Method Not Allowed', { status: 405 })), ); router.get('/health', async () => withCors( new Response(JSON.stringify({ status: 'ok' }), { headers: { 'content-type': 'application/json; charset=utf-8' }, }), ), ); router.get('/.well-known/oauth-authorization-server', async (request: Request) => { const base = new URL(request.url).origin; const env = (globalThis as unknown as { process?: { env?: Record<string, unknown> } })?.process ?.env ?? {}; const scopes = String(env.OAUTH_SCOPES || '') .split(/\s+/) .map((s) => s.trim()) .filter(Boolean); return withCors( new Response( JSON.stringify({ issuer: base, authorization_endpoint: `${base}/authorize`, token_endpoint: `${base}/token`, registration_endpoint: `${base}/register`, response_types_supported: ['code'], grant_types_supported: ['authorization_code', 'refresh_token'], code_challenge_methods_supported: ['S256'], token_endpoint_auth_methods_supported: ['none'], scopes_supported: scopes.length ? scopes : ['mcp'], }), { headers: { 'content-type': 'application/json; charset=utf-8' } }, ), ); }); router.get('/.well-known/oauth-protected-resource', async (request: Request) => { const here = new URL(request.url); const base = here.origin; const sid = here.searchParams.get('sid') ?? undefined; const resourceBase = `${base}${MCP_ENDPOINT_PATH}`; const resourceUrl = (() => { try { if (!sid) return resourceBase; const u = new URL(resourceBase); u.searchParams.set('sid', sid); return u.toString(); } catch { return resourceBase; } })(); return withCors( new Response( JSON.stringify({ authorization_servers: [`${base}/.well-known/oauth-authorization-server`], resource: resourceUrl, }), { headers: { 'content-type': 'application/json; charset=utf-8' } }, ), ); }); router.get('/authorize', async (request: Request) => { const url = new URL(request.url); const state = url.searchParams.get('state') ?? undefined; const codeChallenge = url.searchParams.get('code_challenge'); const codeChallengeMethod = url.searchParams.get('code_challenge_method'); const redirectUri = url.searchParams.get('redirect_uri'); const scope = url.searchParams.get('scope') ?? undefined; const sid = url.searchParams.get('sid') || request.headers.get('Mcp-Session-Id') || undefined; if (!redirectUri) { return withCors(new Response('invalid_request: redirect_uri', { status: 400 })); } if (!codeChallenge || codeChallengeMethod !== 'S256') { return withCors(new Response('invalid_request: pkce', { status: 400 })); } const here = new URL(request.url); const base = here.origin; const txnId = crypto.randomUUID(); await saveTransaction(txnId, { codeChallenge, state, scope, createdAt: Date.now(), }); const env = (globalThis as unknown as { process?: { env?: Record<string, unknown> } })?.process ?.env ?? {}; const clientId = (env.SPOTIFY_CLIENT_ID as string) || undefined; const accountsBase = (env.SPOTIFY_ACCOUNTS_URL as string) || 'https://accounts.spotify.com'; const oauthAuthUrl = (env.OAUTH_AUTHORIZATION_URL as string) || `${accountsBase}/authorize`; const scopeParam = String(env.OAUTH_SCOPES || scope || '') .split(/\s+/) .map((s) => s.trim()) .filter(Boolean) .join(' '); if (clientId) { const cb = new URL('/spotify/callback', base).toString(); const composite = btoa( JSON.stringify({ tid: txnId, cs: state, cr: redirectUri, sid }), ) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/g, ''); const authUrl = new URL(oauthAuthUrl); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('client_id', clientId); authUrl.searchParams.set('redirect_uri', cb); if (scopeParam) authUrl.searchParams.set('scope', scopeParam); if (composite) authUrl.searchParams.set('state', composite); return withCors(Response.redirect(authUrl.toString(), 302)); } const code = crypto.randomUUID(); await saveCode(code, txnId); const target = isAllowedRedirectUri(redirectUri) ? redirectUri : (env.OAUTH_REDIRECT_URI as string) || 'alice://oauth/callback'; const redirect = new URL(target); redirect.searchParams.set('code', code); if (state) redirect.searchParams.set('state', state); return withCors(Response.redirect(redirect.toString(), 302)); }); router.post('/register', async (request: Request) => { const base = new URL(request.url).origin; const now = Math.floor(Date.now() / 1000); const client_id = crypto.randomUUID(); const ct = request.headers.get('content-type') || ''; let body: Record<string, unknown> = {}; try { if (ct.includes('application/json')) { body = (await request.json()) as Record<string, unknown>; } else if (ct.includes('application/x-www-form-urlencoded')) { const form = new URLSearchParams(await request.text()); body = Object.fromEntries(form.entries()); } } catch {} const redirect_urisRaw = (body.redirect_uris as unknown) ?? []; const redirect_uris = Array.isArray(redirect_urisRaw) ? (redirect_urisRaw as unknown[]).filter((u) => typeof u === 'string') : typeof redirect_urisRaw === 'string' ? [redirect_urisRaw] : []; const token_endpoint_auth_method = (body.token_endpoint_auth_method as string) || 'none'; const grant_typesRaw = (body.grant_types as unknown) ?? undefined; const grant_types = Array.isArray(grant_typesRaw) ? (grant_typesRaw as unknown[]).filter((v) => typeof v === 'string') : ['authorization_code', 'refresh_token']; const response_typesRaw = (body.response_types as unknown) ?? undefined; const response_types = Array.isArray(response_typesRaw) ? (response_typesRaw as unknown[]).filter((v) => typeof v === 'string') : ['code']; const client_name = typeof body.client_name === 'string' ? (body.client_name as string) : undefined; return withCors( new Response( JSON.stringify({ client_id, client_id_issued_at: now, client_secret_expires_at: 0, token_endpoint_auth_method, registration_client_uri: `${base}/register/${client_id}`, registration_access_token: crypto.randomUUID(), redirect_uris, grant_types, response_types, ...(client_name ? { client_name } : {}), }), { headers: { 'content-type': 'application/json; charset=utf-8' } }, ), ); }); router.get('/spotify/callback', async (request: Request) => { try { const here = new URL(request.url); const code = here.searchParams.get('code'); const state = here.searchParams.get('state'); if (!code || !state) { return withCors(new Response('invalid_callback', { status: 400 })); } let decoded: { tid?: string; cs?: string; cr?: string; sid?: string } = {}; try { const padded = state.replace(/-/g, '+').replace(/_/g, '/'); decoded = JSON.parse(atob(padded)) as typeof decoded; } catch {} const txnId = decoded.tid || state; const txn = await getTransaction(txnId); if (!txn) { return withCors(new Response('unknown_txn', { status: 400 })); } const env = (globalThis as unknown as { process?: { env?: Record<string, unknown> } }) ?.process?.env ?? {}; const clientId = String(env.SPOTIFY_CLIENT_ID || ''); const clientSecret = String(env.SPOTIFY_CLIENT_SECRET || ''); const accountsBase = String( env.SPOTIFY_ACCOUNTS_URL || 'https://accounts.spotify.com', ); const tokenUrl = String(env.OAUTH_TOKEN_URL || `${accountsBase}/api/token`); if (!clientId || !clientSecret) { return withCors(new Response('missing_client', { status: 500 })); } const cb = new URL('/spotify/callback', here.origin).toString(); const basic = btoa(`${clientId}:${clientSecret}`); const form = new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: cb, }); const resp = await fetch(tokenUrl, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', authorization: `Basic ${basic}`, }, body: form.toString(), }); if (!resp.ok) { const t = await resp.text().catch(() => ''); return withCors( new Response(`spotify_token_error: ${resp.status} ${t}`.trim(), { status: 500, }), ); } const data = (await resp.json()) as { access_token?: string; refresh_token?: string; expires_in?: number | string; scope?: string; }; const access_token = String(data.access_token || ''); if (!access_token) { return withCors(new Response('spotify_no_token', { status: 500 })); } const scopes = String(data.scope || '') .split(' ') .filter(Boolean); const refresh_token = data.refresh_token as string | undefined; const expires_at = Date.now() + Number(data.expires_in ?? 3600) * 1000; const asCode = crypto.randomUUID(); await saveCode(asCode, txnId); await saveTransaction(txnId, { ...txn, spotify: { access_token, refresh_token, expires_at, scopes }, }); const clientRedirect = decoded.cr || String(env.OAUTH_REDIRECT_URI || 'alice://oauth/callback'); const safe = isAllowedRedirectUri(clientRedirect) ? clientRedirect : String(env.OAUTH_REDIRECT_URI || 'alice://oauth/callback'); if (!safe || String(safe).trim() === '') { return withCors(new Response('redirect_not_allowed', { status: 400 })); } const redirect = new URL(safe); redirect.searchParams.set('code', asCode); if (decoded.cs) redirect.searchParams.set('state', decoded.cs); const sid = decoded.sid; if (sid) { try { const s = ensureSession(sid); s.spotify = { access_token, refresh_token, expires_at, scopes }; } catch {} } return withCors(Response.redirect(redirect.toString(), 302)); } catch { return withCors(new Response('spotify_callback_error', { status: 500 })); } }); router.post('/token', async (request: Request) => { const contentType = request.headers.get('content-type') || ''; const params = contentType.includes('application/x-www-form-urlencoded') ? new URLSearchParams(await request.text()) : new URLSearchParams( (await request.json().catch(() => ({}))) as Record<string, string>, ); const grant = params.get('grant_type'); if (grant === 'refresh_token') { const rsRefresh = params.get('refresh_token') || ''; const rec = await getRecordByRsRefreshToken(rsRefresh); if (!rec) { return withCors( new Response(JSON.stringify({ error: 'invalid_grant' }), { status: 400, }), ); } const newAccess = crypto.randomUUID(); const updated = await updateSpotifyTokensByRsRefreshToken( rsRefresh, rec.spotify, newAccess, ); return withCors( new Response( JSON.stringify({ access_token: newAccess, refresh_token: rsRefresh, token_type: 'bearer', expires_in: 3600, scope: (updated?.spotify.scopes || []).join(' '), }), { headers: { 'content-type': 'application/json; charset=utf-8' } }, ), ); } if (grant !== 'authorization_code') { return withCors( new Response(JSON.stringify({ error: 'unsupported_grant_type' }), { status: 400, }), ); } const code = params.get('code') || ''; const codeVerifier = params.get('code_verifier') || ''; const txnId = await getTxnIdByCode(code); if (!txnId) { return withCors( new Response(JSON.stringify({ error: 'invalid_grant' }), { status: 400 }), ); } const txn = await getTransaction(txnId); if (!txn) { return withCors( new Response(JSON.stringify({ error: 'invalid_grant' }), { status: 400 }), ); } // Verify PKCE S256(code_verifier) equals stored codeChallenge const data = new TextEncoder().encode(codeVerifier); const digest = await crypto.subtle.digest('SHA-256', data); const bytes = new Uint8Array(digest); let binary = ''; for (const b of bytes) binary += String.fromCharCode(b); const base64 = btoa(binary) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/g, ''); if (txn.codeChallenge !== base64) { return withCors( new Response(JSON.stringify({ error: 'invalid_grant' }), { status: 400 }), ); } const rsAccess = crypto.randomUUID(); const rsRefresh = crypto.randomUUID(); const spotifyTokens = ( txn as unknown as { spotify?: { access_token: string; refresh_token?: string; expires_at?: number; scopes?: string[]; }; } ).spotify; if (spotifyTokens?.access_token) { await storeRsTokenMapping(rsAccess, spotifyTokens, rsRefresh); } await Promise.all([deleteTransaction(txnId), deleteCode(code)]); return withCors( new Response( JSON.stringify({ access_token: rsAccess, refresh_token: rsRefresh, token_type: 'bearer', expires_in: 3600, scope: (spotifyTokens?.scopes || []).join(' ') || txn.scope || String( (( globalThis as unknown as { process?: { env?: Record<string, unknown> }; } )?.process?.env?.OAUTH_SCOPES as string) || '', ).trim(), }), { headers: { 'content-type': 'application/json; charset=utf-8' } }, ), ); }); router.all('*', () => withCors(new Response('Not Found', { status: 404 }))); export default { fetch(request: Request, env?: Record<string, unknown>): Promise<Response> | Response { if (env) { const g = globalThis as unknown as { process?: { env?: Record<string, unknown> }; }; const existingEnv = (g.process?.env ?? {}) as Record<string, unknown>; g.process = g.process || {}; g.process.env = { ...existingEnv, ...(env as Record<string, unknown>) }; setAuthStoreEnv(env as Record<string, unknown>); } const url = new URL(request.url); // Forward POST / to /mcp for clients using base URL if (url.pathname === '/' && request.method.toUpperCase() === 'POST') { const forwarded = new Request( new URL(MCP_ENDPOINT_PATH, url).toString(), request, ); return router.handle(forwarded); } // Normalize duplicate slashes const normalizedPath = url.pathname.replace(/\/{2,}/g, '/'); if (normalizedPath !== url.pathname) { const normalizedUrl = new URL(request.url); normalizedUrl.pathname = normalizedPath; const normalizedRequest = new Request(normalizedUrl.toString(), request); return router.handle(normalizedRequest); } if (url.pathname === '/') { return withCors( new Response( JSON.stringify({ message: 'Spotify MCP Worker', endpoint: MCP_ENDPOINT_PATH, protocolVersion: getProtocolVersion(), }), { headers: { 'content-type': 'application/json; charset=utf-8' } }, ), ); } return router.handle(request); }, };

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