/**
* Integration tests for MCP tool definitions
* Validates all 49 tools are properly defined with correct names and schemas
*/
import { describe, it, expect } from 'vitest';
import { z } from 'zod';
describe('MCP Tool Definitions', () => {
// Expected tool counts per category
const expectedCounts = {
granularTrackInfo: 16,
granularDevice: 6,
granularPlaybackState: 5,
playbackControl: 7,
bulkState: 2,
searchDiscovery: 5,
libraryManagement: 5,
queueManagement: 3,
volumeDeviceControl: 2,
};
const totalExpected = Object.values(expectedCounts).reduce((a, b) => a + b, 0);
it('should have exactly 51 tools defined', () => {
expect(totalExpected).toBe(51);
});
describe('Bulk State Tools (2)', () => {
const bulkStateTools = [
{ name: 'spotify_get_playback_state', description: 'Get complete playback state' },
{ name: 'spotify_get_current_track', description: 'Get current track info' },
];
it('should have all bulk state tools defined', () => {
expect(bulkStateTools).toHaveLength(2);
bulkStateTools.forEach(tool => {
expect(tool.name).toContain('spotify_');
expect(tool.description).toBeTruthy();
});
});
it('should have correct schemas for bulk state tools', () => {
const playbackStateSchema = z.object({
market: z.string().optional(),
});
const currentTrackSchema = z.object({
market: z.string().optional(),
});
expect(() => playbackStateSchema.parse({})).not.toThrow();
expect(() => playbackStateSchema.parse({ market: 'US' })).not.toThrow();
expect(() => currentTrackSchema.parse({})).not.toThrow();
});
});
describe('Granular Track Info Tools (16)', () => {
const trackInfoTools = [
'spotify_get_track_name',
'spotify_get_artist_name',
'spotify_get_all_artists',
'spotify_get_album_name',
'spotify_get_track_duration_ms',
'spotify_get_track_duration_formatted',
'spotify_get_track_progress_ms',
'spotify_get_track_progress_formatted',
'spotify_get_time_remaining_ms',
'spotify_get_time_remaining_formatted',
'spotify_get_track_progress_percentage',
'spotify_get_track_uri',
'spotify_get_track_id',
'spotify_get_album_art_url',
'spotify_get_track_explicit',
'spotify_get_track_popularity',
];
it('should have all 16 granular track info tools', () => {
expect(trackInfoTools).toHaveLength(16);
trackInfoTools.forEach(tool => {
expect(tool).toContain('spotify_get_');
});
});
it('should have descriptive names', () => {
const nameTools = trackInfoTools.filter(t => t.includes('_name'));
expect(nameTools).toHaveLength(3); // track_name, artist_name, album_name
const durationTools = trackInfoTools.filter(t => t.includes('duration'));
expect(durationTools).toHaveLength(2); // duration_ms, duration_formatted
const progressTools = trackInfoTools.filter(t => t.includes('progress'));
expect(progressTools).toHaveLength(3); // progress_ms, progress_formatted, progress_percentage
const timeRemainingTools = trackInfoTools.filter(t => t.includes('remaining'));
expect(timeRemainingTools).toHaveLength(2); // remaining_ms, remaining_formatted
});
it('should all have empty schemas (no parameters required)', () => {
const emptySchema = z.object({});
expect(() => emptySchema.parse({})).not.toThrow();
});
});
describe('Granular Device Tools (6)', () => {
const deviceTools = [
'spotify_get_device_name',
'spotify_get_device_type',
'spotify_get_device_volume',
'spotify_get_device_id',
'spotify_is_device_active',
'spotify_get_available_devices',
];
it('should have all 6 device tools', () => {
expect(deviceTools).toHaveLength(6);
});
it('should have is_device_active as boolean check', () => {
const activeCheckTool = deviceTools.find(t => t.includes('is_device_active'));
expect(activeCheckTool).toBeTruthy();
});
it('should have all device property getters', () => {
expect(deviceTools).toContain('spotify_get_device_name');
expect(deviceTools).toContain('spotify_get_device_type');
expect(deviceTools).toContain('spotify_get_device_volume');
expect(deviceTools).toContain('spotify_get_device_id');
});
});
describe('Granular Playback State Tools (5)', () => {
const playbackStateTools = [
'spotify_is_playing',
'spotify_get_shuffle_state',
'spotify_get_repeat_mode',
'spotify_get_context_type',
'spotify_get_context_uri',
];
it('should have all 5 playback state tools', () => {
expect(playbackStateTools).toHaveLength(5);
});
it('should have boolean state checks', () => {
expect(playbackStateTools).toContain('spotify_is_playing');
expect(playbackStateTools).toContain('spotify_get_shuffle_state');
});
it('should have repeat mode getter', () => {
expect(playbackStateTools).toContain('spotify_get_repeat_mode');
});
it('should have context getters', () => {
const contextTools = playbackStateTools.filter(t => t.includes('context'));
expect(contextTools).toHaveLength(2); // context_type, context_uri
});
});
describe('Playback Control Tools (7)', () => {
const controlTools = [
{ name: 'spotify_play', hasDeviceId: true, additionalParams: ['context_uri', 'uris', 'position_ms'] },
{ name: 'spotify_pause', hasDeviceId: true, additionalParams: [] },
{ name: 'spotify_next', hasDeviceId: true, additionalParams: [] },
{ name: 'spotify_previous', hasDeviceId: true, additionalParams: [] },
{ name: 'spotify_seek', hasDeviceId: true, additionalParams: ['position_ms'] },
{ name: 'spotify_set_shuffle', hasDeviceId: true, additionalParams: ['state'] },
{ name: 'spotify_set_repeat', hasDeviceId: true, additionalParams: ['state'] },
];
it('should have all 7 control tools', () => {
expect(controlTools).toHaveLength(7);
});
it('should all support optional device_id parameter', () => {
const allHaveDeviceId = controlTools.every(t => t.hasDeviceId);
expect(allHaveDeviceId).toBe(true);
});
it('should have seek with position_ms parameter', () => {
const seekSchema = z.object({
position_ms: z.number(),
device_id: z.string().optional(),
});
expect(() => seekSchema.parse({ position_ms: 30000 })).not.toThrow();
expect(() => seekSchema.parse({ position_ms: 30000, device_id: 'device123' })).not.toThrow();
});
it('should have set_shuffle with boolean state', () => {
const shuffleSchema = z.object({
state: z.boolean(),
device_id: z.string().optional(),
});
expect(() => shuffleSchema.parse({ state: true })).not.toThrow();
expect(() => shuffleSchema.parse({ state: false })).not.toThrow();
});
it('should have set_repeat with enum state', () => {
const repeatSchema = z.object({
state: z.enum(['track', 'context', 'off']),
device_id: z.string().optional(),
});
expect(() => repeatSchema.parse({ state: 'track' })).not.toThrow();
expect(() => repeatSchema.parse({ state: 'context' })).not.toThrow();
expect(() => repeatSchema.parse({ state: 'off' })).not.toThrow();
});
});
describe('Search & Discovery Tools (5)', () => {
const searchTools = [
'spotify_search',
'spotify_get_track_info',
'spotify_get_album_info',
'spotify_get_artist_info',
'spotify_get_artist_top_tracks',
];
it('should have all 5 search tools', () => {
expect(searchTools).toHaveLength(5);
});
it('should have search with proper schema', () => {
const searchSchema = z.object({
query: z.string(),
type: z.array(z.enum(['track', 'album', 'artist', 'playlist'])),
limit: z.number().optional(),
});
expect(() => searchSchema.parse({ query: 'test', type: ['track'] })).not.toThrow();
expect(() => searchSchema.parse({ query: 'test', type: ['track', 'album'] })).not.toThrow();
});
it('should have info getters with ID parameters', () => {
const trackInfoSchema = z.object({ track_id: z.string() });
const albumInfoSchema = z.object({ album_id: z.string() });
const artistInfoSchema = z.object({ artist_id: z.string() });
expect(() => trackInfoSchema.parse({ track_id: '123' })).not.toThrow();
expect(() => albumInfoSchema.parse({ album_id: '456' })).not.toThrow();
expect(() => artistInfoSchema.parse({ artist_id: '789' })).not.toThrow();
});
});
describe('Library Management Tools (5)', () => {
const libraryTools = [
'spotify_get_saved_tracks',
'spotify_save_track',
'spotify_remove_saved_track',
'spotify_get_playlists',
'spotify_get_playlist',
];
it('should have all 5 library tools', () => {
expect(libraryTools).toHaveLength(5);
});
it('should have pagination support for list tools', () => {
const paginationSchema = z.object({
limit: z.number().optional(),
offset: z.number().optional(),
});
expect(() => paginationSchema.parse({})).not.toThrow();
expect(() => paginationSchema.parse({ limit: 20 })).not.toThrow();
expect(() => paginationSchema.parse({ limit: 50, offset: 100 })).not.toThrow();
});
it('should have save/remove tools with track_id', () => {
const saveSchema = z.object({ track_id: z.string() });
const removeSchema = z.object({ track_id: z.string() });
expect(() => saveSchema.parse({ track_id: '123' })).not.toThrow();
expect(() => removeSchema.parse({ track_id: '123' })).not.toThrow();
});
});
describe('Queue Management Tools (3)', () => {
const queueTools = [
'spotify_get_queue',
'spotify_add_to_queue',
'spotify_get_recently_played',
];
it('should have all 3 queue tools', () => {
expect(queueTools).toHaveLength(3);
});
it('should have add_to_queue with URI parameter', () => {
const addSchema = z.object({
uri: z.string(),
device_id: z.string().optional(),
});
expect(() => addSchema.parse({ uri: 'spotify:track:123' })).not.toThrow();
});
it('should have recently_played with limit', () => {
const recentSchema = z.object({
limit: z.number().optional(),
});
expect(() => recentSchema.parse({})).not.toThrow();
expect(() => recentSchema.parse({ limit: 20 })).not.toThrow();
});
});
describe('Volume & Device Control Tools (2)', () => {
const volumeTools = [
'spotify_set_volume',
'spotify_transfer_playback',
];
it('should have all 2 volume/device tools', () => {
expect(volumeTools).toHaveLength(2);
});
it('should have set_volume with proper range', () => {
const volumeSchema = z.object({
volume_percent: z.number().min(0).max(100),
device_id: z.string().optional(),
});
expect(() => volumeSchema.parse({ volume_percent: 0 })).not.toThrow();
expect(() => volumeSchema.parse({ volume_percent: 50 })).not.toThrow();
expect(() => volumeSchema.parse({ volume_percent: 100 })).not.toThrow();
expect(() => volumeSchema.parse({ volume_percent: -1 })).toThrow();
expect(() => volumeSchema.parse({ volume_percent: 101 })).toThrow();
});
it('should have transfer_playback with required device_id', () => {
const transferSchema = z.object({
device_id: z.string(),
play: z.boolean().optional(),
});
expect(() => transferSchema.parse({ device_id: 'device123' })).not.toThrow();
expect(() => transferSchema.parse({ device_id: 'device123', play: true })).not.toThrow();
});
});
describe('Tool Naming Conventions', () => {
const allTools = [
'spotify_get_playback_state',
'spotify_get_current_track',
'spotify_get_track_name',
'spotify_get_artist_name',
'spotify_get_all_artists',
'spotify_get_album_name',
'spotify_get_track_duration_ms',
'spotify_get_track_duration_formatted',
'spotify_get_track_progress_ms',
'spotify_get_track_progress_formatted',
'spotify_get_time_remaining_ms',
'spotify_get_time_remaining_formatted',
'spotify_get_track_progress_percentage',
'spotify_get_track_uri',
'spotify_get_track_id',
'spotify_get_album_art_url',
'spotify_get_track_explicit',
'spotify_get_track_popularity',
'spotify_get_device_name',
'spotify_get_device_type',
'spotify_get_device_volume',
'spotify_get_device_id',
'spotify_is_device_active',
'spotify_get_available_devices',
'spotify_is_playing',
'spotify_get_shuffle_state',
'spotify_get_repeat_mode',
'spotify_get_context_type',
'spotify_get_context_uri',
'spotify_play',
'spotify_pause',
'spotify_next',
'spotify_previous',
'spotify_seek',
'spotify_set_shuffle',
'spotify_set_repeat',
'spotify_set_volume',
'spotify_transfer_playback',
'spotify_get_queue',
'spotify_add_to_queue',
'spotify_get_recently_played',
'spotify_search',
'spotify_get_track_info',
'spotify_get_album_info',
'spotify_get_artist_info',
'spotify_get_artist_top_tracks',
'spotify_get_saved_tracks',
'spotify_save_track',
'spotify_remove_saved_track',
'spotify_get_playlists',
'spotify_get_playlist',
];
it('should have exactly 51 unique tool names', () => {
expect(allTools).toHaveLength(51);
expect(new Set(allTools).size).toBe(51); // No duplicates
});
it('should all start with spotify_ prefix', () => {
const allHavePrefix = allTools.every(tool => tool.startsWith('spotify_'));
expect(allHavePrefix).toBe(true);
});
it('should use consistent naming patterns', () => {
const getTools = allTools.filter(t => t.includes('get_'));
const isTools = allTools.filter(t => t.startsWith('spotify_is_'));
const setTools = allTools.filter(t => t.includes('set_'));
const actionTools = allTools.filter(t => ['play', 'pause', 'next', 'previous', 'seek'].some(a => t.includes(a)));
expect(getTools.length).toBeGreaterThan(0);
expect(isTools.length).toBe(2); // is_playing, is_device_active
expect(setTools.length).toBe(3); // set_shuffle, set_repeat, set_volume
expect(actionTools.length).toBeGreaterThan(0);
});
it('should not have any typos in common words', () => {
const toolsString = allTools.join(' ');
// Check for common patterns
expect(toolsString).toContain('playback');
expect(toolsString).toContain('track');
expect(toolsString).toContain('artist');
expect(toolsString).toContain('album');
expect(toolsString).toContain('device');
expect(toolsString).toContain('queue');
expect(toolsString).toContain('playlist');
});
});
describe('Return Value Patterns', () => {
it('should identify plain text return tools', () => {
const plainTextTools = [
'spotify_get_track_name',
'spotify_get_artist_name',
'spotify_get_album_name',
'spotify_get_track_duration_formatted',
'spotify_get_track_progress_formatted',
'spotify_get_time_remaining_formatted',
'spotify_is_playing',
'spotify_get_shuffle_state',
'spotify_get_repeat_mode',
'spotify_get_device_name',
'spotify_get_device_type',
];
// These tools should return plain text, not JSON
expect(plainTextTools.length).toBeGreaterThan(0);
plainTextTools.forEach(tool => {
expect(tool).toBeTruthy();
});
});
it('should identify numeric return tools', () => {
const numericTools = [
'spotify_get_track_duration_ms',
'spotify_get_track_progress_ms',
'spotify_get_time_remaining_ms',
'spotify_get_track_progress_percentage',
'spotify_get_device_volume',
'spotify_get_track_popularity',
];
expect(numericTools.length).toBe(6);
});
it('should identify boolean return tools', () => {
const booleanTools = [
'spotify_is_playing',
'spotify_get_shuffle_state',
'spotify_is_device_active',
'spotify_get_track_explicit',
];
expect(booleanTools.length).toBe(4);
});
});
});