We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/jmagar/homelab-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
import type {
DockerDiskUsage,
DockerSystemInfo,
HostConfig,
HostStatus,
PruneResult,
} from "../../types.js";
import { HostOperationError, logError } from "../../utils/errors.js";
import type { ClientManager } from "./utils/client-manager.js";
import {
calculateBuildCacheStats,
calculateContainerStats,
calculateImageStats,
calculateVolumeStats,
} from "./utils/stats-calculator.js";
/**
* Service for Docker system operations.
* Handles system info, disk usage, resource pruning, and host status.
*/
export class SystemService {
constructor(private clientManager: ClientManager) {}
/**
* Get status overview for multiple hosts in parallel.
*
* @param hosts - List of Docker hosts to query
* @returns Promise resolving to array of host status information
*/
async getHostStatus(hosts: HostConfig[]): Promise<HostStatus[]> {
// Query all hosts in parallel - errors are handled in getHostStatusSingle
return Promise.all(hosts.map((host) => this.getHostStatusSingle(host)));
}
/**
* Get status for a single host (internal helper).
*/
private async getHostStatusSingle(host: HostConfig): Promise<HostStatus> {
try {
const docker = await this.clientManager.getClient(host);
const containers = await docker.listContainers({ all: true });
const running = containers.filter((c) => c.State === "running").length;
return {
name: host.name,
host: host.host,
connected: true,
containerCount: containers.length,
runningCount: running,
};
} catch (error) {
logError(new HostOperationError("Failed to get host info", host.name, "getHostInfo", error), {
metadata: { host: host.host },
});
return {
name: host.name,
host: host.host,
connected: false,
containerCount: 0,
runningCount: 0,
error: error instanceof Error ? error.message : "Connection failed",
};
}
}
/**
* Get Docker system information for a host.
*
* @param host - Host configuration
* @returns Promise resolving to Docker system information
*/
async getDockerInfo(host: HostConfig): Promise<DockerSystemInfo> {
const docker = await this.clientManager.getClient(host);
const info = await docker.info();
const version = await docker.version();
return {
dockerVersion: version.Version || "unknown",
apiVersion: version.ApiVersion || "unknown",
os: info.OperatingSystem || info.OSType || "unknown",
arch: info.Architecture || "unknown",
kernelVersion: info.KernelVersion || "unknown",
cpus: info.NCPU || 0,
memoryBytes: info.MemTotal || 0,
storageDriver: info.Driver || "unknown",
rootDir: info.DockerRootDir || "/var/lib/docker",
containersTotal: info.Containers || 0,
containersRunning: info.ContainersRunning || 0,
containersPaused: info.ContainersPaused || 0,
containersStopped: info.ContainersStopped || 0,
images: info.Images || 0,
};
}
/**
* Get Docker disk usage statistics (system df).
*
* @param host - Host configuration
* @returns Promise resolving to disk usage statistics
*/
async getDockerDiskUsage(host: HostConfig): Promise<DockerDiskUsage> {
const docker = await this.clientManager.getClient(host);
const df = await docker.df();
// Calculate stats using helper functions
const imageStats = calculateImageStats(df.Images || []);
const containerStats = calculateContainerStats(df.Containers || []);
const volumeStats = calculateVolumeStats(df.Volumes || []);
const buildCacheStats = calculateBuildCacheStats(df.BuildCache || []);
const totalSize =
imageStats.size + containerStats.size + volumeStats.size + buildCacheStats.size;
const totalReclaimable =
imageStats.reclaimable +
containerStats.size +
volumeStats.reclaimable +
buildCacheStats.reclaimable;
return {
images: {
total: imageStats.total,
active: imageStats.active,
size: imageStats.size,
reclaimable: imageStats.reclaimable,
},
containers: {
total: containerStats.total,
running: containerStats.running,
size: containerStats.size + containerStats.rootfsSize,
reclaimable: containerStats.size,
},
volumes: {
total: volumeStats.total,
active: volumeStats.active,
size: volumeStats.size,
reclaimable: volumeStats.reclaimable,
},
buildCache: {
total: buildCacheStats.total,
size: buildCacheStats.size,
reclaimable: buildCacheStats.reclaimable,
},
totalSize,
totalReclaimable,
};
}
/**
* Prune Docker resources (cleanup unused items).
* This is a destructive operation — callers must pass `force: true` to confirm.
*
* @param host - Host configuration
* @param target - Resource type to prune or "all" for everything
* @param options - Options including force confirmation
* @returns Promise resolving to array of prune results per resource type
* @throws Error if force is not true
*/
async pruneDocker(
host: HostConfig,
target: "containers" | "images" | "volumes" | "networks" | "buildcache" | "all",
options: { force?: boolean } = {}
): Promise<PruneResult[]> {
if (!options.force) {
throw new Error("pruneDocker requires { force: true } to confirm destructive operation");
}
const docker = await this.clientManager.getClient(host);
const results: PruneResult[] = [];
const targets =
target === "all"
? (["containers", "images", "volumes", "networks", "buildcache"] as const)
: ([target] as const);
for (const t of targets) {
try {
switch (t) {
case "containers": {
const res = await docker.pruneContainers();
results.push({
type: "containers",
spaceReclaimed: res.SpaceReclaimed || 0,
itemsDeleted: res.ContainersDeleted?.length || 0,
details: res.ContainersDeleted,
});
break;
}
case "images": {
const res = await docker.pruneImages();
results.push({
type: "images",
spaceReclaimed: res.SpaceReclaimed || 0,
itemsDeleted: res.ImagesDeleted?.length || 0,
details: res.ImagesDeleted?.map((i) => i.Deleted || i.Untagged || ""),
});
break;
}
case "volumes": {
const res = await docker.pruneVolumes();
results.push({
type: "volumes",
spaceReclaimed: res.SpaceReclaimed || 0,
itemsDeleted: res.VolumesDeleted?.length || 0,
details: res.VolumesDeleted,
});
break;
}
case "networks": {
const res = await docker.pruneNetworks();
results.push({
type: "networks",
spaceReclaimed: 0,
itemsDeleted: res.NetworksDeleted?.length || 0,
details: res.NetworksDeleted,
});
break;
}
case "buildcache": {
const res = (await docker.pruneBuilder()) as {
SpaceReclaimed?: number;
CachesDeleted?: string[];
};
results.push({
type: "buildcache",
spaceReclaimed: res.SpaceReclaimed || 0,
itemsDeleted: res.CachesDeleted?.length || 0,
details: res.CachesDeleted,
});
break;
}
}
} catch (error) {
logError(
new HostOperationError("Docker cleanup failed", host.name, "dockerCleanup", error),
{
metadata: { type: t },
}
);
results.push({
type: t,
spaceReclaimed: 0,
itemsDeleted: 0,
details: [`Error: ${error instanceof Error ? error.message : "Unknown error"}`],
});
}
}
return results;
}
}