UNPKG

@masonator/coolify-mcp

Version:

MCP server implementation for Coolify

317 lines (316 loc) 15.5 kB
/** * MCP Server Tests v2.0.0 * * Tests for the consolidated MCP tool layer. * CoolifyClient methods are fully tested in coolify-client.test.ts (174 tests). * These tests verify MCP server instantiation and structure. */ import { createRequire } from 'module'; import { describe, it, expect, beforeEach } from '@jest/globals'; import { CoolifyMcpServer, VERSION, truncateLogs, getApplicationActions, getDeploymentActions, getPagination, } from '../lib/mcp-server.js'; describe('CoolifyMcpServer v2', () => { let server; beforeEach(() => { server = new CoolifyMcpServer({ baseUrl: 'http://localhost:3000', accessToken: 'test-token', }); }); describe('constructor', () => { it('should create server instance', () => { expect(server).toBeInstanceOf(CoolifyMcpServer); }); it('should be an MCP server with connect method', () => { expect(typeof server.connect).toBe('function'); }); it('should report version matching package.json', () => { const _require = createRequire(import.meta.url); const { version } = _require('../../package.json'); expect(VERSION).toBe(version); }); }); describe('client', () => { it('should have client instance', () => { const client = server['client']; expect(client).toBeDefined(); }); it('should have all required client methods', () => { const client = server['client']; // Core methods expect(typeof client.getVersion).toBe('function'); // Server operations expect(typeof client.listServers).toBe('function'); expect(typeof client.getServer).toBe('function'); expect(typeof client.getServerResources).toBe('function'); expect(typeof client.getServerDomains).toBe('function'); expect(typeof client.validateServer).toBe('function'); // Project operations expect(typeof client.listProjects).toBe('function'); expect(typeof client.getProject).toBe('function'); expect(typeof client.createProject).toBe('function'); expect(typeof client.updateProject).toBe('function'); expect(typeof client.deleteProject).toBe('function'); // Environment operations expect(typeof client.listProjectEnvironments).toBe('function'); expect(typeof client.getProjectEnvironment).toBe('function'); expect(typeof client.getProjectEnvironmentWithDatabases).toBe('function'); expect(typeof client.createProjectEnvironment).toBe('function'); expect(typeof client.deleteProjectEnvironment).toBe('function'); // Application operations expect(typeof client.listApplications).toBe('function'); expect(typeof client.getApplication).toBe('function'); expect(typeof client.createApplicationPublic).toBe('function'); expect(typeof client.createApplicationPrivateGH).toBe('function'); expect(typeof client.createApplicationPrivateKey).toBe('function'); expect(typeof client.createApplicationDockerImage).toBe('function'); expect(typeof client.updateApplication).toBe('function'); expect(typeof client.deleteApplication).toBe('function'); expect(typeof client.getApplicationLogs).toBe('function'); // Control operations expect(typeof client.startApplication).toBe('function'); expect(typeof client.stopApplication).toBe('function'); expect(typeof client.restartApplication).toBe('function'); expect(typeof client.startDatabase).toBe('function'); expect(typeof client.stopDatabase).toBe('function'); expect(typeof client.restartDatabase).toBe('function'); expect(typeof client.startService).toBe('function'); expect(typeof client.stopService).toBe('function'); expect(typeof client.restartService).toBe('function'); // Database operations expect(typeof client.listDatabases).toBe('function'); expect(typeof client.getDatabase).toBe('function'); expect(typeof client.deleteDatabase).toBe('function'); expect(typeof client.createPostgresql).toBe('function'); expect(typeof client.createMysql).toBe('function'); expect(typeof client.createMariadb).toBe('function'); expect(typeof client.createMongodb).toBe('function'); expect(typeof client.createRedis).toBe('function'); expect(typeof client.createKeydb).toBe('function'); expect(typeof client.createClickhouse).toBe('function'); expect(typeof client.createDragonfly).toBe('function'); // Service operations expect(typeof client.listServices).toBe('function'); expect(typeof client.getService).toBe('function'); expect(typeof client.createService).toBe('function'); expect(typeof client.updateService).toBe('function'); expect(typeof client.deleteService).toBe('function'); // Environment variable operations expect(typeof client.listApplicationEnvVars).toBe('function'); expect(typeof client.createApplicationEnvVar).toBe('function'); expect(typeof client.updateApplicationEnvVar).toBe('function'); expect(typeof client.deleteApplicationEnvVar).toBe('function'); expect(typeof client.listServiceEnvVars).toBe('function'); expect(typeof client.createServiceEnvVar).toBe('function'); expect(typeof client.deleteServiceEnvVar).toBe('function'); // Deployment operations expect(typeof client.listDeployments).toBe('function'); expect(typeof client.getDeployment).toBe('function'); expect(typeof client.deployByTagOrUuid).toBe('function'); expect(typeof client.listApplicationDeployments).toBe('function'); expect(typeof client.cancelDeployment).toBe('function'); // Private key operations expect(typeof client.listPrivateKeys).toBe('function'); expect(typeof client.getPrivateKey).toBe('function'); expect(typeof client.createPrivateKey).toBe('function'); expect(typeof client.updatePrivateKey).toBe('function'); expect(typeof client.deletePrivateKey).toBe('function'); // GitHub App operations expect(typeof client.listGitHubApps).toBe('function'); expect(typeof client.createGitHubApp).toBe('function'); expect(typeof client.updateGitHubApp).toBe('function'); expect(typeof client.deleteGitHubApp).toBe('function'); // Backup operations expect(typeof client.listDatabaseBackups).toBe('function'); expect(typeof client.getDatabaseBackup).toBe('function'); expect(typeof client.createDatabaseBackup).toBe('function'); expect(typeof client.updateDatabaseBackup).toBe('function'); expect(typeof client.deleteDatabaseBackup).toBe('function'); expect(typeof client.listBackupExecutions).toBe('function'); expect(typeof client.getBackupExecution).toBe('function'); // Diagnostic operations expect(typeof client.diagnoseApplication).toBe('function'); expect(typeof client.diagnoseServer).toBe('function'); expect(typeof client.findInfrastructureIssues).toBe('function'); // Batch operations expect(typeof client.restartProjectApps).toBe('function'); expect(typeof client.bulkEnvUpdate).toBe('function'); expect(typeof client.stopAllApps).toBe('function'); expect(typeof client.redeployProjectApps).toBe('function'); }); }); describe('server configuration', () => { it('should store baseUrl and accessToken in client', () => { const client = server['client']; // CoolifyClient stores base URL without /api/v1 suffix expect(client['baseUrl']).toBe('http://localhost:3000'); expect(client['accessToken']).toBe('test-token'); }); }); }); describe('truncateLogs', () => { it('should return logs unchanged when within limits', () => { const logs = 'line1\nline2\nline3'; const result = truncateLogs(logs, 200, 50000); expect(result).toBe(logs); }); it('should truncate to last N lines', () => { const logs = 'line1\nline2\nline3\nline4\nline5'; const result = truncateLogs(logs, 3, 50000); expect(result).toBe('line3\nline4\nline5'); }); it('should truncate by character limit when lines are huge', () => { const hugeLine = 'x'.repeat(100); const logs = `${hugeLine}\n${hugeLine}\n${hugeLine}`; const result = truncateLogs(logs, 200, 50); expect(result.length).toBeLessThanOrEqual(50); expect(result.startsWith('...[truncated]...')).toBe(true); }); it('should not add truncation prefix when under char limit', () => { const logs = 'line1\nline2\nline3'; const result = truncateLogs(logs, 200, 50000); expect(result.startsWith('...[truncated]...')).toBe(false); }); it('should handle empty logs', () => { const result = truncateLogs('', 200, 50000); expect(result).toBe(''); }); it('should use default limits when not specified', () => { const logs = 'line1\nline2'; const result = truncateLogs(logs); expect(result).toBe(logs); }); it('should respect custom line limit', () => { const lines = Array.from({ length: 300 }, (_, i) => `line${i + 1}`).join('\n'); const result = truncateLogs(lines, 50, 50000); const resultLines = result.split('\n'); expect(resultLines.length).toBe(50); expect(resultLines[0]).toBe('line251'); expect(resultLines[49]).toBe('line300'); }); it('should respect custom char limit', () => { const logs = 'x'.repeat(1000); const result = truncateLogs(logs, 200, 100); expect(result.length).toBe(100); }); }); // ============================================================================= // Action Generators Tests // ============================================================================= describe('getApplicationActions', () => { it('should return view logs action for all apps', () => { const actions = getApplicationActions('app-uuid', 'stopped'); expect(actions).toContainEqual({ tool: 'application_logs', args: { uuid: 'app-uuid' }, hint: 'View logs', }); }); it('should return restart/stop actions for running apps', () => { const actions = getApplicationActions('app-uuid', 'running'); expect(actions).toContainEqual({ tool: 'control', args: { resource: 'application', action: 'restart', uuid: 'app-uuid' }, hint: 'Restart', }); expect(actions).toContainEqual({ tool: 'control', args: { resource: 'application', action: 'stop', uuid: 'app-uuid' }, hint: 'Stop', }); }); it('should return start action for stopped apps', () => { const actions = getApplicationActions('app-uuid', 'stopped'); expect(actions).toContainEqual({ tool: 'control', args: { resource: 'application', action: 'start', uuid: 'app-uuid' }, hint: 'Start', }); }); it('should handle running:healthy status', () => { const actions = getApplicationActions('app-uuid', 'running:healthy'); expect(actions.some((a) => a.hint === 'Restart')).toBe(true); expect(actions.some((a) => a.hint === 'Stop')).toBe(true); }); it('should handle undefined status', () => { const actions = getApplicationActions('app-uuid', undefined); expect(actions).toContainEqual({ tool: 'control', args: { resource: 'application', action: 'start', uuid: 'app-uuid' }, hint: 'Start', }); }); }); describe('getDeploymentActions', () => { it('should return cancel action for in_progress deployments', () => { const actions = getDeploymentActions('dep-uuid', 'in_progress', 'app-uuid'); expect(actions).toContainEqual({ tool: 'deployment', args: { action: 'cancel', uuid: 'dep-uuid' }, hint: 'Cancel', }); }); it('should return cancel action for queued deployments', () => { const actions = getDeploymentActions('dep-uuid', 'queued', 'app-uuid'); expect(actions).toContainEqual({ tool: 'deployment', args: { action: 'cancel', uuid: 'dep-uuid' }, hint: 'Cancel', }); }); it('should return app actions when appUuid provided', () => { const actions = getDeploymentActions('dep-uuid', 'finished', 'app-uuid'); expect(actions).toContainEqual({ tool: 'get_application', args: { uuid: 'app-uuid' }, hint: 'View app', }); expect(actions).toContainEqual({ tool: 'application_logs', args: { uuid: 'app-uuid' }, hint: 'App logs', }); }); it('should not return cancel for finished deployments', () => { const actions = getDeploymentActions('dep-uuid', 'finished', 'app-uuid'); expect(actions.some((a) => a.hint === 'Cancel')).toBe(false); }); it('should return empty actions when no appUuid and not in_progress', () => { const actions = getDeploymentActions('dep-uuid', 'finished', undefined); expect(actions).toEqual([]); }); }); describe('getPagination', () => { it('should return undefined when count is less than perPage and page is 1', () => { const result = getPagination('list_apps', 1, 50, 30); expect(result).toBeUndefined(); }); it('should return next when count equals or exceeds perPage', () => { const result = getPagination('list_apps', 1, 50, 50); expect(result).toEqual({ next: { tool: 'list_apps', args: { page: 2, per_page: 50 } }, }); }); it('should return both prev and next for middle pages', () => { const result = getPagination('list_apps', 2, 50, 50); expect(result).toEqual({ prev: { tool: 'list_apps', args: { page: 1, per_page: 50 } }, next: { tool: 'list_apps', args: { page: 3, per_page: 50 } }, }); }); it('should return prev when page > 1 and count < perPage', () => { const result = getPagination('list_apps', 3, 50, 20); expect(result).toEqual({ prev: { tool: 'list_apps', args: { page: 2, per_page: 50 } }, }); }); it('should use default page and perPage when undefined', () => { const result = getPagination('list_apps', undefined, undefined, 100); expect(result).toEqual({ next: { tool: 'list_apps', args: { page: 2, per_page: 50 } }, }); }); it('should return undefined when count is undefined', () => { const result = getPagination('list_apps', 1, 50, undefined); expect(result).toBeUndefined(); }); });