@supernova123/docker-mcp-server 0.3.0 → 0.3.1

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/dist/docker.d.ts CHANGED
@@ -5,6 +5,35 @@ export interface DockerClientOptions {
5
5
  port?: number;
6
6
  }
7
7
  export declare function createDockerClient(options?: DockerClientOptions): Dockerode;
8
+ /**
9
+ * Structured error types for programmatic error classification.
10
+ * Helps AI clients decide whether to retry (retryable) or report (permanent).
11
+ */
12
+ export declare class DockerConnectionError extends Error {
13
+ retryable: boolean;
14
+ constructor(message: string);
15
+ }
16
+ export declare class DockerTimeoutError extends Error {
17
+ retryable: boolean;
18
+ constructor(message: string);
19
+ }
20
+ export declare class DockerPermissionError extends Error {
21
+ retryable: boolean;
22
+ constructor(message: string);
23
+ }
24
+ /**
25
+ * Check Docker daemon connectivity at startup.
26
+ * Returns a structured error if Docker is unreachable.
27
+ */
28
+ export declare function checkDockerConnection(docker: Dockerode): Promise<{
29
+ ok: boolean;
30
+ error?: string;
31
+ }>;
32
+ /**
33
+ * Run a Docker API call with a configurable timeout.
34
+ * Returns structured error on timeout instead of hanging indefinitely.
35
+ */
36
+ export declare function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T>;
8
37
  export declare function formatError(error: unknown): string;
9
38
  export declare function formatContainer(container: Dockerode.ContainerInfo): Record<string, unknown>;
10
39
  export declare function formatImage(image: Dockerode.ImageInfo): Record<string, unknown>;
package/dist/docker.js CHANGED
@@ -9,7 +9,77 @@ export function createDockerClient(options) {
9
9
  // Default: local socket
10
10
  return new Dockerode({ socketPath: "/var/run/docker.sock" });
11
11
  }
12
+ /**
13
+ * Structured error types for programmatic error classification.
14
+ * Helps AI clients decide whether to retry (retryable) or report (permanent).
15
+ */
16
+ export class DockerConnectionError extends Error {
17
+ retryable = false;
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = "DockerConnectionError";
21
+ }
22
+ }
23
+ export class DockerTimeoutError extends Error {
24
+ retryable = true;
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = "DockerTimeoutError";
28
+ }
29
+ }
30
+ export class DockerPermissionError extends Error {
31
+ retryable = false;
32
+ constructor(message) {
33
+ super(message);
34
+ this.name = "DockerPermissionError";
35
+ }
36
+ }
37
+ /**
38
+ * Check Docker daemon connectivity at startup.
39
+ * Returns a structured error if Docker is unreachable.
40
+ */
41
+ export async function checkDockerConnection(docker) {
42
+ try {
43
+ await docker.ping();
44
+ return { ok: true };
45
+ }
46
+ catch (error) {
47
+ const msg = error instanceof Error ? error.message : String(error);
48
+ if (msg.includes("EACCES") || msg.includes("Permission denied")) {
49
+ return {
50
+ ok: false,
51
+ error: `DockerConnectionError: Cannot access Docker socket — ${msg}. Fix: add user to docker group (sudo usermod -aG docker $USER) or run as root.`,
52
+ };
53
+ }
54
+ if (msg.includes("ENOENT") || msg.includes("no such file")) {
55
+ return {
56
+ ok: false,
57
+ error: `DockerConnectionError: Docker socket not found at /var/run/docker.sock — ${msg}. Fix: install Docker (https://docs.docker.com/engine/install/) and ensure the daemon is running.`,
58
+ };
59
+ }
60
+ return {
61
+ ok: false,
62
+ error: `DockerConnectionError: Cannot connect to Docker daemon — ${msg}. Fix: ensure Docker is running (systemctl status docker).`,
63
+ };
64
+ }
65
+ }
66
+ /**
67
+ * Run a Docker API call with a configurable timeout.
68
+ * Returns structured error on timeout instead of hanging indefinitely.
69
+ */
70
+ export async function withTimeout(promise, ms, label) {
71
+ return Promise.race([
72
+ promise,
73
+ new Promise((_, reject) => setTimeout(() => reject(new DockerTimeoutError(`Docker API call "${label}" timed out after ${ms}ms`)), ms)),
74
+ ]);
75
+ }
12
76
  export function formatError(error) {
77
+ if (error instanceof DockerConnectionError)
78
+ return `${error.name}: ${error.message}`;
79
+ if (error instanceof DockerTimeoutError)
80
+ return `${error.name}: ${error.message}`;
81
+ if (error instanceof DockerPermissionError)
82
+ return `${error.name}: ${error.message}`;
13
83
  if (error instanceof Error)
14
84
  return error.message;
15
85
  if (typeof error === "string")
package/dist/index.js CHANGED
@@ -1,10 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { createDockerClient } from "./docker.js";
3
+ import { createDockerClient, checkDockerConnection } from "./docker.js";
4
4
  import { createServer } from "./server.js";
5
+ const DEFAULT_TIMEOUT_MS = 30_000;
5
6
  async function main() {
6
7
  const docker = createDockerClient();
7
- const server = createServer(docker);
8
+ // Startup health check: verify Docker daemon is reachable
9
+ const health = await checkDockerConnection(docker);
10
+ if (!health.ok) {
11
+ process.stderr.write(`${health.error}\n`);
12
+ process.exit(1);
13
+ }
14
+ process.stderr.write("Docker daemon reachable\n");
15
+ const server = createServer(docker, { timeoutMs: DEFAULT_TIMEOUT_MS });
8
16
  const transport = new StdioServerTransport();
9
17
  await server.connect(transport);
10
18
  process.stderr.write("Docker MCP Server running on stdio\n");
package/dist/server.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import Dockerode from "dockerode";
3
- export declare function createServer(docker: Dockerode): McpServer;
3
+ export interface ServerOptions {
4
+ timeoutMs?: number;
5
+ }
6
+ export declare function createServer(docker: Dockerode, options?: ServerOptions): McpServer;
4
7
  //# sourceMappingURL=server.d.ts.map
package/dist/server.js CHANGED
@@ -8,10 +8,10 @@ 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
- export function createServer(docker) {
11
+ export function createServer(docker, options) {
12
12
  const server = new McpServer({
13
13
  name: "docker-mcp-server",
14
- version: "0.3.0",
14
+ version: "0.3.1",
15
15
  });
16
16
  // Register all tool categories
17
17
  registerContainerTools(server, docker);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supernova123/docker-mcp-server",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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/docker.ts CHANGED
@@ -17,7 +17,89 @@ export function createDockerClient(options?: DockerClientOptions): Dockerode {
17
17
  return new Dockerode({ socketPath: "/var/run/docker.sock" });
18
18
  }
19
19
 
20
+ /**
21
+ * Structured error types for programmatic error classification.
22
+ * Helps AI clients decide whether to retry (retryable) or report (permanent).
23
+ */
24
+ export class DockerConnectionError extends Error {
25
+ retryable = false;
26
+ constructor(message: string) {
27
+ super(message);
28
+ this.name = "DockerConnectionError";
29
+ }
30
+ }
31
+
32
+ export class DockerTimeoutError extends Error {
33
+ retryable = true;
34
+ constructor(message: string) {
35
+ super(message);
36
+ this.name = "DockerTimeoutError";
37
+ }
38
+ }
39
+
40
+ export class DockerPermissionError extends Error {
41
+ retryable = false;
42
+ constructor(message: string) {
43
+ super(message);
44
+ this.name = "DockerPermissionError";
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Check Docker daemon connectivity at startup.
50
+ * Returns a structured error if Docker is unreachable.
51
+ */
52
+ export async function checkDockerConnection(
53
+ docker: Dockerode
54
+ ): Promise<{ ok: boolean; error?: string }> {
55
+ try {
56
+ await docker.ping();
57
+ return { ok: true };
58
+ } catch (error) {
59
+ const msg = error instanceof Error ? error.message : String(error);
60
+ if (msg.includes("EACCES") || msg.includes("Permission denied")) {
61
+ return {
62
+ ok: false,
63
+ error: `DockerConnectionError: Cannot access Docker socket — ${msg}. Fix: add user to docker group (sudo usermod -aG docker $USER) or run as root.`,
64
+ };
65
+ }
66
+ if (msg.includes("ENOENT") || msg.includes("no such file")) {
67
+ return {
68
+ ok: false,
69
+ error: `DockerConnectionError: Docker socket not found at /var/run/docker.sock — ${msg}. Fix: install Docker (https://docs.docker.com/engine/install/) and ensure the daemon is running.`,
70
+ };
71
+ }
72
+ return {
73
+ ok: false,
74
+ error: `DockerConnectionError: Cannot connect to Docker daemon — ${msg}. Fix: ensure Docker is running (systemctl status docker).`,
75
+ };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Run a Docker API call with a configurable timeout.
81
+ * Returns structured error on timeout instead of hanging indefinitely.
82
+ */
83
+ export async function withTimeout<T>(
84
+ promise: Promise<T>,
85
+ ms: number,
86
+ label: string
87
+ ): Promise<T> {
88
+ return Promise.race([
89
+ promise,
90
+ new Promise<never>((_, reject) =>
91
+ setTimeout(
92
+ () => reject(new DockerTimeoutError(`Docker API call "${label}" timed out after ${ms}ms`)),
93
+ ms
94
+ )
95
+ ),
96
+ ]);
97
+ }
98
+
20
99
  export function formatError(error: unknown): string {
100
+ if (error instanceof DockerConnectionError) return `${error.name}: ${error.message}`;
101
+ if (error instanceof DockerTimeoutError) return `${error.name}: ${error.message}`;
102
+ if (error instanceof DockerPermissionError) return `${error.name}: ${error.message}`;
21
103
  if (error instanceof Error) return error.message;
22
104
  if (typeof error === "string") return error;
23
105
  return String(error);
package/src/index.ts CHANGED
@@ -1,12 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { createDockerClient } from "./docker.js";
4
+ import { createDockerClient, checkDockerConnection } from "./docker.js";
5
5
  import { createServer } from "./server.js";
6
6
 
7
+ const DEFAULT_TIMEOUT_MS = 30_000;
8
+
7
9
  async function main() {
8
10
  const docker = createDockerClient();
9
- const server = createServer(docker);
11
+
12
+ // Startup health check: verify Docker daemon is reachable
13
+ const health = await checkDockerConnection(docker);
14
+ if (!health.ok) {
15
+ process.stderr.write(`${health.error}\n`);
16
+ process.exit(1);
17
+ }
18
+ process.stderr.write("Docker daemon reachable\n");
19
+
20
+ const server = createServer(docker, { timeoutMs: DEFAULT_TIMEOUT_MS });
10
21
 
11
22
  const transport = new StdioServerTransport();
12
23
  await server.connect(transport);
package/src/server.ts CHANGED
@@ -10,10 +10,14 @@ import { registerNetworkTools } from "./tools/network.js";
10
10
  import { registerVolumeTools } from "./tools/volume.js";
11
11
  import { registerMonitoringTools } from "./tools/monitoring.js";
12
12
 
13
- export function createServer(docker: Dockerode): McpServer {
13
+ export interface ServerOptions {
14
+ timeoutMs?: number;
15
+ }
16
+
17
+ export function createServer(docker: Dockerode, options?: ServerOptions): McpServer {
14
18
  const server = new McpServer({
15
19
  name: "docker-mcp-server",
16
- version: "0.3.0",
20
+ version: "0.3.1",
17
21
  });
18
22
 
19
23
  // Register all tool categories
package/src/docker.ts.bak DELETED
@@ -1,85 +0,0 @@
1
- import Dockerode from "dockerode";
2
-
3
- export interface DockerClientOptions {
4
- socketPath?: string;
5
- host?: string;
6
- port?: number;
7
- }
8
-
9
- export function createDockerClient(options?: DockerClientOptions): Dockerode {
10
- if (options?.socketPath) {
11
- return new Dockerode({ socketPath: options.socketPath });
12
- }
13
- if (options?.host && options?.port) {
14
- return new Dockerode({ host: options.host, port: options.port });
15
- }
16
- // Default: local socket
17
- return new Dockerode({ socketPath: "/var/run/docker.sock" });
18
- }
19
-
20
- export function formatError(error: unknown): string {
21
- if (error instanceof Error) return error.message;
22
- if (typeof error === "string") return error;
23
- return String(error);
24
- }
25
-
26
- export function formatContainer(container: Dockerode.ContainerInfo): Record<string, unknown> {
27
- return {
28
- id: container.Id.substring(0, 12),
29
- name: container.Names[0]?.replace(/^\//, ""),
30
- image: container.Image,
31
- state: container.State,
32
- status: container.Status,
33
- created: new Date(container.Created * 1000).toISOString(),
34
- ports: container.Ports.map((p) => ({
35
- private: p.PrivatePort,
36
- public: p.PublicPort,
37
- type: p.Type,
38
- })),
39
- labels: container.Labels,
40
- mounts: container.Mounts.map((m) => ({
41
- type: m.Type,
42
- source: m.Source,
43
- destination: m.Destination,
44
- mode: m.Mode,
45
- rw: m.RW,
46
- })),
47
- };
48
- }
49
-
50
- export function formatImage(image: Dockerode.ImageInfo): Record<string, unknown> {
51
- return {
52
- id: image.Id.substring(0, 19),
53
- tags: image.RepoTags || ["<none>:<none>"],
54
- size: image.Size,
55
- created: new Date(image.Created).toISOString(),
56
- };
57
- }
58
-
59
- /**
60
- * Sanitize tool output: strip ANSI escapes, invisible Unicode, and truncate.
61
- * Prevents prompt injection via output and caps LLM context cost.
62
- */
63
- export function sanitizeOutput(text: string, maxLength = 1_000_000): string {
64
- // Strip ANSI escape codes (CSI, OSC, simple sequences)
65
- text = text.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "");
66
- text = text.replace(/\x1b\][^\x07]*\x07/g, ""); // OSC terminated by BEL
67
- text = text.replace(/\x1b[@-Z\\-_]/g, ""); // Single-char escapes
68
- // Strip invisible Unicode (Tag chars, bidi overrides, zero-width)
69
- text = text.replace(/[\uE0001-\uE007F\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/g, "");
70
- // Strip Docker stream-frame headers (8-byte prefix per frame)
71
- text = text.replace(/^[\x00-\x0f]{8}/gm, "");
72
- // Truncate to cap memory and LLM context cost
73
- if (text.length > maxLength) {
74
- return text.slice(0, maxLength) + `\n... [output truncated at ${maxLength} chars]`;
75
- }
76
- return text;
77
- }
78
-
79
- export function formatBytes(bytes: number): string {
80
- if (bytes === 0) return "0 B";
81
- const k = 1024;
82
- const sizes = ["B", "KB", "MB", "GB", "TB"];
83
- const i = Math.floor(Math.log(bytes) / Math.log(k));
84
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
85
- }
@@ -1,376 +0,0 @@
1
- import Dockerode from "dockerode";
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import {
4
- ContainerHealthStatusSchema,
5
- ContainerResourceUsageSchema,
6
- WatchEventsSchema,
7
- SearchLogsSchema,
8
- ResourceAlertCheckSchema,
9
- MonitorDashboardSchema,
10
- } from "../types.js";
11
- import { sanitizeOutput } from "../docker.js";
12
-
13
- export function registerMonitoringTools(server: McpServer, docker: Dockerode): void {
14
- // 1. fleet_status — health status of all running containers
15
- server.tool(
16
- "container_health_status",
17
- "Check health status, uptime, and restart count for all running Docker containers. Returns JSON with container name, state, health probe status (healthy/unhealthy/no-healthcheck), and restart count. Use this for a quick fleet health overview; for resource metrics use container_resource_usage instead. Returns an array of objects with name, id, state, status, health, uptime, restartCount, and image fields. Read-only and safe to call repeatedly.",
18
- ContainerHealthStatusSchema.shape,
19
- { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
20
- async (params) => {
21
- try {
22
- const containers = await docker.listContainers({ all: false });
23
- const results = await Promise.all(
24
- containers.map(async (c) => {
25
- const info = await docker.getContainer(c.Id).inspect();
26
- return {
27
- name: c.Names[0]?.replace(/^\//, "") || c.Id.slice(0, 12),
28
- id: c.Id.slice(0, 12),
29
- state: c.State,
30
- status: c.Status,
31
- health: info.State.Health?.Status || "no-healthcheck",
32
- uptime: info.State.StartedAt,
33
- restartCount: info.RestartCount,
34
- image: c.Image,
35
- };
36
- })
37
- );
38
- return {
39
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
40
- };
41
- } catch (err: any) {
42
- return {
43
- content: [{ type: "text", text: `Error: ${err.message}` }],
44
- isError: true,
45
- };
46
- }
47
- }
48
- );
49
-
50
- // 2. fleet_stats — resource usage for all running containers
51
- server.tool(
52
- "container_resource_usage",
53
- "Monitor CPU, memory, and network I/O across all running Docker containers. Returns sorted resource usage metrics with percentage breakdowns for each container. Use container_health_status for health probes; use resource_alert_check for threshold violations. Supports sort by cpu, memory, or network. Returns array of objects with name, id, cpu_percent, memory_usage_mb, memory_percent, network_rx_mb, network_tx_mb. Read-only and safe to call repeatedly.",
54
- ContainerResourceUsageSchema.shape,
55
- { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
56
- async (params) => {
57
- try {
58
- const containers = await docker.listContainers({ all: false });
59
- const results = await Promise.all(
60
- containers.map(async (c) => {
61
- const stats = await docker.getContainer(c.Id).stats({ stream: false });
62
- const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - (stats.precpu_stats?.cpu_usage?.total_usage ?? 0);
63
- const systemDelta = stats.cpu_stats.system_cpu_usage - (stats.precpu_stats?.system_cpu_usage ?? 0);
64
- const cpuCount = stats.cpu_stats.online_cpus ?? 1;
65
- const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * cpuCount * 100 : 0;
66
- const memUsage = stats.memory_stats?.usage ?? 0;
67
- const memLimit = stats.memory_stats?.limit ?? 1;
68
- const memPercent = (memUsage / memLimit) * 100;
69
- const netRx = Object.values(stats.networks ?? {}).reduce((sum: number, n: any) => sum + (n.rx_bytes ?? 0), 0);
70
- const netTx = Object.values(stats.networks ?? {}).reduce((sum: number, n: any) => sum + (n.tx_bytes ?? 0), 0);
71
-
72
- return {
73
- name: c.Names[0]?.replace(/^\//, "") || c.Id.slice(0, 12),
74
- id: c.Id.slice(0, 12),
75
- cpu_percent: Math.round(cpuPercent * 100) / 100,
76
- memory_usage_mb: Math.round((memUsage / 1024 / 1024) * 100) / 100,
77
- memory_percent: Math.round(memPercent * 100) / 100,
78
- network_rx_mb: Math.round((netRx / 1024 / 1024) * 100) / 100,
79
- network_tx_mb: Math.round((netTx / 1024 / 1024) * 100) / 100,
80
- };
81
- })
82
- );
83
-
84
- const sortBy = params.sort_by || "cpu";
85
- results.sort((a: any, b: any) => {
86
- if (sortBy === "cpu") return b.cpu_percent - a.cpu_percent;
87
- if (sortBy === "memory") return b.memory_percent - a.memory_percent;
88
- return (b.network_rx_mb + b.network_tx_mb) - (a.network_rx_mb + a.network_tx_mb);
89
- });
90
-
91
- return {
92
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
93
- };
94
- } catch (err: any) {
95
- return {
96
- content: [{ type: "text", text: `Error: ${err.message}` }],
97
- isError: true,
98
- };
99
- }
100
- }
101
- );
102
-
103
- // 3. watch_events — stream Docker events (simplified: collect events for a duration)
104
- server.tool(
105
- "watch_events",
106
- "Stream Docker container events (start, stop, die, restart, health_status) over a configurable time window. Filter by specific container or event type. Use container_health_status for current state; use this tool to watch for changes over time. Returns array of event objects with type, action, container, and time fields. Returns 'No events captured in the time window.' when no events occur. Read-only and safe to call repeatedly.",
107
- WatchEventsSchema.shape,
108
- { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
109
- async (params) => {
110
- try {
111
- const durationMs = (params.duration || 30) * 1000;
112
- const filter: any = {};
113
- if (params.container) filter.container = [params.container];
114
- if (params.event_type && params.event_type !== "all") filter.event = [params.event_type];
115
- if (params.since) filter.since = [params.since];
116
-
117
- const events: any[] = [];
118
- const stream = await docker.getEvents(filter as Dockerode.GetEventsOptions) as unknown as NodeJS.ReadableStream;
119
-
120
- await new Promise<void>((resolve) => {
121
- const timeout = setTimeout(() => {
122
- resolve();
123
- }, durationMs);
124
-
125
- stream.on("data", (chunk: Buffer) => {
126
- try {
127
- const event = JSON.parse(chunk.toString());
128
- events.push({
129
- type: event.Type,
130
- action: event.Action,
131
- container: event.Actor?.Attributes?.name || event.Actor?.ID?.slice(0, 12),
132
- time: new Date(event.time * 1000).toISOString(),
133
- });
134
- } catch {}
135
- });
136
-
137
- stream.on("error", () => {
138
- clearTimeout(timeout);
139
- resolve();
140
- });
141
-
142
- stream.on("end", () => {
143
- clearTimeout(timeout);
144
- resolve();
145
- });
146
- });
147
-
148
- return {
149
- content: [{ type: "text", text: events.length ? JSON.stringify(events, null, 2) : "No events captured in the time window." }],
150
- };
151
- } catch (err: any) {
152
- return {
153
- content: [{ type: "text", text: `Error: ${err.message}` }],
154
- isError: true,
155
- };
156
- }
157
- }
158
- );
159
-
160
- // 4. search_logs — search logs across multiple containers
161
- server.tool(
162
- "search_logs",
163
- "Search Docker container logs across multiple containers using regex pattern matching. Use stream_logs for single-container log tailing; use this tool to search across multiple containers at once. Returns matching log lines with container name and line content. The pattern parameter accepts any valid regex; set ignore_case for case-insensitive matching. Returns 'No matches found.' when no lines match. Read-only and safe to call repeatedly.",
164
- SearchLogsSchema.shape,
165
- { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
166
- async (params) => {
167
- try {
168
- const targetContainers = params.containers || [];
169
- let containers: { id: string; name: string }[];
170
-
171
- if (targetContainers.length > 0) {
172
- containers = await Promise.all(
173
- targetContainers.map(async (id) => {
174
- const info = await docker.getContainer(id).inspect();
175
- return { id, name: info.Name.replace(/^\//, "") };
176
- })
177
- );
178
- } else {
179
- const list = await docker.listContainers({ all: false });
180
- containers = list.map((c) => ({ id: c.Id, name: c.Names[0]?.replace(/^\//, "") || c.Id.slice(0, 12) }));
181
- }
182
-
183
- const regex = new RegExp(params.pattern, params.ignore_case ? "i" : "");
184
- const matches: any[] = [];
185
-
186
- for (const container of containers) {
187
- try {
188
- const logStream = await docker.getContainer(container.id).logs({
189
- stdout: true,
190
- stderr: true,
191
- tail: params.tail || 500,
192
- since: params.since ? Math.floor(new Date(params.since).getTime() / 1000) : undefined,
193
- });
194
- const output = sanitizeOutput(logStream.toString("utf-8"), 100_000);
195
- const lines = output.split("\n");
196
- for (const line of lines) {
197
- if (regex.test(line)) {
198
- matches.push({ container: container.name, line: line.trim() });
199
- }
200
- }
201
- } catch {}
202
- }
203
-
204
- return {
205
- content: [{ type: "text", text: matches.length ? JSON.stringify(matches, null, 2) : "No matches found." }],
206
- };
207
- } catch (err: any) {
208
- return {
209
- content: [{ type: "text", text: `Error: ${err.message}` }],
210
- isError: true,
211
- };
212
- }
213
- }
214
- );
215
-
216
- // 5. check_thresholds — check all containers against thresholds
217
- server.tool(
218
- "resource_alert_check",
219
- "Check all running Docker containers against configurable CPU%, memory%, and restart count thresholds. Returns containers that violate thresholds with specific metrics that triggered alerts. Use container_resource_usage for raw metrics; use this tool for automated alerting. Default thresholds: 80% CPU, 80% memory, 5 restarts. Returns { violations: [...], checked: N } or { message: 'All containers within thresholds.', checked: N }. Read-only and safe to call repeatedly.",
220
- ResourceAlertCheckSchema.shape,
221
- { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
222
- async (params) => {
223
- try {
224
- const cpuThreshold = params.cpu_percent ?? 80;
225
- const memThreshold = params.memory_percent ?? 80;
226
- const restartThreshold = params.restart_count ?? 5;
227
- const containers = await docker.listContainers({ all: false });
228
- const violations: any[] = [];
229
-
230
- for (const c of containers) {
231
- const info = await docker.getContainer(c.Id).inspect();
232
- const issues: string[] = [];
233
-
234
- // Check restart count
235
- if (info.RestartCount > restartThreshold) {
236
- issues.push(`restarts: ${info.RestartCount} > ${restartThreshold}`);
237
- }
238
-
239
- // Check CPU and memory
240
- try {
241
- const stats = await docker.getContainer(c.Id).stats({ stream: false });
242
- const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - (stats.precpu_stats?.cpu_usage?.total_usage ?? 0);
243
- const systemDelta = stats.cpu_stats.system_cpu_usage - (stats.precpu_stats?.system_cpu_usage ?? 0);
244
- const cpuCount = stats.cpu_stats.online_cpus ?? 1;
245
- const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * cpuCount * 100 : 0;
246
- const memUsage = stats.memory_stats?.usage ?? 0;
247
- const memLimit = stats.memory_stats?.limit ?? 1;
248
- const memPercent = (memUsage / memLimit) * 100;
249
-
250
- if (cpuPercent > cpuThreshold) issues.push(`cpu: ${Math.round(cpuPercent)}% > ${cpuThreshold}%`);
251
- if (memPercent > memThreshold) issues.push(`memory: ${Math.round(memPercent)}% > ${memThreshold}%`);
252
- } catch {}
253
-
254
- if (issues.length > 0) {
255
- violations.push({
256
- container: c.Names[0]?.replace(/^\//, "") || c.Id.slice(0, 12),
257
- id: c.Id.slice(0, 12),
258
- issues,
259
- });
260
- }
261
- }
262
-
263
- return {
264
- content: [{
265
- type: "text",
266
- text: violations.length
267
- ? JSON.stringify({ violations, checked: containers.length }, null, 2)
268
- : JSON.stringify({ message: "All containers within thresholds.", checked: containers.length }),
269
- }],
270
- };
271
- } catch (err: any) {
272
- return {
273
- content: [{ type: "text", text: `Error: ${err.message}` }],
274
- isError: true,
275
- };
276
- }
277
- }
278
- );
279
-
280
- // 6. monitor_dashboard — single-call fleet summary
281
- server.tool(
282
- "monitor_dashboard",
283
- "Comprehensive Docker fleet dashboard in a single API call. Aggregates health status of all containers, top 5 CPU consumers, recent events (last 5 minutes), and threshold violations into one response. Use individual tools (container_health_status, container_resource_usage, watch_events) for targeted queries; use this for a complete fleet overview. Returns object with summary (total, running, healthy, unhealthy), top_consumers, recent_events, and violations. Read-only and safe to call repeatedly.",
284
- MonitorDashboardSchema.shape,
285
- { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
286
- async (params) => {
287
- try {
288
- const containers = await docker.listContainers({ all: false });
289
-
290
- // Fleet health
291
- const health = await Promise.all(
292
- containers.map(async (c) => {
293
- const info = await docker.getContainer(c.Id).inspect();
294
- return {
295
- name: c.Names[0]?.replace(/^\//, "") || c.Id.slice(0, 12),
296
- state: c.State,
297
- health: info.State.Health?.Status || "no-healthcheck",
298
- restartCount: info.RestartCount,
299
- };
300
- })
301
- );
302
-
303
- // Resource usage (top 5 by CPU)
304
- const stats = await Promise.all(
305
- containers.map(async (c) => {
306
- try {
307
- const s = await docker.getContainer(c.Id).stats({ stream: false });
308
- const cpuDelta = s.cpu_stats.cpu_usage.total_usage - (s.precpu_stats?.cpu_usage?.total_usage ?? 0);
309
- const systemDelta = s.cpu_stats.system_cpu_usage - (s.precpu_stats?.system_cpu_usage ?? 0);
310
- const cpuCount = s.cpu_stats.online_cpus ?? 1;
311
- const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * cpuCount * 100 : 0;
312
- const memUsage = s.memory_stats?.usage ?? 0;
313
- const memLimit = s.memory_stats?.limit ?? 1;
314
- const memPercent = (memUsage / memLimit) * 100;
315
- return {
316
- name: c.Names[0]?.replace(/^\//, "") || c.Id.slice(0, 12),
317
- cpu_percent: Math.round(cpuPercent * 100) / 100,
318
- memory_percent: Math.round(memPercent * 100) / 100,
319
- };
320
- } catch {
321
- return null;
322
- }
323
- })
324
- );
325
-
326
- const topConsumers = stats.filter(Boolean).sort((a: any, b: any) => b.cpu_percent - a.cpu_percent).slice(0, 5);
327
-
328
- // Recent events (last 5 minutes) - use simple approach
329
- const recentEvents: any[] = [];
330
- try {
331
- const sinceTs = Math.floor((Date.now() - 5 * 60 * 1000) / 1000);
332
- const eventStream = await docker.getEvents({ since: sinceTs }) as unknown as NodeJS.ReadableStream;
333
- await new Promise<void>((resolve) => {
334
- const timeout = setTimeout(() => { resolve(); }, 2000);
335
- eventStream.on("data", (chunk: Buffer) => {
336
- try {
337
- const e = JSON.parse(chunk.toString());
338
- recentEvents.push({
339
- action: e.Action,
340
- container: e.Actor?.Attributes?.name || e.Actor?.ID?.slice(0, 12),
341
- time: new Date(e.time * 1000).toISOString(),
342
- });
343
- } catch {}
344
- });
345
- eventStream.on("error", () => { clearTimeout(timeout); resolve(); });
346
- eventStream.on("end", () => { clearTimeout(timeout); resolve(); });
347
- });
348
- } catch {}
349
-
350
- // Threshold violations
351
- const violations = stats.filter(Boolean).filter((s: any) => s.cpu_percent > 80 || s.memory_percent > 80);
352
-
353
- const dashboard = {
354
- summary: {
355
- total_containers: containers.length,
356
- running: containers.filter((c) => c.State === "running").length,
357
- unhealthy: health.filter((h) => h.health === "unhealthy").length,
358
- },
359
- health,
360
- top_cpu_consumers: topConsumers,
361
- recent_events: recentEvents.slice(0, 10),
362
- threshold_violations: violations,
363
- };
364
-
365
- return {
366
- content: [{ type: "text", text: JSON.stringify(dashboard, null, 2) }],
367
- };
368
- } catch (err: any) {
369
- return {
370
- content: [{ type: "text", text: `Error: ${err.message}` }],
371
- isError: true,
372
- };
373
- }
374
- }
375
- );
376
- }