/**
* Integration tests for MCP Server
*
* These tests verify that the MCP server correctly handles
* tool listing and tool execution.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
// Mock the tool handlers
vi.mock('../../src/tools/analyze-music.js', () => ({
analyzeMusicTool: {
name: 'analyze_music',
description: 'Test description',
inputSchema: { type: 'object', properties: {}, required: [] },
},
handleAnalyzeMusic: vi.fn(),
}));
vi.mock('../../src/tools/separate-stems.js', () => ({
separateStemsTool: {
name: 'separate_stems',
description: 'Test description',
inputSchema: { type: 'object', properties: {}, required: [] },
},
handleSeparateStems: vi.fn(),
}));
vi.mock('../../src/tools/detect-ai-music.js', () => ({
detectAiMusicTool: {
name: 'detect_ai_music',
description: 'Test description',
inputSchema: { type: 'object', properties: {}, required: [] },
},
handleDetectAiMusic: vi.fn(),
}));
vi.mock('../../src/tools/analyze-loudness.js', () => ({
analyzeLoudnessTool: {
name: 'analyze_loudness',
description: 'Test description',
inputSchema: { type: 'object', properties: {}, required: [] },
},
handleAnalyzeLoudness: vi.fn(),
}));
vi.mock('../../src/tools/check-job-status.js', () => ({
checkJobStatusTool: {
name: 'check_job_status',
description: 'Test description',
inputSchema: { type: 'object', properties: {}, required: [] },
},
handleCheckJobStatus: vi.fn(),
}));
// Mock auth
vi.mock('../../src/utils/auth.js', () => ({
getApiKey: vi.fn(() => 'test-api-key'),
hasApiKey: vi.fn(() => true),
createAuthHeaders: vi.fn(() => ({})),
API_KEY_ENV_VAR: 'IRCAM_AMPLIFY_API_KEY',
}));
import { handleAnalyzeMusic } from '../../src/tools/analyze-music.js';
import { handleSeparateStems } from '../../src/tools/separate-stems.js';
import { handleDetectAiMusic } from '../../src/tools/detect-ai-music.js';
import { handleAnalyzeLoudness } from '../../src/tools/analyze-loudness.js';
import { handleCheckJobStatus } from '../../src/tools/check-job-status.js';
import { hasApiKey } from '../../src/utils/auth.js';
describe('MCP Server Integration', () => {
const originalEnv = process.env;
beforeEach(() => {
vi.clearAllMocks();
process.env = { ...originalEnv };
process.env.IRCAM_AMPLIFY_API_KEY = 'test-api-key';
});
afterEach(() => {
process.env = originalEnv;
vi.resetAllMocks();
});
describe('Tool Registration', () => {
it('should export all 5 tools', async () => {
// Import the tools
const { analyzeMusicTool } = await import('../../src/tools/analyze-music.js');
const { separateStemsTool } = await import('../../src/tools/separate-stems.js');
const { detectAiMusicTool } = await import('../../src/tools/detect-ai-music.js');
const { analyzeLoudnessTool } = await import('../../src/tools/analyze-loudness.js');
const { checkJobStatusTool } = await import('../../src/tools/check-job-status.js');
const tools = [
analyzeMusicTool,
separateStemsTool,
detectAiMusicTool,
analyzeLoudnessTool,
checkJobStatusTool,
];
expect(tools).toHaveLength(5);
tools.forEach((tool) => {
expect(tool).toHaveProperty('name');
expect(tool).toHaveProperty('description');
expect(tool).toHaveProperty('inputSchema');
});
});
it('should have unique tool names', async () => {
const { analyzeMusicTool } = await import('../../src/tools/analyze-music.js');
const { separateStemsTool } = await import('../../src/tools/separate-stems.js');
const { detectAiMusicTool } = await import('../../src/tools/detect-ai-music.js');
const { analyzeLoudnessTool } = await import('../../src/tools/analyze-loudness.js');
const { checkJobStatusTool } = await import('../../src/tools/check-job-status.js');
const names = [
analyzeMusicTool.name,
separateStemsTool.name,
detectAiMusicTool.name,
analyzeLoudnessTool.name,
checkJobStatusTool.name,
];
const uniqueNames = new Set(names);
expect(uniqueNames.size).toBe(5);
});
});
describe('Tool Execution Flow', () => {
it('should execute analyze_music and return result', async () => {
const mockResult = {
genre: ['electronic'],
mood: ['energetic'],
tempo: 128,
key: 'A minor',
instruments: ['synth'],
};
vi.mocked(handleAnalyzeMusic).mockResolvedValue(mockResult);
const result = await handleAnalyzeMusic({
audio_url: 'https://example.com/song.mp3',
});
expect(result).toEqual(mockResult);
});
it('should execute separate_stems and return job_id for async', async () => {
const mockResult = { job_id: 'job-123' };
vi.mocked(handleSeparateStems).mockResolvedValue(mockResult);
const result = await handleSeparateStems({
audio_url: 'https://example.com/song.mp3',
});
expect(result).toEqual({ job_id: 'job-123' });
});
it('should execute detect_ai_music and return classification', async () => {
const mockResult = {
confidence: 85,
classification: 'ai_generated' as const,
};
vi.mocked(handleDetectAiMusic).mockResolvedValue(mockResult);
const result = await handleDetectAiMusic({
audio_url: 'https://example.com/song.mp3',
});
expect(result.classification).toBe('ai_generated');
expect(result.confidence).toBe(85);
});
it('should execute analyze_loudness and return metrics', async () => {
const mockResult = {
integrated_lufs: -14.0,
true_peak_db: -1.0,
loudness_range: 6.0,
};
vi.mocked(handleAnalyzeLoudness).mockResolvedValue(mockResult);
const result = await handleAnalyzeLoudness({
audio_url: 'https://example.com/song.mp3',
});
expect(result.integrated_lufs).toBe(-14.0);
expect(result.true_peak_db).toBe(-1.0);
});
it('should execute check_job_status and return job state', async () => {
const mockResult = {
status: 'completed' as const,
progress: 100,
result: {
vocals_url: 'https://cdn.example.com/vocals.wav',
drums_url: 'https://cdn.example.com/drums.wav',
bass_url: 'https://cdn.example.com/bass.wav',
other_url: 'https://cdn.example.com/other.wav',
},
};
vi.mocked(handleCheckJobStatus).mockResolvedValue(mockResult);
const result = await handleCheckJobStatus({
job_id: 'job-123',
});
expect(result.status).toBe('completed');
expect(result.result).toBeDefined();
});
});
describe('API Key Validation', () => {
it('should check API key before tool execution', () => {
vi.mocked(hasApiKey).mockReturnValue(true);
expect(hasApiKey()).toBe(true);
});
it('should return false when API key is not set', () => {
vi.mocked(hasApiKey).mockReturnValue(false);
expect(hasApiKey()).toBe(false);
});
});
describe('Error Handling', () => {
it('should propagate errors from tool handlers', async () => {
const mockError = {
code: 'INVALID_URL',
message: 'Invalid URL',
suggestion: 'Use a valid URL',
};
vi.mocked(handleAnalyzeMusic).mockRejectedValue(mockError);
await expect(
handleAnalyzeMusic({ audio_url: 'invalid' })
).rejects.toMatchObject({
code: 'INVALID_URL',
});
});
it('should handle rate limiting errors', async () => {
const mockError = {
code: 'RATE_LIMITED',
message: 'Rate limited',
suggestion: 'Wait and retry',
retry_after: 30,
};
vi.mocked(handleAnalyzeMusic).mockRejectedValue(mockError);
await expect(
handleAnalyzeMusic({ audio_url: 'https://example.com/song.mp3' })
).rejects.toMatchObject({
code: 'RATE_LIMITED',
retry_after: 30,
});
});
});
});