@supernova123/docker-mcp-server 0.3.0 → 0.3.2

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,45 @@ 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>;
37
+ /**
38
+ * Retry a Docker API call with exponential backoff.
39
+ * Retries on transient errors (ECONNRESET, ETIMEDOUT, 5xx).
40
+ * Does NOT retry on 4xx errors (bad request, not found, permission denied).
41
+ */
42
+ export declare function withRetry<T>(fn: () => Promise<T>, options?: {
43
+ maxRetries?: number;
44
+ baseDelayMs?: number;
45
+ label?: string;
46
+ }): Promise<T>;
8
47
  export declare function formatError(error: unknown): string;
9
48
  export declare function formatContainer(container: Dockerode.ContainerInfo): Record<string, unknown>;
10
49
  export declare function formatImage(image: Dockerode.ImageInfo): Record<string, unknown>;
package/dist/docker.js CHANGED
@@ -9,7 +9,156 @@ 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
+ }
76
+ /**
77
+ * Retry a Docker API call with exponential backoff.
78
+ * Retries on transient errors (ECONNRESET, ETIMEDOUT, 5xx).
79
+ * Does NOT retry on 4xx errors (bad request, not found, permission denied).
80
+ */
81
+ export async function withRetry(fn, options = {}) {
82
+ const { maxRetries = 3, baseDelayMs = 1000, label = "Docker API call" } = options;
83
+ let lastError;
84
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
85
+ try {
86
+ return await fn();
87
+ }
88
+ catch (error) {
89
+ lastError = error;
90
+ // Don't retry on non-transient errors
91
+ if (!isRetryableError(error)) {
92
+ throw error;
93
+ }
94
+ // Don't retry on last attempt
95
+ if (attempt === maxRetries) {
96
+ throw error;
97
+ }
98
+ // Exponential backoff: 1s, 2s, 4s
99
+ const delay = baseDelayMs * Math.pow(2, attempt);
100
+ process.stderr.write(`[retry] ${label} failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delay}ms...\n`);
101
+ await new Promise(resolve => setTimeout(resolve, delay));
102
+ }
103
+ }
104
+ throw lastError;
105
+ }
106
+ /**
107
+ * Determine if an error is transient and worth retrying.
108
+ * Retries on: connection resets, timeouts, 5xx status codes.
109
+ * Does NOT retry on: 4xx (client errors), permission denied, not found.
110
+ */
111
+ function isRetryableError(error) {
112
+ if (!(error instanceof Error))
113
+ return false;
114
+ const msg = error.message.toLowerCase();
115
+ // Connection-level transient errors
116
+ if (msg.includes("econnreset") || msg.includes("econnrefused"))
117
+ return true;
118
+ if (msg.includes("etimedout") || msg.includes("socket hang up"))
119
+ return true;
120
+ if (msg.includes("epipe") || msg.includes("eai_again"))
121
+ return true;
122
+ // Docker API 5xx errors (server-side)
123
+ if (msg.includes("status code 5"))
124
+ return true;
125
+ if (msg.includes("internal server error"))
126
+ return true;
127
+ if (msg.includes("bad gateway"))
128
+ return true;
129
+ if (msg.includes("service unavailable"))
130
+ return true;
131
+ // Docker daemon busy (transient)
132
+ if (msg.includes("daemon is busy"))
133
+ return true;
134
+ if (msg.includes("too many requests"))
135
+ return true;
136
+ // 4xx errors are NOT retryable (client errors)
137
+ if (msg.includes("status code 4"))
138
+ return false;
139
+ if (msg.includes("not found"))
140
+ return false;
141
+ if (msg.includes("permission denied"))
142
+ return false;
143
+ if (msg.includes("bad request"))
144
+ return false;
145
+ // Permission/connection errors are NOT retryable
146
+ if (error.name === "DockerConnectionError")
147
+ return false;
148
+ if (error.name === "DockerPermissionError")
149
+ return false;
150
+ // Timeout errors ARE retryable (might be transient)
151
+ if (error.name === "DockerTimeoutError")
152
+ return true;
153
+ return false;
154
+ }
12
155
  export function formatError(error) {
156
+ if (error instanceof DockerConnectionError)
157
+ return `${error.name}: ${error.message}`;
158
+ if (error instanceof DockerTimeoutError)
159
+ return `${error.name}: ${error.message}`;
160
+ if (error instanceof DockerPermissionError)
161
+ return `${error.name}: ${error.message}`;
13
162
  if (error instanceof Error)
14
163
  return error.message;
15
164
  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.2",
15
15
  });
16
16
  // Register all tool categories
17
17
  registerContainerTools(server, docker);
@@ -1,16 +1,16 @@
1
1
  import { ListContainersSchema, InspectContainerSchema, StartContainerSchema, StopContainerSchema, RestartContainerSchema, RemoveContainerSchema, RecreateContainerSchema, RunContainerSchema, } from "../types.js";
2
- import { formatContainer, formatError } from "../docker.js";
2
+ import { formatContainer, formatError, withRetry } from "../docker.js";
3
3
  export function registerContainerTools(server, docker) {
4
4
  server.tool("list_containers", "List Docker containers with optional filters (state, label, name). Returns container IDs, names, images, states, ports, and labels.", ListContainersSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
5
5
  try {
6
- const containers = await docker.listContainers({
6
+ const containers = await withRetry(() => docker.listContainers({
7
7
  all: params.all ?? false,
8
8
  filters: JSON.stringify({
9
9
  ...(params.label ? { label: params.label } : {}),
10
10
  ...(params.name ? { name: [`/${params.name}`] } : {}),
11
11
  ...(params.state ? { status: [params.state] } : {}),
12
12
  }),
13
- });
13
+ }), { label: "list_containers" });
14
14
  const results = containers.map(formatContainer);
15
15
  return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
16
16
  }
@@ -31,7 +31,7 @@ export function registerContainerTools(server, docker) {
31
31
  server.tool("start_container", "Start a stopped Docker container by ID or name.", StartContainerSchema.shape, { idempotentHint: true, openWorldHint: false }, async (params) => {
32
32
  try {
33
33
  const container = docker.getContainer(params.container_id);
34
- await container.start();
34
+ await withRetry(() => container.start(), { label: "start_container" });
35
35
  return { content: [{ type: "text", text: `Container ${params.container_id} started.` }] };
36
36
  }
37
37
  catch (error) {
@@ -41,7 +41,7 @@ export function registerContainerTools(server, docker) {
41
41
  server.tool("stop_container", "Stop a running Docker container by ID or name with optional timeout.", StopContainerSchema.shape, { destructiveHint: true, openWorldHint: false }, async (params) => {
42
42
  try {
43
43
  const container = docker.getContainer(params.container_id);
44
- await container.stop({ t: params.timeout ?? 10 });
44
+ await withRetry(() => container.stop({ t: params.timeout ?? 10 }), { label: "stop_container" });
45
45
  return { content: [{ type: "text", text: `Container ${params.container_id} stopped.` }] };
46
46
  }
47
47
  catch (error) {
@@ -55,7 +55,7 @@ export function registerContainerTools(server, docker) {
55
55
  server.tool("restart_container", "Restart a Docker container by ID or name with optional timeout. This tears down the running process and starts a new one — use stop_container for a graceful shutdown or remove_container to delete entirely. The timeout parameter (default 10s) controls how long to wait before force-killing. Returns a confirmation string on success. Idempotent: restarting an already-stopped container starts it again. Returns an error string if the container does not exist or is not running.", RestartContainerSchema.shape, { destructiveHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
56
56
  try {
57
57
  const container = docker.getContainer(params.container_id);
58
- await container.restart({ t: params.timeout ?? 10 });
58
+ await withRetry(() => container.restart({ t: params.timeout ?? 10 }), { label: "restart_container" });
59
59
  return { content: [{ type: "text", text: `Container ${params.container_id} restarted.` }] };
60
60
  }
61
61
  catch (error) {
@@ -1,12 +1,12 @@
1
1
  import { ListImagesSchema, PullImageSchema, BuildImageSchema, RemoveImageSchema, } from "../types.js";
2
- import { formatImage, formatError } from "../docker.js";
2
+ import { formatImage, formatError, withRetry } from "../docker.js";
3
3
  export function registerImageTools(server, docker) {
4
4
  server.tool("list_images", "List Docker images with optional filters. Returns image IDs, tags, sizes, and creation dates.", ListImagesSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
5
5
  try {
6
- const images = await docker.listImages({
6
+ const images = await withRetry(() => docker.listImages({
7
7
  all: params.all ?? false,
8
8
  filters: params.filter ? JSON.stringify({ reference: [params.filter] }) : undefined,
9
- });
9
+ }), { label: "list_images" });
10
10
  const results = images.map(formatImage);
11
11
  return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
12
12
  }
@@ -1,16 +1,16 @@
1
1
  import { StreamLogsSchema, ContainerStatsSchema } from "../types.js";
2
- import { formatError, formatBytes, sanitizeOutput } from "../docker.js";
2
+ import { formatError, sanitizeOutput, withRetry, formatBytes } from "../docker.js";
3
3
  export function registerLogsTools(server, docker) {
4
4
  server.tool("stream_logs", "Get logs from a single Docker container by ID or name. Use stream_logs for one container; use compose_logs for multi-service Compose stacks. Supports tail count (default 100 lines), since timestamp for filtering, and follow mode. Returns UTF-8 log text with multiplexed stream headers stripped, or 'No logs found.' when the container has no output. Read-only and safe to call repeatedly. Returns an error string if the container does not exist.", StreamLogsSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
5
5
  try {
6
6
  const container = docker.getContainer(params.container_id);
7
- const logs = await container.logs({
7
+ const logs = await withRetry(() => container.logs({
8
8
  stdout: true,
9
9
  stderr: true,
10
10
  tail: params.tail ?? 100,
11
11
  since: params.since ? Math.floor(new Date(params.since).getTime() / 1000) : undefined,
12
12
  follow: false,
13
- });
13
+ }), { label: "container_logs" });
14
14
  // Dockerode returns a Buffer with multiplexed stream headers
15
15
  // Use 100KB cap for logs to keep LLM context small
16
16
  const output = sanitizeOutput(logs.toString("utf-8"), 100_000);
@@ -1,11 +1,11 @@
1
1
  import { ListNetworksSchema, ListVolumesSchema } from "../types.js";
2
- import { formatError } from "../docker.js";
2
+ import { formatError, withRetry } from "../docker.js";
3
3
  export function registerNetworkTools(server, docker) {
4
4
  server.tool("list_networks", "List Docker networks with optional filter. Returns network IDs, names, drivers, and scopes.", ListNetworksSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
5
5
  try {
6
- const networks = await docker.listNetworks({
6
+ const networks = await withRetry(() => docker.listNetworks({
7
7
  filters: params.filter ? JSON.stringify({ name: [params.filter] }) : undefined,
8
- });
8
+ }), { label: "list_networks" });
9
9
  const results = networks.map((n) => ({
10
10
  id: n.Id.substring(0, 12),
11
11
  name: n.Name,
@@ -1,14 +1,14 @@
1
1
  import { CreateVolumeSchema, InspectVolumeSchema, RemoveVolumeSchema, PruneVolumesSchema, } from "../types.js";
2
- import { formatError } from "../docker.js";
2
+ import { formatError, withRetry } from "../docker.js";
3
3
  export function registerVolumeTools(server, docker) {
4
4
  server.tool("create_volume", "Create a Docker volume with optional driver, labels, and options. Returns volume name, driver, and mountpoint.", CreateVolumeSchema.shape, { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async (params) => {
5
5
  try {
6
- const result = await docker.createVolume({
6
+ const result = await withRetry(() => docker.createVolume({
7
7
  Name: params.name,
8
8
  Driver: params.driver || "local",
9
9
  Labels: params.labels,
10
10
  DriverOpts: params.options,
11
- });
11
+ }), { label: "create_volume" });
12
12
  return {
13
13
  content: [{
14
14
  type: "text",
package/dist/types.d.ts CHANGED
@@ -5,15 +5,15 @@ export declare const ListContainersSchema: z.ZodObject<{
5
5
  name: z.ZodOptional<z.ZodString>;
6
6
  state: z.ZodOptional<z.ZodEnum<["running", "stopped", "paused", "exited", "created", "restarting"]>>;
7
7
  }, "strip", z.ZodTypeAny, {
8
+ label?: string[] | undefined;
8
9
  name?: string | undefined;
9
10
  state?: "created" | "running" | "stopped" | "paused" | "exited" | "restarting" | undefined;
10
11
  all?: boolean | undefined;
11
- label?: string[] | undefined;
12
12
  }, {
13
+ label?: string[] | undefined;
13
14
  name?: string | undefined;
14
15
  state?: "created" | "running" | "stopped" | "paused" | "exited" | "restarting" | undefined;
15
16
  all?: boolean | undefined;
16
- label?: string[] | undefined;
17
17
  }>;
18
18
  export declare const InspectContainerSchema: z.ZodObject<{
19
19
  container_id: z.ZodString;
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.2",
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,171 @@ 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
+
99
+
100
+ /**
101
+ * Retry a Docker API call with exponential backoff.
102
+ * Retries on transient errors (ECONNRESET, ETIMEDOUT, 5xx).
103
+ * Does NOT retry on 4xx errors (bad request, not found, permission denied).
104
+ */
105
+ export async function withRetry<T>(
106
+ fn: () => Promise<T>,
107
+ options: { maxRetries?: number; baseDelayMs?: number; label?: string } = {}
108
+ ): Promise<T> {
109
+ const { maxRetries = 3, baseDelayMs = 1000, label = "Docker API call" } = options;
110
+ let lastError: unknown;
111
+
112
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
113
+ try {
114
+ return await fn();
115
+ } catch (error) {
116
+ lastError = error;
117
+
118
+ // Don't retry on non-transient errors
119
+ if (!isRetryableError(error)) {
120
+ throw error;
121
+ }
122
+
123
+ // Don't retry on last attempt
124
+ if (attempt === maxRetries) {
125
+ throw error;
126
+ }
127
+
128
+ // Exponential backoff: 1s, 2s, 4s
129
+ const delay = baseDelayMs * Math.pow(2, attempt);
130
+ process.stderr.write(
131
+ `[retry] ${label} failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delay}ms...\n`
132
+ );
133
+ await new Promise(resolve => setTimeout(resolve, delay));
134
+ }
135
+ }
136
+
137
+ throw lastError;
138
+ }
139
+
140
+ /**
141
+ * Determine if an error is transient and worth retrying.
142
+ * Retries on: connection resets, timeouts, 5xx status codes.
143
+ * Does NOT retry on: 4xx (client errors), permission denied, not found.
144
+ */
145
+ function isRetryableError(error: unknown): boolean {
146
+ if (!(error instanceof Error)) return false;
147
+
148
+ const msg = error.message.toLowerCase();
149
+
150
+ // Connection-level transient errors
151
+ if (msg.includes("econnreset") || msg.includes("econnrefused")) return true;
152
+ if (msg.includes("etimedout") || msg.includes("socket hang up")) return true;
153
+ if (msg.includes("epipe") || msg.includes("eai_again")) return true;
154
+
155
+ // Docker API 5xx errors (server-side)
156
+ if (msg.includes("status code 5")) return true;
157
+ if (msg.includes("internal server error")) return true;
158
+ if (msg.includes("bad gateway")) return true;
159
+ if (msg.includes("service unavailable")) return true;
160
+
161
+ // Docker daemon busy (transient)
162
+ if (msg.includes("daemon is busy")) return true;
163
+ if (msg.includes("too many requests")) return true;
164
+
165
+ // 4xx errors are NOT retryable (client errors)
166
+ if (msg.includes("status code 4")) return false;
167
+ if (msg.includes("not found")) return false;
168
+ if (msg.includes("permission denied")) return false;
169
+ if (msg.includes("bad request")) return false;
170
+
171
+ // Permission/connection errors are NOT retryable
172
+ if (error.name === "DockerConnectionError") return false;
173
+ if (error.name === "DockerPermissionError") return false;
174
+
175
+ // Timeout errors ARE retryable (might be transient)
176
+ if (error.name === "DockerTimeoutError") return true;
177
+
178
+ return false;
179
+ }
180
+
20
181
  export function formatError(error: unknown): string {
182
+ if (error instanceof DockerConnectionError) return `${error.name}: ${error.message}`;
183
+ if (error instanceof DockerTimeoutError) return `${error.name}: ${error.message}`;
184
+ if (error instanceof DockerPermissionError) return `${error.name}: ${error.message}`;
21
185
  if (error instanceof Error) return error.message;
22
186
  if (typeof error === "string") return error;
23
187
  return String(error);
@@ -82,4 +246,4 @@ export function formatBytes(bytes: number): string {
82
246
  const sizes = ["B", "KB", "MB", "GB", "TB"];
83
247
  const i = Math.floor(Math.log(bytes) / Math.log(k));
84
248
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
85
- }
249
+ }
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.2",
17
21
  });
18
22
 
19
23
  // Register all tool categories