feat: add instance lifecycle management with restart, inspect, and CLI commands

Adds restart/inspect methods to InstanceService, state validation for stop,
REST endpoints for restart and inspect, and full CLI command suite for
instance list/start/stop/restart/remove/logs/inspect.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-02-21 05:11:48 +00:00
parent 7c07749580
commit 4d796e2aa7
7 changed files with 386 additions and 3 deletions

View File

@@ -1,8 +1,16 @@
import type { McpInstance } from '@prisma/client';
import type { IMcpInstanceRepository, IMcpServerRepository } from '../repositories/interfaces.js';
import type { McpOrchestrator, ContainerSpec } from './orchestrator.js';
import type { McpOrchestrator, ContainerSpec, ContainerInfo } from './orchestrator.js';
import { NotFoundError } from './mcp-server.service.js';
export class InvalidStateError extends Error {
readonly statusCode = 409;
constructor(message: string) {
super(message);
this.name = 'InvalidStateError';
}
}
export class InstanceService {
constructor(
private instanceRepo: IMcpInstanceRepository,
@@ -71,6 +79,9 @@ export class InstanceService {
async stop(id: string): Promise<McpInstance> {
const instance = await this.getById(id);
if (instance.status === 'STOPPED') {
throw new InvalidStateError(`Instance '${id}' is already stopped`);
}
if (!instance.containerId) {
return this.instanceRepo.updateStatus(id, 'STOPPED');
}
@@ -87,6 +98,37 @@ export class InstanceService {
}
}
async restart(id: string): Promise<McpInstance> {
const instance = await this.getById(id);
// Stop if running
if (instance.containerId && (instance.status === 'RUNNING' || instance.status === 'STARTING')) {
try {
await this.orchestrator.stopContainer(instance.containerId);
} catch {
// Container may already be stopped
}
try {
await this.orchestrator.removeContainer(instance.containerId, true);
} catch {
// Container may already be gone
}
}
await this.instanceRepo.delete(id);
// Start a fresh instance for the same server
return this.start(instance.serverId);
}
async inspect(id: string): Promise<ContainerInfo> {
const instance = await this.getById(id);
if (!instance.containerId) {
throw new InvalidStateError(`Instance '${id}' has no container`);
}
return this.orchestrator.inspectContainer(instance.containerId);
}
async remove(id: string): Promise<void> {
const instance = await this.getById(id);