UNPKG

docker-mcp

Version:

A Model Context Protocol (MCP) server that enables AI agents to interact with Docker containers locally or remotely via SSH. Provides comprehensive Docker management capabilities including container operations, logs, monitoring, and cleanup.

467 lines 19 kB
import Docker from "dockerode"; export class DockerService { docker; constructor(options) { // 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 = {}) { 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) { 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) { 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, timeout) { 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, timeout) { 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, options = {}) { 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) { 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, options = {}) { try { const container = this.docker.getContainer(containerId); const logOptions = { 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, data) => { 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) => { 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)}`); } } parseDockerLogs(buffer) { 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, stream = false) { try { const container = this.docker.getContainer(containerId); return await container.stats({ stream: stream }); } catch (error) { throw new Error(`Failed to get stats for container ${containerId}: ${error instanceof Error ? error.message : String(error)}`); } } async execInContainer(containerId, command, options = {}) { 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) => { 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) { 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) { 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 = {}) { 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) { 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, options = {}) { try { const stream = await this.docker.pull(imageName, options); return new Promise((resolve, reject) => { let output = ''; stream.on('data', (chunk) => { 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, options = {}) { 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 = {}) { 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) { 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 = {}) { 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) { 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() { 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() { 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() { 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 = {}) { try { const result = await this.docker.pruneContainers({ filters: Object.keys(filters).length > 0 ? JSON.stringify(filters) : undefined }); return { deletedItems: result.ContainersDeleted || [], reclaimedSpace: result.SpaceReclaimed || 0, errors: [], summary: `Removed ${result.ContainersDeleted?.length || 0} containers, reclaimed ${result.SpaceReclaimed || 0} bytes` }; } catch (error) { throw new Error(`Failed to prune containers: ${error instanceof Error ? error.message : String(error)}`); } } async pruneImages(filters = {}) { try { const result = await this.docker.pruneImages({ filters: Object.keys(filters).length > 0 ? JSON.stringify(filters) : undefined }); return { deletedItems: result.ImagesDeleted?.map((img) => img.Deleted || img.Untagged).filter(Boolean) || [], reclaimedSpace: result.SpaceReclaimed || 0, errors: [], summary: `Removed ${result.ImagesDeleted?.length || 0} images, reclaimed ${result.SpaceReclaimed || 0} bytes` }; } catch (error) { throw new Error(`Failed to prune images: ${error instanceof Error ? error.message : String(error)}`); } } async pruneVolumes(filters = {}) { try { const result = await this.docker.pruneVolumes({ filters: Object.keys(filters).length > 0 ? JSON.stringify(filters) : undefined }); return { deletedItems: result.VolumesDeleted || [], reclaimedSpace: result.SpaceReclaimed || 0, errors: [], summary: `Removed ${result.VolumesDeleted?.length || 0} volumes, reclaimed ${result.SpaceReclaimed || 0} bytes` }; } catch (error) { throw new Error(`Failed to prune volumes: ${error instanceof Error ? error.message : String(error)}`); } } async pruneNetworks(filters = {}) { try { const result = await this.docker.pruneNetworks({ filters: Object.keys(filters).length > 0 ? JSON.stringify(filters) : undefined }); return { deletedItems: result.NetworksDeleted || [], reclaimedSpace: 0, // Networks don't have size errors: [], summary: `Removed ${result.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 = 10, maxRestarts = 3) { try { const containers = await this.listContainers({ all: true }); const restartLoops = []; 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() { 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) => !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, img) => sum + img.Size, 0), items: danglingImages.map((img) => img.Id) }, containers: { count: stoppedContainers.length, size: stoppedContainers.reduce((sum, c) => sum + (c.SizeRw || 0), 0), items: stoppedContainers.map((c) => c.Id) }, volumes: { count: unusedVolumes.length, size: unusedVolumes.reduce((sum, v) => sum + (v.UsageData?.Size || 0), 0), items: unusedVolumes.map((v) => v.Name) }, networks: { count: customNetworks.length, items: customNetworks.map((n) => 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)}`); } } } //# sourceMappingURL=DockerService.js.map