diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..70b4b36 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run lint + - run: npm run build + - run: npm test diff --git a/BLUEPRINT.md b/BLUEPRINT.md deleted file mode 100644 index 7e97f56..0000000 --- a/BLUEPRINT.md +++ /dev/null @@ -1,528 +0,0 @@ -Here is a comprehensive, architectural blueprint for building a Spotify MCP server. This is designed for a "one-shot" implementation, meaning you can follow these steps linearly to get a fully functional server. - -### **Project Overview** -* **Language:** TypeScript (Node.js) -* **Protocol:** Model Context Protocol (MCP) via `@modelcontextprotocol/sdk` -* **Architecture:** - * **Auth Service:** A standalone script to perform the initial OAuth Handshake and save tokens locally. - * **API Handler:** An Axios instance with interceptors to handle automatic token refreshing. - * **MCP Server:** Exposes Spotify API endpoints as executable Tools for the LLM. - ---- - -### **Phase 1: Project Initialization & Quality Control** - -#### 1. File Structure -Create the following directory structure: -```text -spotify-mcp/ -├── src/ -│ ├── auth/ -│ │ ├── setup.ts <-- Initial auth script -│ │ └── token-manager.ts <-- Handles reading/writing/refreshing tokens -│ ├── tools/ -│ │ └── spotify-api.ts <-- Maps MCP tools to Spotify Endpoints -│ └── index.ts <-- Main Entry point -├── package.json -├── tsconfig.json -├── eslint.config.mjs -└── .env -``` - -#### 2. `package.json` (Dependencies) -```json -{ - "name": "spotify-mcp-server", - "version": "1.0.0", - "type": "module", - "scripts": { - "build": "tsc", - "start": "node dist/index.js", - "auth": "tsx src/auth/setup.ts", - "lint": "eslint src/**/*.ts" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.1", - "axios": "^1.7.0", - "dotenv": "^16.4.5", - "zod": "^3.23.8" - }, - "devDependencies": { - "@eslint/js": "^9.0.0", - "@types/node": "^20.0.0", - "eslint": "^9.0.0", - "globals": "^15.0.0", - "tsx": "^4.7.0", - "typescript": "^5.4.0", - "typescript-eslint": "^8.0.0", - "open": "^10.1.0", - "express": "^4.19.0", - "@types/express": "^4.17.0" - } -} -``` - -#### 3. `tsconfig.json` (Strict Typing) -```json -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*"] -} -``` - -#### 4. `eslint.config.mjs` (Modern Flat Config) -```javascript -import globals from "globals"; -import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; - -export default [ - { files: ["**/*.ts"] }, - { languageOptions: { globals: globals.node } }, - pluginJs.configs.recommended, - ...tseslint.configs.recommended, - { - rules: { - "@typescript-eslint/no-unused-vars": "warn", - "@typescript-eslint/no-explicit-any": "warn", - "no-console": ["warn", { allow: ["error", "info"] }] - } - } -]; -``` - ---- - -### **Phase 2: Authentication Infrastructure** - -We need a way to get tokens initially and a way to keep them alive. - -#### 1. `.env` Setup -Create a `.env` file. You must get these from the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard). -```ini -SPOTIFY_CLIENT_ID=your_client_id_here -SPOTIFY_CLIENT_SECRET=your_client_secret_here -SPOTIFY_REDIRECT_URI=http://localhost:8888/callback -``` - -#### 2. `src/auth/token-manager.ts` -This handles storage and automatic refreshing. - -```typescript -import fs from 'fs/promises'; -import path from 'path'; -import axios from 'axios'; -import dotenv from 'dotenv'; - -dotenv.config(); - -const TOKEN_PATH = path.join(process.cwd(), 'spotify-tokens.json'); - -interface TokenData { - access_token: string; - refresh_token: string; - expires_at: number; -} - -export class TokenManager { - private tokenData: TokenData | null = null; - - async loadTokens(): Promise { - try { - const data = await fs.readFile(TOKEN_PATH, 'utf-8'); - this.tokenData = JSON.parse(data); - return this.tokenData; - } catch (error) { - return null; - } - } - - async saveTokens(data: any) { - const tokens: TokenData = { - access_token: data.access_token, - refresh_token: data.refresh_token || this.tokenData?.refresh_token, - // Calculate expiry (current time + expires_in seconds - buffer) - expires_at: Date.now() + (data.expires_in * 1000) - }; - - this.tokenData = tokens; - await fs.writeFile(TOKEN_PATH, JSON.stringify(tokens, null, 2)); - } - - async getValidAccessToken(): Promise { - if (!this.tokenData) await this.loadTokens(); - if (!this.tokenData) throw new Error("No tokens found. Run 'npm run auth' first."); - - // Check if expired (with 1 minute buffer) - if (Date.now() > this.tokenData.expires_at - 60000) { - await this.refreshAccessToken(); - } - - return this.tokenData.access_token; - } - - private async refreshAccessToken() { - console.error("Refreshing Spotify Access Token..."); - try { - const params = new URLSearchParams(); - params.append('grant_type', 'refresh_token'); - params.append('refresh_token', this.tokenData!.refresh_token); - - const authHeader = Buffer.from( - `${process.env.SPOTIFY_CLIENT_ID}:${process.env.SPOTIFY_CLIENT_SECRET}` - ).toString('base64'); - - const response = await axios.post('https://accounts.spotify.com/api/token', params, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${authHeader}` - } - }); - - await this.saveTokens(response.data); - } catch (error) { - console.error("Failed to refresh token. You may need to re-authenticate."); - throw error; - } - } -} - -export const tokenManager = new TokenManager(); -``` - -#### 3. `src/auth/setup.ts` (The "One-Time" Script) -Run this script (`npm run auth`) to log in via the browser. - -```typescript -import express from 'express'; -import open from 'open'; -import dotenv from 'dotenv'; -import axios from 'axios'; -import { tokenManager } from './token-manager.js'; - -dotenv.config(); - -const app = express(); -const port = 8888; - -const SCOPES = [ - 'user-read-playback-state', - 'user-modify-playback-state', - 'user-read-currently-playing' -].join(' '); - -app.get('/login', (req, res) => { - const params = new URLSearchParams({ - response_type: 'code', - client_id: process.env.SPOTIFY_CLIENT_ID!, - scope: SCOPES, - redirect_uri: process.env.SPOTIFY_REDIRECT_URI!, - }); - res.redirect(`https://accounts.spotify.com/authorize?${params.toString()}`); -}); - -app.get('/callback', async (req, res) => { - const code = req.query.code as string; - if (!code) { - res.send('No code provided'); - return; - } - - try { - const params = new URLSearchParams(); - params.append('code', code); - params.append('redirect_uri', process.env.SPOTIFY_REDIRECT_URI!); - params.append('grant_type', 'authorization_code'); - - const authHeader = Buffer.from( - `${process.env.SPOTIFY_CLIENT_ID}:${process.env.SPOTIFY_CLIENT_SECRET}` - ).toString('base64'); - - const response = await axios.post('https://accounts.spotify.com/api/token', params, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${authHeader}` - } - }); - - await tokenManager.saveTokens(response.data); - res.send('Authentication successful! You can close this window and start the MCP server.'); - process.exit(0); - } catch (error) { - console.error(error); - res.send('Authentication failed check console.'); - } -}); - -app.listen(port, async () => { - console.log(`Auth server running on http://localhost:${port}`); - await open(`http://localhost:${port}/login`); -}); -``` - ---- - -### **Phase 3: API Implementation** - -#### `src/tools/spotify-api.ts` -This file contains the wrapper logic that fetches the token and executes the request. - -```typescript -import axios, { AxiosRequestConfig } from 'axios'; -import { tokenManager } from '../auth/token-manager.js'; - -const BASE_URL = 'https://api.spotify.com/v1'; - -/** - * Generic helper to make authenticated requests to Spotify - */ -export async function spotifyRequest( - method: 'GET' | 'POST' | 'PUT', - endpoint: string, - data?: any, - params?: any -): Promise { - const token = await tokenManager.getValidAccessToken(); - - const config: AxiosRequestConfig = { - method, - url: `${BASE_URL}${endpoint}`, - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - params, - data - }; - - try { - const response = await axios(config); - return response.data; - } catch (error: any) { - if (error.response) { - // Clean error messaging for the LLM - const status = error.response.status; - const msg = error.response.data?.error?.message || error.message; - - if (status === 403) throw new Error(`Spotify Forbidden: ${msg} (Is user Premium?)`); - if (status === 404) throw new Error(`Spotify Not Found: ${msg} (Is a device active?)`); - - throw new Error(`Spotify API Error ${status}: ${msg}`); - } - throw error; - } -} -``` - ---- - -### **Phase 4: The MCP Server (`index.ts`)** - -This ties everything together. It defines the tools using Zod schemas and delegates to the `spotifyRequest` helper. - -```typescript -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { z } from "zod"; -import { spotifyRequest } from "./tools/spotify-api.js"; - -// Initialize Server -const server = new McpServer({ - name: "spotify-mcp-server", - version: "1.0.0", -}); - -// --- 1. Get Playback State --- -server.tool( - "spotify_get_playback_state", - "Get information about the user's current playback state, including track, progress, and active device.", - { - market: z.string().optional().describe("ISO 3166-1 alpha-2 country code"), - }, - async ({ market }) => { - const data = await spotifyRequest("GET", "/me/player", undefined, { market }); - if (!data) return { content: [{ type: "text", text: "No active playback found." }] }; - return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; - } -); - -// --- 2. Get Currently Playing Track --- -server.tool( - "spotify_get_current_track", - "Get the object currently being played on the user's Spotify account.", - { - market: z.string().optional(), - }, - async ({ market }) => { - const data = await spotifyRequest("GET", "/me/player/currently-playing", undefined, { market }); - if (!data) return { content: [{ type: "text", text: "Nothing currently playing." }] }; - return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; - } -); - -// --- 3. Start/Resume Playback --- -server.tool( - "spotify_start_resume_playback", - "Start a new context or resume current playback on the user's active device.", - { - device_id: z.string().optional().describe("The id of the device to target."), - context_uri: z.string().optional().describe("Spotify URI of the context to play (album, artist, playlist)."), - uris: z.array(z.string()).optional().describe("Array of track URIs to play."), - position_ms: z.number().optional().describe("Position in ms to start."), - }, - async ({ device_id, context_uri, uris, position_ms }) => { - const body: any = {}; - if (context_uri) body.context_uri = context_uri; - if (uris) body.uris = uris; - if (position_ms !== undefined) body.position_ms = position_ms; - - await spotifyRequest("PUT", "/me/player/play", body, { device_id }); - return { content: [{ type: "text", text: "Playback started/resumed." }] }; - } -); - -// --- 4. Pause Playback --- -server.tool( - "spotify_pause_playback", - "Pause playback on the user's account.", - { - device_id: z.string().optional(), - }, - async ({ device_id }) => { - await spotifyRequest("PUT", "/me/player/pause", undefined, { device_id }); - return { content: [{ type: "text", text: "Playback paused." }] }; - } -); - -// --- 5. Skip to Next --- -server.tool( - "spotify_skip_next", - "Skips to next track in the user's queue.", - { - device_id: z.string().optional(), - }, - async ({ device_id }) => { - await spotifyRequest("POST", "/me/player/next", undefined, { device_id }); - return { content: [{ type: "text", text: "Skipped to next track." }] }; - } -); - -// --- 6. Skip to Previous --- -server.tool( - "spotify_skip_previous", - "Skips to previous track in the user's queue.", - { - device_id: z.string().optional(), - }, - async ({ device_id }) => { - await spotifyRequest("POST", "/me/player/previous", undefined, { device_id }); - return { content: [{ type: "text", text: "Skipped to previous track." }] }; - } -); - -// --- 7. Set Repeat Mode --- -server.tool( - "spotify_set_repeat_mode", - "Set the repeat mode for the user's playback.", - { - state: z.enum(["track", "context", "off"]).describe("track, context, or off"), - device_id: z.string().optional(), - }, - async ({ state, device_id }) => { - await spotifyRequest("PUT", "/me/player/repeat", undefined, { state, device_id }); - return { content: [{ type: "text", text: `Repeat mode set to ${state}.` }] }; - } -); - -// --- 8. Set Volume --- -server.tool( - "spotify_set_volume", - "Set the volume for the user's current playback device.", - { - volume_percent: z.number().min(0).max(100).describe("Volume 0-100"), - device_id: z.string().optional(), - }, - async ({ volume_percent, device_id }) => { - await spotifyRequest("PUT", "/me/player/volume", undefined, { volume_percent, device_id }); - return { content: [{ type: "text", text: `Volume set to ${volume_percent}%.` }] }; - } -); - -// --- 9. Get Queue --- -server.tool( - "spotify_get_queue", - "Get the list of objects that make up the user's queue.", - {}, - async () => { - const data = await spotifyRequest("GET", "/me/player/queue"); - return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; - } -); - -// Start the server -async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Spotify MCP Server running on stdio"); -} - -main().catch((error) => { - console.error("Server error:", error); - process.exit(1); -}); -``` - ---- - -### **Phase 5: Execution Instructions** - -#### 1. Build the Server -```bash -npm install -npm run build -``` - -#### 2. Authenticate -You must run this once (and occasionally if your refresh token is revoked). -```bash -npm run auth -``` -* This will open your browser. -* Login to Spotify. -* Wait for the "Authentication successful" message. -* This creates `spotify-tokens.json` in your project root. - -#### 3. Configure Claude Desktop (or other MCP client) -Add this to your `claude_desktop_config.json`: - -```json -{ - "mcpServers": { - "spotify": { - "command": "node", - "args": ["/ABSOLUTE/PATH/TO/spotify-mcp/dist/index.js"], - "env": { - "SPOTIFY_CLIENT_ID": "your_client_id", - "SPOTIFY_CLIENT_SECRET": "your_client_secret" - } - } - } -} -``` - -#### 4. Usage -Restart Claude. You can now ask: -* "What song is playing?" -* "Turn the volume down to 50%" -* "Queue 'Bohemian Rhapsody'" (Note: You might need to create a `search` tool for this, or use the URIs directly if you know them, but `get_queue` is fully implemented). -* "Pause the music." diff --git a/TESTING.md b/TESTING.md deleted file mode 100644 index 2f0dff9..0000000 --- a/TESTING.md +++ /dev/null @@ -1,324 +0,0 @@ -# Test Suite Documentation - -This document describes the comprehensive test suite for the Spotify MCP Server. - -## Overview - -The test suite validates all 51 MCP tools and core functionality **without requiring network access** or actual Spotify API credentials. All external dependencies are mocked to ensure tests run reliably in any environment. - -## Test Statistics - -- **Total Tests:** 94 tests across 5 test files -- **Test Files:** 5 -- **Coverage:** All 51 tools validated -- **Execution Time:** ~2.5 seconds -- **Success Rate:** 100% (94/94 passing) - -## Test Structure - -``` -tests/ -├── __mocks__/ -│ ├── spotify-data.ts # Mock Spotify API responses -│ └── axios-mock.ts # Mock Axios HTTP client -├── unit/ -│ ├── spotify-api.test.ts # Utility functions (formatDuration, calculateProgress) -│ ├── token-manager.test.ts # OAuth token management logic -│ └── api-wrapper.test.ts # API error handling and request construction -└── integration/ - ├── tool-definitions.test.ts # All 51 tool definitions and schemas - └── schema-validation.test.ts # Parameter validation edge cases -``` - -## Test Categories - -### 1. Unit Tests (28 tests) - -#### Spotify API Utilities (9 tests) -- **File:** `tests/unit/spotify-api.test.ts` -- **Tests:** - - `formatDuration()` - Converts milliseconds to M:SS or H:MM:SS format - - Sub-1-minute durations (0:30, 0:59) - - Sub-1-hour durations (3:42, 10:00) - - Over-1-hour durations (1:00:00, 2:03:04) - - Edge cases (0ms, 1ms, 999ms) - - Zero-padding validation - - `calculateProgress()` - Calculates percentage from current/total ms - - Standard percentages (0%, 50%, 100%) - - Rounding behavior (33.33% → 33%) - - Edge cases (0/0, divide by zero) - - Real track duration examples - -#### Token Manager (8 tests) -- **File:** `tests/unit/token-manager.test.ts` -- **Tests:** - - Token data structure validation - - Expiry time calculation - - 1-minute refresh buffer logic - - Token refresh request format - - Authorization header construction - - Refresh token preservation during updates - -#### API Wrapper (11 tests) -- **File:** `tests/unit/api-wrapper.test.ts` -- **Tests:** - - HTTP error handling: - - 401 Unauthorized → "Re-run npm run auth" - - 403 Forbidden → "Premium required" - - 404 Not Found → "Is device active?" - - 429 Rate Limit → "Too many requests" - - Missing error messages - - Request construction for GET, PUT, POST, DELETE - - Base URL validation - - Endpoint URL construction - -### 2. Integration Tests (66 tests) - -#### Tool Definitions (37 tests) -- **File:** `tests/integration/tool-definitions.test.ts` -- **Tests:** - - **Total count:** 51 tools verified - - **Bulk State Tools (2):** - - `spotify_get_playback_state` - - `spotify_get_current_track` - - **Granular Track Info Tools (16):** - - Track name, artist name, all artists, album name - - Duration (ms & formatted), progress (ms & formatted), time remaining (ms & formatted) - - Progress percentage, track URI/ID, album art URL - - Explicit flag, popularity score - - **Granular Device Tools (6):** - - Device name, type, volume, ID - - Active status check - - Available devices list - - **Granular Playback State Tools (5):** - - Is playing, shuffle state, repeat mode - - Context type, context URI - - **Playback Control Tools (7):** - - Play, pause, next, previous, seek - - Set shuffle, set repeat - - **Search & Discovery Tools (5):** - - Search, track info, album info, artist info - - Artist top tracks - - **Library Management Tools (5):** - - Get/save/remove saved tracks - - Get playlists, get specific playlist - - **Queue Management Tools (3):** - - Get queue, add to queue, recently played - - **Volume & Device Control Tools (2):** - - Set volume, transfer playback - - **Naming conventions:** - - All tools start with `spotify_` - - Consistent verb patterns (get_, is_, set_) - - No duplicates, no typos - -#### Schema Validation (29 tests) -- **File:** `tests/integration/schema-validation.test.ts` -- **Tests:** - - **Volume schema:** 0-100 range, rejects negative/over 100 - - **Repeat mode:** Accepts 'track', 'context', 'off', rejects invalid - - **Shuffle:** Boolean only, rejects strings/numbers - - **Search:** Query string + type array + optional limit - - **Play:** Optional device_id, context_uri, uris, position_ms - - **Seek:** Required position_ms - - **Pagination:** Optional limit/offset - - **ID parameters:** track_id, album_id, artist_id, playlist_id - - **URI parameters:** Spotify URIs (spotify:track:*, etc.) - - **Market codes:** ISO 3166-1 alpha-2 (US, GB, JP) - - **Transfer playback:** Required device_id, optional play boolean - - **Empty schemas:** All granular getter tools - -## Mock Data - -### Spotify API Responses -All mock data is realistic and based on actual Spotify API responses: - -- **Mock Track:** "Mr. Brightside" by The Killers - - Duration: 222,973ms (3:42) - - Album: "Hot Fuss" - - Explicit: false - - Popularity: 87 - -- **Mock Device:** "My MacBook Pro" - - Type: Computer - - Volume: 75% - - Active: true - -- **Mock Playback State:** - - Currently playing: Mr. Brightside - - Progress: 89,523ms (40% through) - - Shuffle: off - - Repeat: off - - Context: Album - -- **Additional Mock Data:** - - Queue with multiple tracks - - Multiple devices (MacBook, iPhone, Speaker) - - Search results - - Recently played tracks - - Saved tracks and playlists - - Artist and album info - -## Running Tests - -### Run All Tests -```bash -npm test -``` - -### Watch Mode (Re-run on file changes) -```bash -npm run test:watch -``` - -### Coverage Report -```bash -npm run test:coverage -``` - -### Run Specific Test File -```bash -npx vitest tests/unit/spotify-api.test.ts -``` - -## Test Design Principles - -### 1. No Network Access Required -- All HTTP requests are mocked -- No actual Spotify API calls -- No file system dependencies (except test files themselves) -- Tests run entirely in-memory - -### 2. Fast Execution -- Total suite runs in ~2.5 seconds -- No timeouts or delays -- Parallel test execution - -### 3. Deterministic Results -- No flaky tests -- No random data -- Same results every run -- No environment dependencies - -### 4. Comprehensive Coverage -- All 51 tools validated -- All error codes tested -- All parameter combinations -- Edge cases covered - -### 5. Realistic Mocks -- Mock data based on real Spotify API -- Realistic track durations, IDs, URIs -- Actual response structures - -## What's Tested - -### ✅ Fully Tested -- All 51 tool definitions and names -- All Zod schemas and parameter validation -- Utility functions (formatDuration, calculateProgress) -- Error handling for all HTTP status codes -- Token expiration logic -- Request construction for all HTTP methods -- Volume range validation (0-100) -- Repeat mode enum validation -- Boolean parameter validation -- ID/URI parameter formats -- Pagination parameter handling - -### ⚠️ Requires Manual Testing -The following require actual Spotify credentials and active devices: -- OAuth authentication flow (browser-based) -- Actual API calls to Spotify -- Token refresh with real tokens -- Device discovery -- Playback control on real devices -- Premium-only features -- Rate limiting behavior - -## Continuous Integration - -These tests are designed to run in CI/CD pipelines: - -```yaml -# Example GitHub Actions workflow -- name: Run Tests - run: | - npm install - npm test -``` - -No environment variables or secrets required! - -## Test Maintenance - -### Adding New Tools -When adding a new tool: -1. Add mock data to `tests/__mocks__/spotify-data.ts` if needed -2. Add tool name to `tests/integration/tool-definitions.test.ts` -3. Add schema tests to `tests/integration/schema-validation.test.ts` -4. Update expected counts in test files -5. Run `npm test` to verify - -### Updating Existing Tools -When modifying a tool: -1. Update affected test assertions -2. Update mock data if response format changes -3. Run `npm test` to catch regressions - -## Test Output Example - -``` -✓ tests/unit/api-wrapper.test.ts (11 tests) 6ms -✓ tests/unit/token-manager.test.ts (8 tests) 5ms -✓ tests/integration/tool-definitions.test.ts (37 tests) 29ms -✓ tests/integration/schema-validation.test.ts (29 tests) 28ms -✓ tests/unit/spotify-api.test.ts (9 tests) 5ms - -Test Files 5 passed (5) - Tests 94 passed (94) - Start at 03:47:09 - Duration 2.49s -``` - -## Troubleshooting - -### Tests Fail After Updates -1. Check if tool count changed (update `tool-definitions.test.ts`) -2. Verify Zod schemas match implementation -3. Check mock data structure matches API responses - -### Slow Test Execution -- Tests should run in < 5 seconds -- If slower, check for: - - Actual network calls (should be mocked) - - File I/O operations (should be mocked) - - Accidental timeouts - -### Import Errors -- Ensure all test files use `.js` extensions for imports -- Verify `vitest.config.ts` is properly configured -- Check `package.json` has `"type": "module"` - -## Coverage Goals - -Current coverage: -- **Tool Definitions:** 100% (51/51 tools) -- **Schemas:** 100% (all parameters validated) -- **Utility Functions:** 100% -- **Error Handling:** 100% (all error codes) -- **Token Logic:** 95% (core logic, excluding file I/O) - -## Future Enhancements - -Potential test improvements: -- [ ] E2E tests with real Spotify sandbox account -- [ ] Performance benchmarks for tool execution -- [ ] Load testing for concurrent requests -- [ ] Visual regression tests for auth UI -- [ ] Mutation testing for code coverage validation - ---- - -**All tests passing! 🎉** - -The Spotify MCP Server is production-ready with comprehensive test coverage. diff --git a/package-lock.json b/package-lock.json index 6e8221c..a4ce0cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1625,6 +1625,7 @@ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -1999,6 +2000,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2650,6 +2652,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2877,6 +2880,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -4657,6 +4661,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4715,6 +4720,7 @@ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -4761,6 +4767,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4843,6 +4850,7 @@ "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -4936,6 +4944,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4949,6 +4958,7 @@ "integrity": "sha512-QSD4I0fN6uZQfftryIXuqvqgBxTvJ3ZNkF6RWECd82YGAYAfhcppBLFXzXJHQAAhVFyYEuFTrq6h0hQqjB7jIQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.13", "@vitest/mocker": "4.0.13", @@ -5120,6 +5130,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }