Skip to main content
Glama

Spotify Streamable MCP Server

by iceener
spec.md15.4 kB
## Spotify MCP Server Code Standards (TypeScript · Node/Bun · Hono · Zod · UUID) This document defines the rules we strictly follow to keep the codebase readable, maintainable, reliable, and pragmatic. The philosophy is functional-first: pure logic at the core, side effects at the edges. ### Core Principles - Purity first: business logic is composed of small, pure functions. Perform I/O (HTTP, FS, env, time) at the edges. - Immutability by default: avoid mutation; use `readonly` arrays/records and return new values. - Composition over inheritance: prefer function composition and modules over classes and deep hierarchies. - Explicitness: explicit types, narrow interfaces, descriptive names. No magic, no implicit behavior. - Defensive boundaries: validate all external inputs at module boundaries with Zod; never trust runtime data. - YAGNI and small surface area: build only what is necessary; remove dead or demo code promptly. ## TypeScript Rules ### Strict typing - `tsconfig.json` uses strict mode; do not weaken it. - Do not use `any`. Prefer `unknown` at boundaries, then narrow via Zod or TypeScript guards. - Prefer `type` aliases for unions/intersections; use `interface` only for extensible object shapes. - Use `readonly` in arrays/tuples/object properties for intent and safety. - Model precise domain types. Avoid over-generic types that hide meaning. ### Names and structure - Functions are verbs; values are nouns. Avoid abbreviations. Prefer clarity over brevity. - Exported APIs must have full, explicit signatures. Avoid optional output shapes. - Co-locate types with their modules if scope is local; lift only shared, stable types. ### Error typing - Treat `unknown` errors explicitly. Convert to domain errors early. - Never throw strings. If throwing, throw `Error` (or a narrow subclass), and prefer returning typed Results at boundaries. ## Node/Bun Practices ### Runtime and modules - Use ESM and modern ES2022 features. Prefer `import`/`export` consistently. - Centralize environment access in `src/config/env.ts`. Do not read `process.env` elsewhere. - Avoid top-level I/O side effects. Wire apps in `src/index.ts` only. ### Async, timeouts, and cancellation - All I/O must have a timeout. Prefer `AbortSignal.timeout(ms)` or an `AbortController` passed through. - Parallelize independent work with `Promise.all`, never `await` sequentially in loops. - Propagate `AbortSignal` where possible. ### Logging - Use the central `logger` only. No `console.log` outside very early boot or final fatal exits. - Log structured objects with stable keys. Include `message`, `category`, and minimal context. ## Hono Guidelines ### Routing and handlers - One purpose per route handler. Validate inputs with Zod, then call pure services. - Prefer a global `app.onError` to normalize failures. If not present, ensure route handlers and middlewares normalize errors consistently. - Keep Hono `Context` (`c`) usage shallow: parse input, call service, format output. Example request validation and handler composition: ```ts import { z } from "zod"; import { Hono } from "hono"; const app = new Hono(); const SearchQuery = z.object({ q: z.string().min(1), limit: z.number().int().min(1).max(50).default(10), }); app.get("/search", async (c) => { const parsed = SearchQuery.safeParse({ q: c.req.query("q"), limit: Number(c.req.query("limit")) || undefined, }); if (!parsed.success) return c.json({ error: parsed.error.format() }, 400); const { q, limit } = parsed.data; const results = await searchService({ q, limit, signal: AbortSignal.timeout(10_000), }); return c.json({ ok: true, results }); }); app.onError((err, c) => c.json({ ok: false, error: err.message }, 500)); ``` ## Zod Schema Patterns - Define all external input/output with Zod. Co-locate schema with the module that owns it. - Prefer `z.enum([...])` for finite sets over `z.string()` with comments. - Use `.describe()` to document schema fields; this improves tool metadata and self-documentation. - For tool outputs, define a `z.object(...)` and export both the schema and `z.infer` type. Example of input/output schemas and type inference: ```ts import { z } from "zod"; export const SpotifySearchInput = z.object({ query: z.string().min(1).describe("Search query"), type: z.enum(["track", "album", "artist"]).default("track"), limit: z.number().int().min(1).max(50).default(10), }); export type SpotifySearchInput = z.infer<typeof SpotifySearchInput>; export const SpotifySearchResult = z.object({ items: z.array( z.object({ id: z.string(), name: z.string(), uri: z.string() }) ), count: z.number().int(), }); export type SpotifySearchResult = z.infer<typeof SpotifySearchResult>; ``` ## UUID Usage - Prefer `crypto.randomUUID()` in Node 20+ for simplicity. If using the `uuid` package, use v4 for non-ordered, collision-resistant IDs. - Generate IDs at the edges (e.g., when creating new records). Do not generate IDs for transient in-memory values unless necessary. - Treat incoming IDs as untrusted strings; validate with a regex or Zod (`z.string().uuid()`). Do not parse without validation. ```ts import { randomUUID } from "node:crypto"; const id = randomUUID(); // RFC 4122 v4 ``` ## Error Handling and Results - Validate early, fail fast, and return structured errors. Prefer returning Result-like objects at module boundaries. - When integrating with MCP tools, set `isError: true` on failures and include a concise human-readable `content` along with structured details. ```ts import type { CallToolResult } from "@modelcontextprotocol/sdk/types"; export function toToolError(message: string): CallToolResult { return { isError: true, content: [{ type: "text", text: message }] }; } ``` ## Concurrency, Timeouts, and Retries - Default network timeout: 10–30s depending on endpoint. No unbounded awaits. - Parallelize independent calls via `Promise.all`. Never fire-and-forget; always await or explicitly detach with reasoning. - Retries: use limited retries with backoff only for idempotent operations. No retries on 4xx unless specified. ```ts const [tracks, artists] = await Promise.all([ fetchTracks(q, { signal }), fetchArtists(q, { signal }), ]); ``` ## MCP Integration Rules (Tools/Resources/Prompts) - Tools must define `name`, `description`, `inputSchema`, and `outputSchema`. Prefer returning `structuredContent` that matches the `outputSchema`. - Errors must set `isError: true` and include `content` with a short message; avoid leaking internals. - Pagination: cap list sizes and provide cursors. Avoid sending large payloads. - Resources should be read-only and cheap. Tools may have side effects; declare annotations accurately (`readOnlyHint`, `openWorldHint`). Example tool registration: ```ts server.registerTool( spotifySearchTool.name, { description: spotifySearchTool.description, inputSchema: SpotifySearchInput.shape, outputSchema: SpotifySearchResult.shape, annotations: { title: "Spotify Search", readOnlyHint: true, openWorldHint: true, }, }, spotifySearchTool.handler ); ``` ## Security Baselines - Validate all inputs from clients, env, and external APIs. Never trust strings. - Redact secrets in logs. Never log tokens, cookies, or Authorization headers. - Enforce CORS/origin checks for HTTP surfaces where applicable. Bind to localhost for local servers. - Limit payload sizes and concurrency to prevent resource exhaustion. ## Performance and Pragmatism - Avoid premature optimization and caching. Measure first; optimize only hotspots. - Keep modules small. Split when files exceed reasonable cognitive load. - Prefer streaming or pagination for large data. ## Typical Mistakes to Avoid - Using `any` or implicit `any`, weakening strictness, or bypassing type errors instead of fixing design. - Throwing raw values or catching and swallowing errors, resulting in silent failures. - Performing input validation deep inside logic rather than at the edges. - Mutating inputs, shared state, or global singletons; causing hidden coupling. - Mixing concerns: embedding transport (Hono/MCP) logic into business logic modules. - Sequential `await` in loops when work is independent; ignoring timeouts and cancellation. - Logging unstructured strings, secrets, or excessive data; hindering observability. - Overengineering generic abstractions and utility layers before real use cases exist. - Creating classes for simple data transformations that are better expressed as pure functions. - Skipping `outputSchema` on MCP tools or returning shapes that diverge from schemas. ## Implementation Checklist - Inputs validated with Zod at boundaries; outputs typed and validated where practical. - Pure functions for core logic; side effects isolated in thin adapters. - Timeouts and `AbortSignal` wired through I/O calls; no unbounded awaits. - Structured logging with stable keys; no console noise. - MCP tools register `inputSchema` and `outputSchema`; errors set `isError: true`. - No usage of `any`; minimal `unknown` with prompt narrowing. - No dead code; remove scaffolding and examples not in use. Adhering to these rules yields predictable, testable, and maintainable code with minimal surprises. ## Project Scripts Use Bun for all tasks. Scripts are defined in `package.json`: ```json { "scripts": { "dev": "bun --hot src/index.ts", // Run with HMR for local dev "prestart": "bun run build", // Ensure build before start "start": "bun run dist/index.js", // Run compiled server "build": "bun build src/index.ts --outdir dist --target bun", "lint": "biome check .", // Lint + format check (no writes) "lint:fix": "biome check --write .", // Apply safe lint fixes "format": "biome format --write .", // Apply formatting "format:check": "biome format .", // Check formatting only "typecheck": "tsc --noEmit", // TypeScript type checking "check": "bun run format:check && bun run lint && bun run typecheck", // Format check + lint + typecheck "validate": "bun run format && bun run lint:fix && bun run typecheck", "clean": "rm -rf dist", // Clean build artifacts "test": "bun test", // Unit/integration tests "test:client": "bun run scripts/test-client.ts" // Minimal MCP client harness } } ``` Notes: - Always run `bun run check` before committing. CI should invoke `validate`. - `start` runs the built artifact; `dev` runs TS with HMR. ## Linting/Formatting Policy - Biome handles formatting and linting. Keep rules pragmatic to avoid churn. - Avoid `any` where feasible; warnings are acceptable when interoperating with SDK internals. - Prefer optional chaining over non-null assertions; where unavoidable, document why. ## Readability & Style (Addendum) - Favor clarity over brevity. Prefer vertical whitespace between logical blocks (imports, config, decoding, mapping, return) and between unrelated statements. - Line wrapping: keep lines under ~100 chars; split complex expressions across lines. - No inline unknown casts. Avoid `(x as unknown as T)` and ad-hoc casts on JSON. Decode external JSON using Zod codecs located in `src/types/spotify.codecs.ts` and work with inferred types. - Extract mappers and validators. Do not inline large mapping logic inside tools; use `src/utils/mappers.ts` and small helpers. - Inputs/outputs: schemas live next to their owner module; tools import and call `.parse(...)` at the boundary. Avoid creating “schema-like objects” that aren’t `z.object`. - Naming: functions are verbs; data are nouns. Avoid abbreviations. Use descriptive variable names (e.g., `playlistDetailsResponse` vs `json`). ### Naming conventions - Avoid single-letter identifiers for variables, parameters, and functions. Exceptions: conventional loop indexes (`i`, `j`) inside short, local loops only. - Prefer explicit descriptive names (`response`, `devicesResponse`, `playlistTracks`, `normalizedTracks`). ## Error Handling Policy (Addendum) - Always check Response status. Treat 204 as a successful empty response where expected; otherwise require `response.ok`. - Map common Spotify errors into stable tool error codes and human messages: - 401 → `unauthorized` (ask user to authenticate) - 403 → `forbidden` (scope/premium restrictions) - 429 → `rate_limited` (respect `Retry-After` when present) - other non-2xx → `bad_response` - Tool failures MUST set `isError: true` and include a concise `content` message plus `structuredContent` with `{ code, error, ... }`. - Do not parse JSON before validating `response.ok` or handling the 204 case. - Prefer a shared helper to enforce status checks uniformly (e.g., `expectOkOr204(response, context)`), used by all tools/services. ## Functional Modularity (Addendum) - Replace stateful class-style services with pure modules that accept explicit dependencies and return functions. Keep transient caches (like app-token) in thin closures only. - Split Spotify integration by domain: - Implemented: `catalog.ts` (search), `player.ts` (status/control) - Pending extraction (currently implemented directly inside tools): playlists (list/get/items/create/update/add/remove/reorder), library (tracks_get/add/remove/contains) - Tools should validate inputs with Zod, call a single pure service function, and shape the `structuredContent` output. - Centralize user-token retrieval in `src/core/auth.ts` (e.g., `getUserBearer()`), imported by tools/services. ## Input/Output Schemas (Addendum) - Wrap tool inputs in `z.object({...})`. Avoid exporting a plain object of field schemas. - Call `InputSchema.parse(args)` at tool boundaries. - For outputs, prefer `OutputSchema.parse(structured)` in development to catch drift early (guard by NODE_ENV). ## Cancellation & Timeouts (Addendum) - Continue enforcing per-request timeouts in the HTTP client. - Accept and propagate an optional `AbortSignal` through service functions into `fetch` for MCP-triggered cancellation. ## Environment & Security (Addendum) - No direct `process.env` outside `src/config/env.ts`. Add any new env like `ALLOWED_ORIGINS` to `env.ts` and consume via `config`. - Validate Origins via a centralized utility that reads from `config.ALLOWED_ORIGINS` (comma-separated allowlist). Keep permissive localhost defaults in development only. - A dedicated MCP security middleware guards `/mcp` routes; CORS is applied globally via middleware. - Structured content can optionally be echoed into textual `content` when `SPOTIFY_MCP_INCLUDE_JSON_IN_CONTENT=true`. This is implemented for `search_catalog`, `player_status`, and `spotify_library`. `spotify_playlist` currently emits concise text only (structured payload is always returned via `structuredContent`). ## Refactor Acceptance Criteria (Addendum) - All Spotify tools: - Validate inputs with Zod `z.object` schemas - Use centralized `getUserBearer()` - Check `response.ok` / handle 204 before JSON - Map 401/403/429 to stable error codes; set `isError: true` on failures - Return `structuredContent` conforming to output schemas - No `process.env` outside `env.ts`. - New functional service modules introduced as described; tools become thin orchestrators. Temporary exception: `spotify_playlist` and `spotify_library` still perform HTTP calls inline until their services are extracted.

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