@supernova123/docker-mcp-server 0.1.4 → 0.1.6

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/.dockerignore ADDED
@@ -0,0 +1,9 @@
1
+ node_modules
2
+ dist
3
+ .git
4
+ *.md
5
+ !README.md
6
+ tests
7
+ tsconfig.json
8
+ vitest.config.ts
9
+ .DS_Store
package/Dockerfile ADDED
@@ -0,0 +1,29 @@
1
+ FROM node:22-slim AS builder
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy package files first for better layer caching
6
+ COPY package.json package-lock.json ./
7
+ RUN npm ci --ignore-scripts
8
+
9
+ # Copy source and build
10
+ COPY tsconfig.json ./
11
+ COPY src/ ./src/
12
+ RUN npm run build
13
+
14
+ # Production stage
15
+ FROM node:22-slim
16
+
17
+ WORKDIR /app
18
+
19
+ # Copy package files and install production deps only
20
+ COPY package.json package-lock.json ./
21
+ RUN npm ci --omit=dev --ignore-scripts
22
+
23
+ # Copy built output
24
+ COPY --from=builder /app/dist/ ./dist/
25
+
26
+ # The server communicates via stdio (MCP transport)
27
+ # No EXPOSE needed — stdio transport doesn't listen on a port
28
+
29
+ ENTRYPOINT ["node", "dist/index.js"]
@@ -45,6 +45,10 @@ export function registerContainerTools(server, docker) {
45
45
  return { content: [{ type: "text", text: `Container ${params.container_id} stopped.` }] };
46
46
  }
47
47
  catch (error) {
48
+ // 304 means container is already stopped — treat as success
49
+ if (error?.statusCode === 304) {
50
+ return { content: [{ type: "text", text: `Container ${params.container_id} was already stopped.` }] };
51
+ }
48
52
  return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
49
53
  }
50
54
  });
@@ -103,7 +107,7 @@ export function registerContainerTools(server, docker) {
103
107
  return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
104
108
  }
105
109
  });
106
- server.tool("run_container", "Create and start a new Docker container with one command. Supports image, env, ports, volumes, restart policy, and command override.", RunContainerSchema.shape, async (params) => {
110
+ server.tool("run_container", "Create and start a new Docker container with one command. Supports image, env, ports, volumes, restart policy, and command override. Auto-pulls missing images.", RunContainerSchema.shape, async (params) => {
107
111
  try {
108
112
  const createOpts = {
109
113
  Image: params.image,
@@ -126,7 +130,28 @@ export function registerContainerTools(server, docker) {
126
130
  : undefined,
127
131
  },
128
132
  };
129
- const container = await docker.createContainer(createOpts);
133
+ let container;
134
+ try {
135
+ container = await docker.createContainer(createOpts);
136
+ }
137
+ catch (createError) {
138
+ // Auto-pull if image not found (HTTP 404)
139
+ if (createError?.statusCode === 404 && /no such image|No such image/i.test(createError.message || "")) {
140
+ const stream = await docker.pull(params.image);
141
+ await new Promise((resolve, reject) => {
142
+ docker.modem.followProgress(stream, (err) => {
143
+ if (err)
144
+ reject(err);
145
+ else
146
+ resolve();
147
+ });
148
+ });
149
+ container = await docker.createContainer(createOpts);
150
+ }
151
+ else {
152
+ throw createError;
153
+ }
154
+ }
130
155
  await container.start();
131
156
  return {
132
157
  content: [{ type: "text", text: `Container created and started. ID: ${container.id.substring(0, 12)}` }],
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@supernova123/docker-mcp-server",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "mcpName": "io.github.friendlygeorge/docker-mcp-server",
5
- "description": "MCP server for Docker \u2014 container management, health checks, auto-restart, Compose lifecycle, and log streaming for Claude, Cursor, and AI agents",
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",
7
7
  "main": "dist/index.js",
8
8
  "bin": {
@@ -17,20 +17,20 @@
17
17
  },
18
18
  "keywords": [
19
19
  "mcp",
20
+ "mcp-server",
20
21
  "docker",
22
+ "docker-mcp",
21
23
  "container",
22
24
  "compose",
23
25
  "health-check",
24
26
  "devops",
25
27
  "ai-agent",
26
28
  "model-context-protocol",
27
- "mcp-server",
28
- "modelcontextprotocol",
29
29
  "claude",
30
30
  "cursor",
31
31
  "chatgpt",
32
- "docker-mcp",
33
- "mcp-docker"
32
+ "claude-desktop",
33
+ "ai"
34
34
  ],
35
35
  "author": "Nova",
36
36
  "license": "MIT",
@@ -53,4 +53,4 @@
53
53
  "typescript": "^5.7.0",
54
54
  "vitest": "^3.1.0"
55
55
  }
56
- }
56
+ }
@@ -74,7 +74,11 @@ export function registerContainerTools(server: McpServer, docker: Dockerode): vo
74
74
  const container = docker.getContainer(params.container_id);
75
75
  await container.stop({ t: params.timeout ?? 10 });
76
76
  return { content: [{ type: "text", text: `Container ${params.container_id} stopped.` }] };
77
- } catch (error) {
77
+ } catch (error: any) {
78
+ // 304 means container is already stopped — treat as success
79
+ if (error?.statusCode === 304) {
80
+ return { content: [{ type: "text", text: `Container ${params.container_id} was already stopped.` }] };
81
+ }
78
82
  return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
79
83
  }
80
84
  }
@@ -152,7 +156,7 @@ export function registerContainerTools(server: McpServer, docker: Dockerode): vo
152
156
 
153
157
  server.tool(
154
158
  "run_container",
155
- "Create and start a new Docker container with one command. Supports image, env, ports, volumes, restart policy, and command override.",
159
+ "Create and start a new Docker container with one command. Supports image, env, ports, volumes, restart policy, and command override. Auto-pulls missing images.",
156
160
  RunContainerSchema.shape,
157
161
  async (params) => {
158
162
  try {
@@ -180,7 +184,25 @@ export function registerContainerTools(server: McpServer, docker: Dockerode): vo
180
184
  },
181
185
  };
182
186
 
183
- const container = await docker.createContainer(createOpts);
187
+ let container;
188
+ try {
189
+ container = await docker.createContainer(createOpts);
190
+ } catch (createError: any) {
191
+ // Auto-pull if image not found (HTTP 404)
192
+ if (createError?.statusCode === 404 && /no such image|No such image/i.test(createError.message || "")) {
193
+ const stream = await docker.pull(params.image);
194
+ await new Promise<void>((resolve, reject) => {
195
+ docker.modem.followProgress(stream, (err: Error | null) => {
196
+ if (err) reject(err);
197
+ else resolve();
198
+ });
199
+ });
200
+ container = await docker.createContainer(createOpts);
201
+ } else {
202
+ throw createError;
203
+ }
204
+ }
205
+
184
206
  await container.start();
185
207
  return {
186
208
  content: [{ type: "text", text: `Container created and started. ID: ${container.id.substring(0, 12)}` }],