Skip to main content
Glama
sondt2709

Docker MCP

by sondt2709
DockerService.ts18.1 kB
import Docker from "dockerode"; import { DockerContainerSummary, DockerImageSummary, DockerNetworkSummary, DockerVolumeSummary, DockerSystemInfo, DockerVersion, DockerDiskUsage, ContainerRestartInfo, CleanupResult, CleanupSummary } from "../types/docker.js"; export class DockerService { private docker: Docker; constructor(options?: Docker.DockerOptions) { // Initialize dockerode with provided options or default local socket this.docker = new Docker(options || { socketPath: '/var/run/docker.sock' }); } // Container management methods async listContainers(options: { all?: boolean; filters?: Record<string, string> } = {}): Promise<any[]> { try { const containers = await this.docker.listContainers({ all: options.all || false, filters: options.filters ? JSON.stringify(options.filters) : undefined }); return containers; } catch (error) { throw new Error(`Failed to list containers: ${error instanceof Error ? error.message : String(error)}`); } } async inspectContainer(containerId: string): Promise<any> { try { const container = this.docker.getContainer(containerId); return await container.inspect(); } catch (error) { throw new Error(`Failed to inspect container ${containerId}: ${error instanceof Error ? error.message : String(error)}`); } } async startContainer(containerId: string): Promise<void> { try { const container = this.docker.getContainer(containerId); await container.start(); } catch (error) { throw new Error(`Failed to start container ${containerId}: ${error instanceof Error ? error.message : String(error)}`); } } async stopContainer(containerId: string, timeout?: number): Promise<void> { try { const container = this.docker.getContainer(containerId); await container.stop({ t: timeout }); } catch (error) { throw new Error(`Failed to stop container ${containerId}: ${error instanceof Error ? error.message : String(error)}`); } } async restartContainer(containerId: string, timeout?: number): Promise<void> { try { const container = this.docker.getContainer(containerId); await container.restart({ t: timeout }); } catch (error) { throw new Error(`Failed to restart container ${containerId}: ${error instanceof Error ? error.message : String(error)}`); } } async removeContainer(containerId: string, options: { force?: boolean; v?: boolean } = {}): Promise<void> { try { const container = this.docker.getContainer(containerId); await container.remove(options); } catch (error) { throw new Error(`Failed to remove container ${containerId}: ${error instanceof Error ? error.message : String(error)}`); } } async createContainer(options: Docker.ContainerCreateOptions): Promise<Docker.Container> { try { const container = await this.docker.createContainer(options); return container; } catch (error) { throw new Error(`Failed to create container: ${error instanceof Error ? error.message : String(error)}`); } } async getContainerLogs(containerId: string, options: { tail?: number; since?: string; until?: string; timestamps?: boolean; } = {}): Promise<string> { try { const container = this.docker.getContainer(containerId); const logOptions: any = { stdout: true, stderr: true, timestamps: options.timestamps || false, tail: options.tail || 100, // Default to last 100 lines since: options.since, until: options.until }; // Use callback-based approach which is more reliable return new Promise((resolve, reject) => { container.logs(logOptions, (err: any, data: any) => { if (err) { reject(err); return; } if (data && typeof data.on === 'function') { // It's a stream - collect all data let rawData = Buffer.alloc(0); data.on('data', (chunk: Buffer) => { rawData = Buffer.concat([rawData, chunk]); }); data.on('end', () => { const cleanedLogs = this.parseDockerLogs(rawData); resolve(cleanedLogs); }); data.on('error', reject); } else if (data) { // It's already a buffer or string const cleanedLogs = this.parseDockerLogs(Buffer.from(data)); resolve(cleanedLogs); } else { resolve(''); } }); }); } catch (error) { throw new Error(`Failed to get logs for container ${containerId}: ${error instanceof Error ? error.message : String(error)}`); } } private parseDockerLogs(buffer: Buffer): string { let result = ''; let offset = 0; while (offset < buffer.length) { if (offset + 8 > buffer.length) { // Not enough bytes for header, might be incomplete break; } // Docker stream format: // Byte 0: stream type (0=stdin, 1=stdout, 2=stderr) // Bytes 1-3: padding (always 0) // Bytes 4-7: size of payload (big endian) // Bytes 8+: actual log data const streamType = buffer.readUInt8(offset); const size = buffer.readUInt32BE(offset + 4); if (offset + 8 + size > buffer.length) { // Invalid size or incomplete data break; } // Extract the actual log content const logContent = buffer.slice(offset + 8, offset + 8 + size); result += logContent.toString(); offset += 8 + size; } return result; } async getContainerStats(containerId: string, stream: boolean = false): Promise<any> { try { const container = this.docker.getContainer(containerId); return await container.stats({ stream: stream as any }); } catch (error) { throw new Error(`Failed to get stats for container ${containerId}: ${error instanceof Error ? error.message : String(error)}`); } } async execInContainer(containerId: string, command: string[], options: any = {}): Promise<{ output: string; exitCode: number }> { try { const container = this.docker.getContainer(containerId); const exec = await container.exec({ Cmd: command, AttachStdout: true, AttachStderr: true, ...options }); const stream = await exec.start({ hijack: true, stdin: false }); return new Promise((resolve, reject) => { let output = ''; stream.on('data', (chunk: Buffer) => { output += chunk.toString(); }); stream.on('end', async () => { try { const inspectData = await exec.inspect(); resolve({ output, exitCode: inspectData.ExitCode || 0 }); } catch (error) { reject(error); } }); stream.on('error', reject); }); } catch (error) { throw new Error(`Failed to execute command in container ${containerId}: ${error instanceof Error ? error.message : String(error)}`); } } async getContainerProcesses(containerId: string): Promise<any> { try { const container = this.docker.getContainer(containerId); return await container.top(); } catch (error) { throw new Error(`Failed to get processes for container ${containerId}: ${error instanceof Error ? error.message : String(error)}`); } } async getContainerChanges(containerId: string): Promise<any> { try { const container = this.docker.getContainer(containerId); return await container.changes(); } catch (error) { throw new Error(`Failed to get changes for container ${containerId}: ${error instanceof Error ? error.message : String(error)}`); } } // Image management methods async listImages(options: { all?: boolean } = {}): Promise<any[]> { try { const images = await this.docker.listImages({ all: options.all || false }); return images; } catch (error) { throw new Error(`Failed to list images: ${error instanceof Error ? error.message : String(error)}`); } } async inspectImage(imageId: string): Promise<any> { try { const image = this.docker.getImage(imageId); return await image.inspect(); } catch (error) { throw new Error(`Failed to inspect image ${imageId}: ${error instanceof Error ? error.message : String(error)}`); } } async pullImage(imageName: string, options: any = {}): Promise<string> { try { const stream = await this.docker.pull(imageName, options); return new Promise((resolve, reject) => { let output = ''; stream.on('data', (chunk: Buffer) => { output += chunk.toString(); }); stream.on('end', () => { resolve(output); }); stream.on('error', reject); }); } catch (error) { throw new Error(`Failed to pull image ${imageName}: ${error instanceof Error ? error.message : String(error)}`); } } async removeImage(imageId: string, options: { force?: boolean; noprune?: boolean } = {}): Promise<any> { try { const image = this.docker.getImage(imageId); return await image.remove(options); } catch (error) { throw new Error(`Failed to remove image ${imageId}: ${error instanceof Error ? error.message : String(error)}`); } } // Network management methods async listNetworks(filters: Record<string, string> = {}): Promise<any[]> { try { const networks = await this.docker.listNetworks({ filters: Object.keys(filters).length > 0 ? JSON.stringify(filters) : undefined }); return networks; } catch (error) { throw new Error(`Failed to list networks: ${error instanceof Error ? error.message : String(error)}`); } } async inspectNetwork(networkId: string): Promise<any> { try { const network = this.docker.getNetwork(networkId); return await network.inspect(); } catch (error) { throw new Error(`Failed to inspect network ${networkId}: ${error instanceof Error ? error.message : String(error)}`); } } // Volume management methods async listVolumes(filters: Record<string, string> = {}): Promise<any> { try { const volumes = await this.docker.listVolumes({ filters: Object.keys(filters).length > 0 ? JSON.stringify(filters) : undefined }); return volumes; } catch (error) { throw new Error(`Failed to list volumes: ${error instanceof Error ? error.message : String(error)}`); } } async inspectVolume(volumeName: string): Promise<any> { try { const volume = this.docker.getVolume(volumeName); return await volume.inspect(); } catch (error) { throw new Error(`Failed to inspect volume ${volumeName}: ${error instanceof Error ? error.message : String(error)}`); } } // System information methods async getSystemInfo(): Promise<any> { try { const info = await this.docker.info(); return info; } catch (error) { throw new Error(`Failed to get system info: ${error instanceof Error ? error.message : String(error)}`); } } async getVersion(): Promise<any> { try { const version = await this.docker.version(); return version; } catch (error) { throw new Error(`Failed to get Docker version: ${error instanceof Error ? error.message : String(error)}`); } } async getDiskUsage(): Promise<any> { try { const df = await this.docker.df(); return df; } catch (error) { throw new Error(`Failed to get disk usage: ${error instanceof Error ? error.message : String(error)}`); } } // Cleanup methods async pruneContainers(filters: Record<string, string> = {}): Promise<CleanupResult> { try { const result = await this.docker.pruneContainers({ filters: Object.keys(filters).length > 0 ? JSON.stringify(filters) : undefined }); return { deletedItems: (result as any).ContainersDeleted || [], reclaimedSpace: (result as any).SpaceReclaimed || 0, errors: [], summary: `Removed ${(result as any).ContainersDeleted?.length || 0} containers, reclaimed ${(result as any).SpaceReclaimed || 0} bytes` }; } catch (error) { throw new Error(`Failed to prune containers: ${error instanceof Error ? error.message : String(error)}`); } } async pruneImages(filters: Record<string, string> = {}): Promise<CleanupResult> { try { const result = await this.docker.pruneImages({ filters: Object.keys(filters).length > 0 ? JSON.stringify(filters) : undefined }); return { deletedItems: (result as any).ImagesDeleted?.map((img: any) => img.Deleted || img.Untagged).filter(Boolean) || [], reclaimedSpace: (result as any).SpaceReclaimed || 0, errors: [], summary: `Removed ${(result as any).ImagesDeleted?.length || 0} images, reclaimed ${(result as any).SpaceReclaimed || 0} bytes` }; } catch (error) { throw new Error(`Failed to prune images: ${error instanceof Error ? error.message : String(error)}`); } } async pruneVolumes(filters: Record<string, string> = {}): Promise<CleanupResult> { try { const result = await this.docker.pruneVolumes({ filters: Object.keys(filters).length > 0 ? JSON.stringify(filters) : undefined }); return { deletedItems: (result as any).VolumesDeleted || [], reclaimedSpace: (result as any).SpaceReclaimed || 0, errors: [], summary: `Removed ${(result as any).VolumesDeleted?.length || 0} volumes, reclaimed ${(result as any).SpaceReclaimed || 0} bytes` }; } catch (error) { throw new Error(`Failed to prune volumes: ${error instanceof Error ? error.message : String(error)}`); } } async pruneNetworks(filters: Record<string, string> = {}): Promise<CleanupResult> { try { const result = await this.docker.pruneNetworks({ filters: Object.keys(filters).length > 0 ? JSON.stringify(filters) : undefined }); return { deletedItems: (result as any).NetworksDeleted || [], reclaimedSpace: 0, // Networks don't have size errors: [], summary: `Removed ${(result as any).NetworksDeleted?.length || 0} networks` }; } catch (error) { throw new Error(`Failed to prune networks: ${error instanceof Error ? error.message : String(error)}`); } } // Advanced methods async detectRestartLoops(timeWindowMinutes: number = 10, maxRestarts: number = 3): Promise<ContainerRestartInfo[]> { try { const containers = await this.listContainers({ all: true }); const restartLoops: ContainerRestartInfo[] = []; for (const container of containers) { const inspection = await this.inspectContainer(container.Id); const restartCount = inspection.RestartCount || 0; if (restartCount >= maxRestarts) { const lastRestartTime = inspection.State.StartedAt || new Date().toISOString(); const restartTime = new Date(lastRestartTime); const now = new Date(); const timeDiff = (now.getTime() - restartTime.getTime()) / (1000 * 60); // minutes if (timeDiff <= timeWindowMinutes) { restartLoops.push({ containerId: container.Id, name: container.Names[0] || 'unknown', restartCount, lastRestartTime, isRestartLoop: true, restartPolicy: inspection.HostConfig.RestartPolicy?.Name || 'no', exitCode: inspection.State.ExitCode, error: inspection.State.Error }); } } } return restartLoops; } catch (error) { throw new Error(`Failed to detect restart loops: ${error instanceof Error ? error.message : String(error)}`); } } async getCleanupSummary(): Promise<CleanupSummary> { try { const [containers, images, volumes, networks] = await Promise.all([ this.listContainers({ all: true }), this.listImages({ all: true }), this.listVolumes(), this.listNetworks() ]); // Filter stopped containers const stoppedContainers = containers.filter(c => c.State !== 'running'); const danglingImages = images.filter(img => !img.RepoTags || img.RepoTags.includes('<none>:<none>')); const unusedVolumes = volumes.Volumes?.filter((v: any) => !v.UsageData || v.UsageData.RefCount === 0) || []; const customNetworks = networks.filter(n => !['bridge', 'host', 'none'].includes(n.Name)); return { images: { count: danglingImages.length, size: danglingImages.reduce((sum: number, img: any) => sum + img.Size, 0), items: danglingImages.map((img: any) => img.Id) }, containers: { count: stoppedContainers.length, size: stoppedContainers.reduce((sum: number, c: any) => sum + (c.SizeRw || 0), 0), items: stoppedContainers.map((c: any) => c.Id) }, volumes: { count: unusedVolumes.length, size: unusedVolumes.reduce((sum: number, v: any) => sum + (v.UsageData?.Size || 0), 0), items: unusedVolumes.map((v: any) => v.Name) }, networks: { count: customNetworks.length, items: customNetworks.map((n: any) => n.Id) }, buildCache: { count: 0, size: 0, items: [] }, totalSize: 0 // Will be calculated }; } catch (error) { throw new Error(`Failed to get cleanup summary: ${error instanceof Error ? error.message : String(error)}`); } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sondt2709/docker-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server