@supernova123/docker-mcp-server 0.1.5 → 0.2.0

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.
@@ -0,0 +1,411 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ // Mock Dockerode before importing the module under test
4
+ const mockListContainers = vi.fn();
5
+ const mockInspect = vi.fn();
6
+ const mockStats = vi.fn();
7
+ const mockGetEvents = vi.fn();
8
+ const mockLogs = vi.fn();
9
+
10
+ vi.mock("dockerode", () => {
11
+ return {
12
+ default: vi.fn().mockImplementation(() => ({
13
+ listContainers: mockListContainers,
14
+ getContainer: vi.fn().mockReturnValue({
15
+ inspect: mockInspect,
16
+ stats: mockStats,
17
+ logs: mockLogs,
18
+ }),
19
+ getEvents: mockGetEvents,
20
+ })),
21
+ };
22
+ });
23
+
24
+ import { registerMonitoringTools } from "../src/tools/monitoring.js";
25
+
26
+ // Minimal MCP server mock (same pattern as container.test.ts)
27
+ function createMockServer() {
28
+ const tools: Record<string, { description: string; handler: Function }> = {};
29
+ return {
30
+ tool: (name: string, description: string, _schema: unknown, handler: Function) => {
31
+ tools[name] = { description, handler };
32
+ },
33
+ tools,
34
+ };
35
+ }
36
+
37
+ // Helper: mock container list
38
+ function mockContainers(ids: string[], names: string[]) {
39
+ mockListContainers.mockResolvedValue(
40
+ ids.map((id, i) => ({
41
+ Id: id,
42
+ Names: [names[i] || `/container-${i}`],
43
+ Image: "nginx:latest",
44
+ State: "running",
45
+ Status: "Up 1 hour",
46
+ }))
47
+ );
48
+ }
49
+
50
+ // Helper: mock inspect result
51
+ function mockInspectResult(overrides: Record<string, any> = {}) {
52
+ return {
53
+ Id: "abc123",
54
+ State: {
55
+ Running: true,
56
+ StartedAt: "2026-06-15T10:00:00Z",
57
+ Health: { Status: "healthy" },
58
+ },
59
+ RestartCount: 0,
60
+ Name: "/test-container",
61
+ ...overrides,
62
+ };
63
+ }
64
+
65
+ // Helper: mock stats result
66
+ function mockStatsResult(overrides: Record<string, any> = {}) {
67
+ return {
68
+ cpu_stats: {
69
+ cpu_usage: { total_usage: 1000000 },
70
+ system_cpu_usage: 10000000,
71
+ online_cpus: 2,
72
+ },
73
+ precpu_stats: {
74
+ cpu_usage: { total_usage: 900000 },
75
+ system_cpu_usage: 9500000,
76
+ },
77
+ memory_stats: {
78
+ usage: 100 * 1024 * 1024, // 100MB
79
+ limit: 1024 * 1024 * 1024, // 1GB
80
+ },
81
+ networks: {
82
+ eth0: { rx_bytes: 1024 * 1024, tx_bytes: 512 * 1024 },
83
+ },
84
+ ...overrides,
85
+ };
86
+ }
87
+
88
+ describe("Monitoring Tools", () => {
89
+ let server: ReturnType<typeof createMockServer>;
90
+
91
+ beforeEach(() => {
92
+ vi.restoreAllMocks();
93
+ server = createMockServer();
94
+ // Create a fresh docker-like object with direct mock references
95
+ const docker = {
96
+ listContainers: mockListContainers,
97
+ getContainer: (id: string) => ({
98
+ inspect: mockInspect,
99
+ stats: mockStats,
100
+ logs: mockLogs,
101
+ }),
102
+ getEvents: mockGetEvents,
103
+ } as any;
104
+ registerMonitoringTools(server, docker);
105
+ });
106
+
107
+ describe("fleet_status", () => {
108
+ it("returns health status for all running containers", async () => {
109
+ mockContainers(["abc123", "def456"], ["web", "db"]);
110
+ mockInspect
111
+ .mockResolvedValueOnce(mockInspectResult({ RestartCount: 2 }))
112
+ .mockResolvedValueOnce(mockInspectResult({ RestartCount: 0, State: { Running: true, StartedAt: "2026-06-15T11:00:00Z", Health: { Status: "unhealthy" } } }));
113
+
114
+ const result = await server.tools["fleet_status"].handler({});
115
+ const data = JSON.parse(result.content[0].text);
116
+
117
+ expect(data).toHaveLength(2);
118
+ expect(data[0].name).toBe("web");
119
+ expect(data[0].id).toBe("abc123");
120
+ expect(data[0].health).toBe("healthy");
121
+ expect(data[0].restartCount).toBe(2);
122
+ expect(data[1].health).toBe("unhealthy");
123
+ });
124
+
125
+ it("handles containers with no healthcheck", async () => {
126
+ mockContainers(["abc123"], ["no-health"]);
127
+ mockInspect.mockResolvedValue(mockInspectResult({
128
+ State: { Running: true, StartedAt: "2026-06-15T10:00:00Z" },
129
+ }));
130
+
131
+ const result = await server.tools["fleet_status"].handler({});
132
+ const data = JSON.parse(result.content[0].text);
133
+
134
+ expect(data[0].health).toBe("no-healthcheck");
135
+ });
136
+
137
+ it("returns error on Docker failure", async () => {
138
+ mockListContainers.mockRejectedValue(new Error("Docker daemon not running"));
139
+ const result = await server.tools["fleet_status"].handler({});
140
+
141
+ expect(result.isError).toBe(true);
142
+ expect(result.content[0].text).toContain("Docker daemon not running");
143
+ });
144
+ });
145
+
146
+ describe("fleet_stats", () => {
147
+ it("returns resource usage sorted by CPU by default", async () => {
148
+ mockContainers(["abc123", "def456"], ["low-cpu", "high-cpu"]);
149
+ mockStats
150
+ .mockResolvedValueOnce(mockStatsResult({
151
+ cpu_stats: { cpu_usage: { total_usage: 100000 }, system_cpu_usage: 10000000, online_cpus: 2 },
152
+ precpu_stats: { cpu_usage: { total_usage: 90000 }, system_cpu_usage: 9500000 },
153
+ }))
154
+ .mockResolvedValueOnce(mockStatsResult({
155
+ cpu_stats: { cpu_usage: { total_usage: 500000 }, system_cpu_usage: 10000000, online_cpus: 2 },
156
+ precpu_stats: { cpu_usage: { total_usage: 100000 }, system_cpu_usage: 9500000 },
157
+ }));
158
+
159
+ const result = await server.tools["fleet_stats"].handler({});
160
+ const data = JSON.parse(result.content[0].text);
161
+
162
+ expect(data).toHaveLength(2);
163
+ // high-cpu should be first (sorted by CPU desc)
164
+ expect(data[0].name).toBe("high-cpu");
165
+ expect(data[0].cpu_percent).toBeGreaterThan(data[1].cpu_percent);
166
+ expect(data[0].memory_usage_mb).toBeGreaterThan(0);
167
+ expect(data[0].network_rx_mb).toBeGreaterThanOrEqual(0);
168
+ });
169
+
170
+ it("sorts by memory when requested", async () => {
171
+ mockContainers(["abc123", "def456"], ["low-mem", "high-mem"]);
172
+ mockStats
173
+ .mockResolvedValueOnce(mockStatsResult({
174
+ memory_stats: { usage: 50 * 1024 * 1024, limit: 1024 * 1024 * 1024 },
175
+ }))
176
+ .mockResolvedValueOnce(mockStatsResult({
177
+ memory_stats: { usage: 900 * 1024 * 1024, limit: 1024 * 1024 * 1024 },
178
+ }));
179
+
180
+ const result = await server.tools["fleet_stats"].handler({ sort_by: "memory" });
181
+ const data = JSON.parse(result.content[0].text);
182
+
183
+ expect(data[0].name).toBe("high-mem");
184
+ expect(data[0].memory_percent).toBeGreaterThan(data[1].memory_percent);
185
+ });
186
+
187
+ it("returns error on Docker failure", async () => {
188
+ mockListContainers.mockRejectedValue(new Error("Cannot connect"));
189
+ const result = await server.tools["fleet_stats"].handler({});
190
+
191
+ expect(result.isError).toBe(true);
192
+ expect(result.content[0].text).toContain("Cannot connect");
193
+ });
194
+ });
195
+
196
+ describe("watch_events", () => {
197
+ it("collects events within time window", async () => {
198
+ const { Readable } = await import("stream");
199
+ const event1 = JSON.stringify({ Type: "container", Action: "start", Actor: { Attributes: { name: "web" }, ID: "abc123" }, time: Math.floor(Date.now() / 1000) });
200
+ const event2 = JSON.stringify({ Type: "container", Action: "stop", Actor: { Attributes: { name: "web" }, ID: "abc123" }, time: Math.floor(Date.now() / 1000) });
201
+
202
+ const stream = new Readable({
203
+ read() {
204
+ this.push(event1 + "\n");
205
+ this.push(event2 + "\n");
206
+ this.push(null); // end stream
207
+ },
208
+ });
209
+ mockGetEvents.mockResolvedValue(stream);
210
+
211
+ const result = await server.tools["watch_events"].handler({ duration: 5 });
212
+ const data = JSON.parse(result.content[0].text);
213
+
214
+ expect(data).toHaveLength(2);
215
+ expect(data[0].type).toBe("container");
216
+ expect(data[0].action).toBe("start");
217
+ expect(data[0].container).toBe("web");
218
+ });
219
+
220
+ it("returns message when no events captured", async () => {
221
+ const { Readable } = await import("stream");
222
+ const stream = new Readable({ read() { this.push(null); } });
223
+ mockGetEvents.mockResolvedValue(stream);
224
+
225
+ const result = await server.tools["watch_events"].handler({ duration: 1 });
226
+ expect(result.content[0].text).toBe("No events captured in the time window.");
227
+ });
228
+
229
+ it("returns error on Docker failure", async () => {
230
+ mockGetEvents.mockRejectedValue(new Error("Docker socket not found"));
231
+ const result = await server.tools["watch_events"].handler({});
232
+
233
+ expect(result.isError).toBe(true);
234
+ expect(result.content[0].text).toContain("Docker socket not found");
235
+ });
236
+ });
237
+
238
+ describe("search_logs", () => {
239
+ it("searches logs with regex pattern", async () => {
240
+ mockContainers(["abc123"], ["web"]);
241
+ mockInspect.mockResolvedValue(mockInspectResult());
242
+ // Docker logs return Buffer with 8-byte header per line
243
+ const logLines = Buffer.from("2026-06-15 ERROR connection refused\n2026-06-15 INFO server started\n2026-06-15 ERROR timeout\n");
244
+ mockLogs.mockResolvedValue(logLines);
245
+
246
+ const result = await server.tools["search_logs"].handler({ pattern: "ERROR" });
247
+ const data = JSON.parse(result.content[0].text);
248
+
249
+ expect(data).toHaveLength(2);
250
+ expect(data[0].container).toBe("web");
251
+ expect(data[0].line).toContain("ERROR");
252
+ });
253
+
254
+ it("searches across multiple containers when no specific containers given", async () => {
255
+ mockContainers(["abc123", "def456"], ["web", "db"]);
256
+ mockInspect
257
+ .mockResolvedValueOnce(mockInspectResult())
258
+ .mockResolvedValueOnce(mockInspectResult());
259
+ mockLogs
260
+ .mockResolvedValueOnce(Buffer.from("2026-06-15 ERROR web error\n"))
261
+ .mockResolvedValueOnce(Buffer.from("2026-06-15 INFO db ready\n"));
262
+
263
+ const result = await server.tools["search_logs"].handler({ pattern: "ERROR" });
264
+ const data = JSON.parse(result.content[0].text);
265
+
266
+ expect(data).toHaveLength(1);
267
+ expect(data[0].container).toBe("web");
268
+ });
269
+
270
+ it("returns no matches message when pattern not found", async () => {
271
+ mockContainers(["abc123"], ["web"]);
272
+ mockInspect.mockResolvedValue(mockInspectResult());
273
+ mockLogs.mockResolvedValue(Buffer.from("2026-06-15 INFO all good\n"));
274
+
275
+ const result = await server.tools["search_logs"].handler({ pattern: "CRITICAL" });
276
+ expect(result.content[0].text).toBe("No matches found.");
277
+ });
278
+
279
+ it("supports case-insensitive matching", async () => {
280
+ mockContainers(["abc123"], ["web"]);
281
+ mockInspect.mockResolvedValue(mockInspectResult());
282
+ mockLogs.mockResolvedValue(Buffer.from("2026-06-15 Error lowercase\n"));
283
+
284
+ const result = await server.tools["search_logs"].handler({ pattern: "error", ignore_case: true });
285
+ const data = JSON.parse(result.content[0].text);
286
+
287
+ expect(data).toHaveLength(1);
288
+ });
289
+
290
+ it("returns error on Docker failure", async () => {
291
+ mockListContainers.mockRejectedValue(new Error("Cannot connect"));
292
+ const result = await server.tools["search_logs"].handler({ pattern: "ERROR" });
293
+
294
+ expect(result.isError).toBe(true);
295
+ expect(result.content[0].text).toContain("Cannot connect");
296
+ });
297
+ });
298
+
299
+ describe("check_thresholds", () => {
300
+ it("returns violations when containers exceed thresholds", async () => {
301
+ mockContainers(["abc123"], ["high-cpu"]);
302
+ mockInspect.mockResolvedValue(mockInspectResult({ RestartCount: 10 }));
303
+ mockStats.mockResolvedValue(mockStatsResult({
304
+ cpu_stats: { cpu_usage: { total_usage: 9000000 }, system_cpu_usage: 10000000, online_cpus: 2 },
305
+ precpu_stats: { cpu_usage: { total_usage: 1000000 }, system_cpu_usage: 9500000 },
306
+ }));
307
+
308
+ const result = await server.tools["check_thresholds"].handler({ cpu_percent: 80, restart_count: 5 });
309
+ const data = JSON.parse(result.content[0].text);
310
+
311
+ expect(data.violations).toHaveLength(1);
312
+ expect(data.violations[0].container).toBe("high-cpu");
313
+ expect(data.violations[0].issues.length).toBeGreaterThan(0);
314
+ expect(data.violations[0].issues.some((i: string) => i.includes("restarts"))).toBe(true);
315
+ expect(data.violations[0].issues.some((i: string) => i.includes("cpu"))).toBe(true);
316
+ });
317
+
318
+ it("returns all-clear when within thresholds", async () => {
319
+ mockContainers(["abc123"], ["healthy"]);
320
+ mockInspect.mockResolvedValue(mockInspectResult({ RestartCount: 0 }));
321
+ mockStats.mockResolvedValue(mockStatsResult({
322
+ cpu_stats: { cpu_usage: { total_usage: 100000 }, system_cpu_usage: 10000000, online_cpus: 2 },
323
+ precpu_stats: { cpu_usage: { total_usage: 90000 }, system_cpu_usage: 9500000 },
324
+ }));
325
+
326
+ const result = await server.tools["check_thresholds"].handler({});
327
+ const data = JSON.parse(result.content[0].text);
328
+
329
+ expect(data.message).toContain("All containers within thresholds");
330
+ expect(data.checked).toBe(1);
331
+ });
332
+
333
+ it("uses default thresholds when not specified", async () => {
334
+ mockContainers(["abc123"], ["ok"]);
335
+ mockInspect.mockResolvedValue(mockInspectResult({ RestartCount: 4 })); // < 5 default
336
+ mockStats.mockResolvedValue(mockStatsResult({
337
+ cpu_stats: { cpu_usage: { total_usage: 100000 }, system_cpu_usage: 10000000, online_cpus: 2 },
338
+ precpu_stats: { cpu_usage: { total_usage: 90000 }, system_cpu_usage: 9500000 },
339
+ }));
340
+
341
+ const result = await server.tools["check_thresholds"].handler({});
342
+ const data = JSON.parse(result.content[0].text);
343
+
344
+ expect(data.message).toContain("All containers within thresholds");
345
+ });
346
+
347
+ it("returns error on Docker failure", async () => {
348
+ mockListContainers.mockRejectedValue(new Error("Daemon down"));
349
+ const result = await server.tools["check_thresholds"].handler({});
350
+
351
+ expect(result.isError).toBe(true);
352
+ expect(result.content[0].text).toContain("Daemon down");
353
+ });
354
+ });
355
+
356
+ describe("monitor_dashboard", () => {
357
+ it("returns comprehensive fleet summary", async () => {
358
+ mockContainers(["abc123", "def456"], ["web", "db"]);
359
+ mockInspect
360
+ .mockResolvedValueOnce(mockInspectResult({ RestartCount: 0 }))
361
+ .mockResolvedValueOnce(mockInspectResult({ RestartCount: 3, State: { Running: true, StartedAt: "2026-06-15T10:00:00Z", Health: { Status: "unhealthy" } } }));
362
+ mockStats
363
+ .mockResolvedValueOnce(mockStatsResult({
364
+ cpu_stats: { cpu_usage: { total_usage: 200000 }, system_cpu_usage: 10000000, online_cpus: 2 },
365
+ precpu_stats: { cpu_usage: { total_usage: 100000 }, system_cpu_usage: 9500000 },
366
+ }))
367
+ .mockResolvedValueOnce(mockStatsResult({
368
+ cpu_stats: { cpu_usage: { total_usage: 5000000 }, system_cpu_usage: 10000000, online_cpus: 2 },
369
+ precpu_stats: { cpu_usage: { total_usage: 1000000 }, system_cpu_usage: 9500000 },
370
+ }));
371
+
372
+ // Mock events (empty stream)
373
+ const { Readable } = await import("stream");
374
+ const eventStream = new Readable({ read() { this.push(null); } });
375
+ mockGetEvents.mockResolvedValue(eventStream);
376
+
377
+ const result = await server.tools["monitor_dashboard"].handler({});
378
+ const data = JSON.parse(result.content[0].text);
379
+
380
+ expect(data.summary.total_containers).toBe(2);
381
+ expect(data.summary.running).toBe(2);
382
+ expect(data.summary.unhealthy).toBe(1);
383
+ expect(data.health).toHaveLength(2);
384
+ expect(data.top_cpu_consumers).toHaveLength(2);
385
+ // db should be first (higher CPU)
386
+ expect(data.top_cpu_consumers[0].name).toBe("db");
387
+ });
388
+
389
+ it("handles empty fleet", async () => {
390
+ mockListContainers.mockResolvedValue([]);
391
+ const { Readable } = await import("stream");
392
+ const eventStream = new Readable({ read() { this.push(null); } });
393
+ mockGetEvents.mockResolvedValue(eventStream);
394
+
395
+ const result = await server.tools["monitor_dashboard"].handler({});
396
+ const data = JSON.parse(result.content[0].text);
397
+
398
+ expect(data.summary.total_containers).toBe(0);
399
+ expect(data.summary.running).toBe(0);
400
+ expect(data.health).toHaveLength(0);
401
+ });
402
+
403
+ it("returns error on Docker failure", async () => {
404
+ mockListContainers.mockRejectedValue(new Error("Connection refused"));
405
+ const result = await server.tools["monitor_dashboard"].handler({});
406
+
407
+ expect(result.isError).toBe(true);
408
+ expect(result.content[0].text).toContain("Connection refused");
409
+ });
410
+ });
411
+ });