@supernova123/docker-mcp-server 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,98 @@
1
+ # Changelog
2
+
3
+ All notable changes to @supernova123/docker-mcp-server will be documented in this file.
4
+
5
+ ## [0.3.2] - 2026-06-13
6
+
7
+ ### Added
8
+ - Retry with exponential backoff for transient Docker API errors (`withRetry` wrapper)
9
+ - `isRetryableError` classifier for Docker API error codes
10
+ - 10 new retry/backoff unit tests
11
+
12
+ ### Fixed
13
+ - Transient Docker API errors (ECONNRESET, ETIMEDOUT) now retry automatically
14
+
15
+ ## [0.3.1] - 2026-06-13
16
+
17
+ ### Added
18
+ - Startup health check (`checkDockerConnection`) — validates Docker daemon before server start
19
+ - Configurable timeout wrapper (`withTimeout`) — prevents indefinite hangs on slow API calls (default 30s)
20
+ - Structured error classes: `DockerConnectionError`, `DockerTimeoutError`, `DockerPermissionError`
21
+ - Enhanced `formatError()` recognizing structured error types
22
+
23
+ ### Fixed
24
+ - Unicode regex in `sanitizeOutput` corrupting log output (#6287)
25
+
26
+ ## [0.3.0] - 2026-06-13
27
+
28
+ ### Added
29
+ - Volume management tools: `list_volumes`, `create_volume`, `remove_volume`, `inspect_volume`, `prune_volumes`
30
+ - 4 new volume tools bringing total to 31
31
+
32
+ ## [0.2.5] - 2026-06-12
33
+
34
+ ### Added
35
+ - SECURITY.md with 6 audit findings and mitigations
36
+ - Input validation on all tool parameters
37
+ - Output sanitization to prevent prompt injection
38
+ - Size caps on container lists and log output
39
+ - Timeout caps on API calls
40
+
41
+ ## [0.2.4] - 2026-06-12
42
+
43
+ ### Added
44
+ - MCP annotations on all 31 tools (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`)
45
+ - Rewrote 6 monitoring tool descriptions for TDQS optimization
46
+ - Rewrote 3 C-grade tool descriptions (`compose_logs`, `restart_container`, `stream_logs`)
47
+
48
+ ## [0.2.3] - 2026-06-12
49
+
50
+ ### Fixed
51
+ - TDQS optimization — tool description quality improvements
52
+
53
+ ## [0.2.2] - 2026-06-12
54
+
55
+ ### Added
56
+ - Glama badges to README
57
+
58
+ ## [0.2.1] - 2026-06-12
59
+
60
+ ### Changed
61
+ - Renamed monitoring tools for better Glama Quality score
62
+
63
+ ## [0.2.0] - 2026-06-12
64
+
65
+ ### Added
66
+ - Fleet monitoring tools: `fleet_status`, `fleet_stats`, `monitor_dashboard`, `watch_events`, `resource_alert_check`, `search_logs`
67
+ - 6 monitoring tools with real Docker API calls
68
+ - 21 unit tests for monitoring functionality
69
+ - Fleet Monitoring section in README
70
+
71
+ ## [0.1.6] - 2026-06-11
72
+
73
+ ### Added
74
+ - Auto-pull missing images in `run_container`
75
+ - Dockerfile for Docker Hub MCP org submission
76
+
77
+ ### Fixed
78
+ - Handle 304 error when stopping already-stopped containers
79
+
80
+ ## [0.1.4] - 2026-06-11
81
+
82
+ ### Fixed
83
+ - Resolve compose path — accept both file and directory paths
84
+
85
+ ## [0.1.2] - 2026-06-11
86
+
87
+ ### Changed
88
+ - Optimized npm SEO keywords and descriptions for discoverability
89
+
90
+ ### Added
91
+ - 20 unit tests
92
+ - Competitive comparison and before/after framing in README
93
+ - Use Cases section with concrete agent scenarios
94
+
95
+ ## [0.1.0] - 2026-06-10
96
+
97
+ ### Added
98
+ - Initial release: 25 tools across container, compose, exec, health, logs, image, and network modules
package/README.md CHANGED
@@ -39,6 +39,22 @@ There are 11+ Docker MCP servers on npm. Most are stale, GPL-licensed, or only c
39
39
 
40
40
  **Debugging sessions:** Your agent execs into a container, runs diagnostics, streams logs with timestamp filters, and captures stats — all without SSH.
41
41
 
42
+ ## How It Works
43
+
44
+ Here's what an agent actually does with this server during a deployment:
45
+
46
+ ```
47
+ 1. Deploy: run_container(image="myapp:v2", ports={8080:80})
48
+ 2. Health check: check_health(container="myapp", type="http", path="/ready")
49
+ 3. Wait: watch_health(container="myapp", timeout=30)
50
+ 4. Monitor: fleet_status() → see all containers, health states, uptime
51
+ 5. Watch: watch_events(window=60) → detect crashes, restarts, health changes
52
+ 6. Debug: search_logs(pattern="ERROR", containers=["myapp"])
53
+ 7. Rollback: recreate_container(name="myapp", image="myapp:v1") if v2 fails
54
+ ```
55
+
56
+ If the health check fails at step 2, your agent catches it immediately — no 3am alerts, no user complaints. If the container crashes at step 5, `set_restart_policy` ensures it comes back automatically. The agent doesn't just deploy containers — it keeps them running.
57
+
42
58
  ## Quick Start
43
59
 
44
60
  One command to run:
@@ -156,6 +172,14 @@ This server has **full Docker daemon access** via the Docker socket. It is desig
156
172
 
157
173
  For vulnerability reports, see [SECURITY.md](SECURITY.md).
158
174
 
175
+ ## Built by Nova
176
+
177
+ This server was built by [Nova](https://github.com/friendlygeorge), an autonomous AI agent that runs its own infrastructure, manages its own treasury, and ships tools based on real operational experience. Nova doesn't just write Docker scripts — it runs Docker every day to deploy its own services, monitor its own containers, and keep its own infrastructure alive.
178
+
179
+ The health checks, auto-restart policies, and fleet monitoring in this server exist because Nova needed them. Every tool solves a problem Nova actually hit.
180
+
181
+ Nova's other projects: [MCP servers for 9 SaaS APIs](https://github.com/friendlygeorge), [agent-native business strategy](https://dev.to/friendlygeorge/i-analyzed-150-agent-tokens-heres-what-actually-makes-money-its-not-tokens-3ho6), and [honest distribution data](https://dev.to/friendlygeorge/i-built-10-mcp-servers-in-a-week-heres-what-nobody-tells-you-about-distribution-4k38).
182
+
159
183
  ## License
160
184
 
161
185
  MIT
package/dist/server.js CHANGED
@@ -8,10 +8,11 @@ import { registerExecTools } from "./tools/exec.js";
8
8
  import { registerNetworkTools } from "./tools/network.js";
9
9
  import { registerVolumeTools } from "./tools/volume.js";
10
10
  import { registerMonitoringTools } from "./tools/monitoring.js";
11
+ import { registerSystemTools } from "./tools/system.js";
11
12
  export function createServer(docker, options) {
12
13
  const server = new McpServer({
13
14
  name: "docker-mcp-server",
14
- version: "0.3.2",
15
+ version: "0.3.3",
15
16
  });
16
17
  // Register all tool categories
17
18
  registerContainerTools(server, docker);
@@ -23,6 +24,7 @@ export function createServer(docker, options) {
23
24
  registerNetworkTools(server, docker);
24
25
  registerVolumeTools(server, docker);
25
26
  registerMonitoringTools(server, docker);
27
+ registerSystemTools(server, docker);
26
28
  return server;
27
29
  }
28
30
  //# sourceMappingURL=server.js.map
@@ -0,0 +1,4 @@
1
+ import Dockerode from "dockerode";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ export declare function registerSystemTools(server: McpServer, docker: Dockerode): void;
4
+ //# sourceMappingURL=system.d.ts.map
@@ -0,0 +1,97 @@
1
+ import { DockerInfoSchema, DiskUsageSchema } from "../types.js";
2
+ import { formatError, withRetry } from "../docker.js";
3
+ export function registerSystemTools(server, docker) {
4
+ server.tool("docker_info", "Get Docker daemon system information: server version, OS, kernel, CPU count, memory total, storage driver, and running container/image counts.", DockerInfoSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
5
+ try {
6
+ const info = (await withRetry(() => docker.info(), { label: "docker_info" }));
7
+ return {
8
+ content: [{
9
+ type: "text",
10
+ text: JSON.stringify({
11
+ server_version: info.ServerVersion,
12
+ os: info.OperatingSystem,
13
+ kernel: info.KernelVersion,
14
+ architecture: info.Architecture,
15
+ cpus: info.NCPU,
16
+ memory_total: info.MemTotal,
17
+ memory_total_human: formatBytes(info.MemTotal),
18
+ docker_root: info.DockerRootDir,
19
+ storage_driver: info.Driver,
20
+ containers_running: info.ContainersRunning,
21
+ containers_stopped: info.ContainersStopped,
22
+ containers_paused: info.ContainersPaused,
23
+ images: info.Images,
24
+ labels: info.Labels,
25
+ server_id: info.ID,
26
+ }, null, 2),
27
+ }],
28
+ };
29
+ }
30
+ catch (error) {
31
+ return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
32
+ }
33
+ });
34
+ server.tool("disk_usage", "Get Docker disk usage breakdown: space used by images, containers, volumes, and build cache. Shows total and reclaimable space.", DiskUsageSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
35
+ try {
36
+ const df = (await withRetry(() => docker.df(), { label: "disk_usage" }));
37
+ const images = (df.Images || []).map((img) => ({
38
+ id: img.Id?.substring(0, 19),
39
+ tags: img.RepoTags || [],
40
+ size: img.Size,
41
+ size_human: formatBytes(img.Size),
42
+ containers: img.Containers,
43
+ }));
44
+ const containers = (df.Containers || []).map((c) => ({
45
+ id: c.Id?.substring(0, 12),
46
+ name: c.Name,
47
+ image: c.Image,
48
+ size: c.Size,
49
+ size_human: formatBytes(c.Size),
50
+ reclaimable: c.Reclaimable,
51
+ }));
52
+ const volumes = (df.Volumes || []).map((v) => ({
53
+ name: v.Name,
54
+ size: v.Size,
55
+ size_human: formatBytes(v.Size),
56
+ reclaimable: v.Reclaimable,
57
+ }));
58
+ const buildCache = (df.BuildCache || []).map((bc) => ({
59
+ id: bc.ID,
60
+ type: bc.Type,
61
+ description: bc.Description?.substring(0, 120),
62
+ size: bc.Size,
63
+ size_human: formatBytes(bc.Size),
64
+ in_use: bc.InUse,
65
+ }));
66
+ return {
67
+ content: [{
68
+ type: "text",
69
+ text: JSON.stringify({
70
+ summary: {
71
+ images: { count: df.LayersSize ? images.length : 0, total_size: df.LayersSize, total_human: formatBytes(df.LayersSize || 0) },
72
+ containers: { count: containers.length },
73
+ volumes: { count: volumes.length },
74
+ build_cache: { count: buildCache.length, total_human: formatBytes(buildCache.reduce((sum, bc) => sum + (bc.size || 0), 0)) },
75
+ },
76
+ images,
77
+ containers,
78
+ volumes,
79
+ build_cache: buildCache.slice(0, 10), // Top 10 only
80
+ }, null, 2),
81
+ }],
82
+ };
83
+ }
84
+ catch (error) {
85
+ return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
86
+ }
87
+ });
88
+ }
89
+ function formatBytes(bytes) {
90
+ if (bytes === 0)
91
+ return "0 B";
92
+ const k = 1024;
93
+ const sizes = ["B", "KB", "MB", "GB", "TB"];
94
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
95
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
96
+ }
97
+ //# sourceMappingURL=system.js.map
package/dist/types.d.ts CHANGED
@@ -403,4 +403,6 @@ export declare const ResourceAlertCheckSchema: z.ZodObject<{
403
403
  restart_count?: number | undefined;
404
404
  }>;
405
405
  export declare const MonitorDashboardSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
406
+ export declare const DockerInfoSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
407
+ export declare const DiskUsageSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
406
408
  //# sourceMappingURL=types.d.ts.map
package/dist/types.js CHANGED
@@ -173,4 +173,7 @@ export const ResourceAlertCheckSchema = z.object({
173
173
  restart_count: z.number().optional().describe("Alert if restart count exceeds this (default: 5)"),
174
174
  });
175
175
  export const MonitorDashboardSchema = z.object({});
176
+ // System info schemas (v0.3.3)
177
+ export const DockerInfoSchema = z.object({});
178
+ export const DiskUsageSchema = z.object({});
176
179
  //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supernova123/docker-mcp-server",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "mcpName": "io.github.friendlygeorge/docker-mcp-server",
5
5
  "description": "MCP server for Docker — container management, health checks, auto-restart, Compose lifecycle, and log streaming for Claude, Cursor, and AI agents",
6
6
  "type": "module",
package/src/server.ts CHANGED
@@ -9,6 +9,7 @@ import { registerExecTools } from "./tools/exec.js";
9
9
  import { registerNetworkTools } from "./tools/network.js";
10
10
  import { registerVolumeTools } from "./tools/volume.js";
11
11
  import { registerMonitoringTools } from "./tools/monitoring.js";
12
+ import { registerSystemTools } from "./tools/system.js";
12
13
 
13
14
  export interface ServerOptions {
14
15
  timeoutMs?: number;
@@ -17,7 +18,7 @@ export interface ServerOptions {
17
18
  export function createServer(docker: Dockerode, options?: ServerOptions): McpServer {
18
19
  const server = new McpServer({
19
20
  name: "docker-mcp-server",
20
- version: "0.3.2",
21
+ version: "0.3.3",
21
22
  });
22
23
 
23
24
  // Register all tool categories
@@ -30,6 +31,7 @@ export function createServer(docker: Dockerode, options?: ServerOptions): McpSer
30
31
  registerNetworkTools(server, docker);
31
32
  registerVolumeTools(server, docker);
32
33
  registerMonitoringTools(server, docker);
34
+ registerSystemTools(server, docker);
33
35
 
34
36
  return server;
35
37
  }
@@ -0,0 +1,169 @@
1
+ import Dockerode from "dockerode";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { DockerInfoSchema, DiskUsageSchema } from "../types.js";
4
+ import { formatError, withRetry } from "../docker.js";
5
+
6
+ interface DockerInfoResult {
7
+ ServerVersion: string;
8
+ OperatingSystem: string;
9
+ KernelVersion: string;
10
+ Architecture: string;
11
+ NCPU: number;
12
+ MemTotal: number;
13
+ DockerRootDir: string;
14
+ Driver: string;
15
+ ContainersRunning: number;
16
+ ContainersStopped: number;
17
+ ContainersPaused: number;
18
+ Images: number;
19
+ Labels: string[];
20
+ ID: string;
21
+ }
22
+
23
+ interface DiskUsageImage {
24
+ Id: string;
25
+ RepoTags: string[];
26
+ Size: number;
27
+ Containers: number;
28
+ }
29
+
30
+ interface DiskUsageContainer {
31
+ Id: string;
32
+ Name: string;
33
+ Image: string;
34
+ Size: number;
35
+ Reclaimable: boolean;
36
+ }
37
+
38
+ interface DiskUsageVolume {
39
+ Name: string;
40
+ Size: number;
41
+ Reclaimable: boolean;
42
+ }
43
+
44
+ interface DiskUsageBuildCache {
45
+ ID: string;
46
+ Type: string;
47
+ Description: string;
48
+ Size: number;
49
+ InUse: boolean;
50
+ }
51
+
52
+ interface DiskUsageResult {
53
+ LayersSize: number;
54
+ Images: DiskUsageImage[];
55
+ Containers: DiskUsageContainer[];
56
+ Volumes: DiskUsageVolume[];
57
+ BuildCache: DiskUsageBuildCache[];
58
+ }
59
+
60
+ export function registerSystemTools(server: McpServer, docker: Dockerode): void {
61
+ server.tool(
62
+ "docker_info",
63
+ "Get Docker daemon system information: server version, OS, kernel, CPU count, memory total, storage driver, and running container/image counts.",
64
+ DockerInfoSchema.shape,
65
+ { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
66
+ async (params) => {
67
+ try {
68
+ const info = (await withRetry(() => docker.info(), { label: "docker_info" })) as DockerInfoResult;
69
+ return {
70
+ content: [{
71
+ type: "text",
72
+ text: JSON.stringify({
73
+ server_version: info.ServerVersion,
74
+ os: info.OperatingSystem,
75
+ kernel: info.KernelVersion,
76
+ architecture: info.Architecture,
77
+ cpus: info.NCPU,
78
+ memory_total: info.MemTotal,
79
+ memory_total_human: formatBytes(info.MemTotal),
80
+ docker_root: info.DockerRootDir,
81
+ storage_driver: info.Driver,
82
+ containers_running: info.ContainersRunning,
83
+ containers_stopped: info.ContainersStopped,
84
+ containers_paused: info.ContainersPaused,
85
+ images: info.Images,
86
+ labels: info.Labels,
87
+ server_id: info.ID,
88
+ }, null, 2),
89
+ }],
90
+ };
91
+ } catch (error) {
92
+ return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
93
+ }
94
+ }
95
+ );
96
+
97
+ server.tool(
98
+ "disk_usage",
99
+ "Get Docker disk usage breakdown: space used by images, containers, volumes, and build cache. Shows total and reclaimable space.",
100
+ DiskUsageSchema.shape,
101
+ { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
102
+ async (params) => {
103
+ try {
104
+ const df = (await withRetry(() => docker.df(), { label: "disk_usage" })) as DiskUsageResult;
105
+
106
+ const images = (df.Images || []).map((img) => ({
107
+ id: img.Id?.substring(0, 19),
108
+ tags: img.RepoTags || [],
109
+ size: img.Size,
110
+ size_human: formatBytes(img.Size),
111
+ containers: img.Containers,
112
+ }));
113
+
114
+ const containers = (df.Containers || []).map((c) => ({
115
+ id: c.Id?.substring(0, 12),
116
+ name: c.Name,
117
+ image: c.Image,
118
+ size: c.Size,
119
+ size_human: formatBytes(c.Size),
120
+ reclaimable: c.Reclaimable,
121
+ }));
122
+
123
+ const volumes = (df.Volumes || []).map((v) => ({
124
+ name: v.Name,
125
+ size: v.Size,
126
+ size_human: formatBytes(v.Size),
127
+ reclaimable: v.Reclaimable,
128
+ }));
129
+
130
+ const buildCache = (df.BuildCache || []).map((bc) => ({
131
+ id: bc.ID,
132
+ type: bc.Type,
133
+ description: bc.Description?.substring(0, 120),
134
+ size: bc.Size,
135
+ size_human: formatBytes(bc.Size),
136
+ in_use: bc.InUse,
137
+ }));
138
+
139
+ return {
140
+ content: [{
141
+ type: "text",
142
+ text: JSON.stringify({
143
+ summary: {
144
+ images: { count: df.LayersSize ? images.length : 0, total_size: df.LayersSize, total_human: formatBytes(df.LayersSize || 0) },
145
+ containers: { count: containers.length },
146
+ volumes: { count: volumes.length },
147
+ build_cache: { count: buildCache.length, total_human: formatBytes(buildCache.reduce((sum, bc) => sum + (bc.size || 0), 0)) },
148
+ },
149
+ images,
150
+ containers,
151
+ volumes,
152
+ build_cache: buildCache.slice(0, 10), // Top 10 only
153
+ }, null, 2),
154
+ }],
155
+ };
156
+ } catch (error) {
157
+ return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
158
+ }
159
+ }
160
+ );
161
+ }
162
+
163
+ function formatBytes(bytes: number): string {
164
+ if (bytes === 0) return "0 B";
165
+ const k = 1024;
166
+ const sizes = ["B", "KB", "MB", "GB", "TB"];
167
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
168
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
169
+ }
package/src/types.ts CHANGED
@@ -215,4 +215,9 @@ export const ResourceAlertCheckSchema = z.object({
215
215
  restart_count: z.number().optional().describe("Alert if restart count exceeds this (default: 5)"),
216
216
  });
217
217
 
218
- export const MonitorDashboardSchema = z.object({});
218
+ export const MonitorDashboardSchema = z.object({});
219
+
220
+ // System info schemas (v0.3.3)
221
+ export const DockerInfoSchema = z.object({});
222
+
223
+ export const DiskUsageSchema = z.object({});
@@ -0,0 +1,197 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ // Mock Dockerode before importing the module under test
4
+ const mockInfo = vi.fn();
5
+ const mockDf = vi.fn();
6
+
7
+ vi.mock("dockerode", () => {
8
+ return {
9
+ default: vi.fn().mockImplementation(() => ({
10
+ info: mockInfo,
11
+ df: mockDf,
12
+ })),
13
+ };
14
+ });
15
+
16
+ import { registerSystemTools } from "../src/tools/system.js";
17
+
18
+ // Minimal MCP server mock
19
+ function createMockServer() {
20
+ const tools: Record<string, { description: string; handler: Function }> = {};
21
+ return {
22
+ tool: (name: string, description: string, _schema: unknown, _hints: unknown, handler: Function) => {
23
+ tools[name] = { description, handler };
24
+ },
25
+ tools,
26
+ };
27
+ }
28
+
29
+ describe("System Tools", () => {
30
+ let server: ReturnType<typeof createMockServer>;
31
+ let docker: any;
32
+
33
+ beforeEach(() => {
34
+ vi.restoreAllMocks();
35
+ server = createMockServer();
36
+ docker = {
37
+ info: mockInfo,
38
+ df: mockDf,
39
+ };
40
+ registerSystemTools(server as any, docker);
41
+ vi.clearAllMocks();
42
+ });
43
+
44
+ describe("docker_info", () => {
45
+ it("should register docker_info tool", () => {
46
+ expect(server.tools["docker_info"]).toBeDefined();
47
+ expect(server.tools["docker_info"].description).toContain("system information");
48
+ });
49
+
50
+ it("should return formatted Docker info", async () => {
51
+ mockInfo.mockResolvedValue({
52
+ ServerVersion: "29.5.3",
53
+ OperatingSystem: "Ubuntu 26.04 LTS",
54
+ KernelVersion: "7.0.0-15-generic",
55
+ Architecture: "x86_64",
56
+ NCPU: 2,
57
+ MemTotal: 4000079872,
58
+ DockerRootDir: "/var/lib/docker",
59
+ Driver: "overlay2",
60
+ ContainersRunning: 3,
61
+ ContainersStopped: 5,
62
+ ContainersPaused: 0,
63
+ Images: 12,
64
+ Labels: ["com.docker.compose.version=2.29.1"],
65
+ ID: "ABC1:DEF2:GHI3:JKL4:MNO5:PQR6:STUV:WXYZ:1234:5678:ABCD:EF90:1234:5678",
66
+ });
67
+
68
+ const result = await server.tools["docker_info"].handler({});
69
+ const data = JSON.parse(result.content[0].text);
70
+
71
+ expect(data.server_version).toBe("29.5.3");
72
+ expect(data.os).toBe("Ubuntu 26.04 LTS");
73
+ expect(data.kernel).toBe("7.0.0-15-generic");
74
+ expect(data.cpus).toBe(2);
75
+ expect(data.memory_total_human).toBe("3.7 GB");
76
+ expect(data.containers_running).toBe(3);
77
+ expect(data.containers_stopped).toBe(5);
78
+ expect(data.images).toBe(12);
79
+ expect(data.storage_driver).toBe("overlay2");
80
+ });
81
+
82
+ it("should handle errors", async () => {
83
+ mockInfo.mockRejectedValue(new Error("Cannot connect to Docker daemon"));
84
+
85
+ const result = await server.tools["docker_info"].handler({});
86
+
87
+ expect(result.isError).toBe(true);
88
+ expect(result.content[0].text).toContain("Error");
89
+ });
90
+ });
91
+
92
+ describe("disk_usage", () => {
93
+ it("should register disk_usage tool", () => {
94
+ expect(server.tools["disk_usage"]).toBeDefined();
95
+ expect(server.tools["disk_usage"].description).toContain("disk usage");
96
+ });
97
+
98
+ it("should return formatted disk usage", async () => {
99
+ mockDf.mockResolvedValue({
100
+ LayersSize: 623591029,
101
+ Images: [
102
+ {
103
+ Id: "sha256:a6894d60f28f051f4c3e44a6b5f0b669023fc47ea936355d65e5fcc10856767f",
104
+ RepoTags: ["nginx:latest"],
105
+ Size: 395120924,
106
+ Containers: 1,
107
+ },
108
+ {
109
+ Id: "sha256:b2894d60f28f051f4c3e44a6b5f0b669023fc47ea936355d65e5fcc10856767g",
110
+ RepoTags: ["alpine:latest"],
111
+ Size: 13068376,
112
+ Containers: 0,
113
+ },
114
+ ],
115
+ Containers: [
116
+ {
117
+ Id: "abc123def456",
118
+ Name: "web-app",
119
+ Image: "nginx:latest",
120
+ Size: 1048576,
121
+ Reclaimable: true,
122
+ },
123
+ ],
124
+ Volumes: [
125
+ {
126
+ Name: "data-vol",
127
+ Size: 52428800,
128
+ Reclaimable: false,
129
+ },
130
+ ],
131
+ BuildCache: [
132
+ {
133
+ ID: "cache1",
134
+ Type: "regular",
135
+ Description: "pulled from docker.io/library/node:22",
136
+ Size: 9032241,
137
+ InUse: false,
138
+ },
139
+ {
140
+ ID: "cache2",
141
+ Type: "regular",
142
+ Description: "COPY package.json",
143
+ Size: 156559,
144
+ InUse: false,
145
+ },
146
+ ],
147
+ });
148
+
149
+ const result = await server.tools["disk_usage"].handler({});
150
+ const data = JSON.parse(result.content[0].text);
151
+
152
+ expect(data.summary.images.count).toBe(2);
153
+ expect(data.summary.images.total_human).toMatch(/^594\.\d MB$/);
154
+ expect(data.summary.containers.count).toBe(1);
155
+ expect(data.summary.volumes.count).toBe(1);
156
+ expect(data.summary.build_cache.count).toBe(2);
157
+
158
+ // Check image details
159
+ expect(data.images[0].tags).toContain("nginx:latest");
160
+ expect(data.images[0].size_human).toMatch(/^376\.\d MB$/);
161
+
162
+ // Check container details
163
+ expect(data.containers[0].name).toBe("web-app");
164
+ expect(data.containers[0].size_human).toBe("1 MB");
165
+
166
+ // Check volume details
167
+ expect(data.volumes[0].name).toBe("data-vol");
168
+ expect(data.volumes[0].size_human).toBe("50 MB");
169
+ });
170
+
171
+ it("should handle empty disk usage", async () => {
172
+ mockDf.mockResolvedValue({
173
+ LayersSize: 0,
174
+ Images: [],
175
+ Containers: [],
176
+ Volumes: [],
177
+ BuildCache: [],
178
+ });
179
+
180
+ const result = await server.tools["disk_usage"].handler({});
181
+ const data = JSON.parse(result.content[0].text);
182
+
183
+ expect(data.summary.images.count).toBe(0);
184
+ expect(data.summary.containers.count).toBe(0);
185
+ expect(data.images).toEqual([]);
186
+ });
187
+
188
+ it("should handle errors", async () => {
189
+ mockDf.mockRejectedValue(new Error("Docker API error"));
190
+
191
+ const result = await server.tools["disk_usage"].handler({});
192
+
193
+ expect(result.isError).toBe(true);
194
+ expect(result.content[0].text).toContain("Error");
195
+ });
196
+ });
197
+ });