@supernova123/docker-mcp-server 0.1.3 → 0.1.5

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.
@@ -1,11 +1,32 @@
1
1
  import { execSync, exec as execCb } from "child_process";
2
+ import { existsSync, statSync } from "fs";
3
+ import { join } from "path";
2
4
  import { promisify } from "util";
3
5
  import { ComposeUpSchema, ComposeDownSchema, ComposePsSchema, ComposeLogsSchema, ComposeRestartSchema, } from "../types.js";
4
6
  import { formatError } from "../docker.js";
5
7
  const execAsync = promisify(execCb);
8
+ const COMPOSE_FILE_NAMES = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
9
+ function resolveComposePath(inputPath) {
10
+ // If it's already a file that exists, use it directly
11
+ if (existsSync(inputPath) && statSync(inputPath).isFile()) {
12
+ return inputPath;
13
+ }
14
+ // If it's a directory, look for compose files inside it
15
+ if (existsSync(inputPath) && statSync(inputPath).isDirectory()) {
16
+ for (const name of COMPOSE_FILE_NAMES) {
17
+ const filePath = join(inputPath, name);
18
+ if (existsSync(filePath))
19
+ return filePath;
20
+ }
21
+ throw new Error(`No docker-compose.yml found in ${inputPath}. Looked for: ${COMPOSE_FILE_NAMES.join(", ")}`);
22
+ }
23
+ // Path doesn't exist — pass it through and let Docker produce the error
24
+ return inputPath;
25
+ }
6
26
  function runCompose(path, args) {
27
+ const filePath = resolveComposePath(path);
7
28
  try {
8
- const result = execSync(`docker compose -f ${path} ${args.join(" ")}`, {
29
+ const result = execSync(`docker compose -f "${filePath}" ${args.join(" ")}`, {
9
30
  encoding: "utf-8",
10
31
  timeout: 30000,
11
32
  });
@@ -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
  });
package/dist/types.js CHANGED
@@ -60,18 +60,18 @@ export const RemoveImageSchema = z.object({
60
60
  });
61
61
  // Docker Compose schemas
62
62
  export const ComposeUpSchema = z.object({
63
- path: z.string().describe("Path to docker-compose.yml directory"),
63
+ path: z.string().describe("Path to docker-compose.yml file or its parent directory"),
64
64
  build: z.boolean().optional().describe("Build images before starting (default: false)"),
65
65
  detach: z.boolean().optional().describe("Run in detached mode (default: true)"),
66
66
  services: z.array(z.string()).optional().describe("Specific services to start"),
67
67
  });
68
68
  export const ComposeDownSchema = z.object({
69
- path: z.string().describe("Path to docker-compose.yml directory"),
69
+ path: z.string().describe("Path to docker-compose.yml file or its parent directory"),
70
70
  volumes: z.boolean().optional().describe("Remove named volumes (default: false)"),
71
71
  timeout: z.number().optional().describe("Shutdown timeout in seconds (default: 10)"),
72
72
  });
73
73
  export const ComposePsSchema = z.object({
74
- path: z.string().describe("Path to docker-compose.yml directory"),
74
+ path: z.string().describe("Path to docker-compose.yml file or its parent directory"),
75
75
  });
76
76
  export const ComposeLogsSchema = z.object({
77
77
  path: z.string().describe("Path to docker-compose.yml directory"),
@@ -80,7 +80,7 @@ export const ComposeLogsSchema = z.object({
80
80
  follow: z.boolean().optional().describe("Follow log output (default: false)"),
81
81
  });
82
82
  export const ComposeRestartSchema = z.object({
83
- path: z.string().describe("Path to docker-compose.yml directory"),
83
+ path: z.string().describe("Path to docker-compose.yml file or its parent directory"),
84
84
  services: z.array(z.string()).optional().describe("Specific services to restart (empty = all)"),
85
85
  timeout: z.number().optional().describe("Shutdown timeout in seconds (default: 10)"),
86
86
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supernova123/docker-mcp-server",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",
@@ -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",
package/server.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.friendlygeorge/docker-mcp-server",
4
+ "description": "MCP server for Docker — containers, health checks, Compose, logs for AI agents",
5
+ "repository": {
6
+ "url": "https://github.com/friendlygeorge/docker-mcp-server",
7
+ "source": "github"
8
+ },
9
+ "version": "0.1.3",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "@supernova123/docker-mcp-server",
14
+ "version": "0.1.3",
15
+ "transport": {
16
+ "type": "stdio"
17
+ },
18
+ "environmentVariables": [
19
+ {
20
+ "description": "Docker socket path (default: /var/run/docker.sock)",
21
+ "isRequired": false,
22
+ "format": "string",
23
+ "name": "DOCKER_HOST"
24
+ }
25
+ ]
26
+ }
27
+ ]
28
+ }
@@ -1,4 +1,6 @@
1
1
  import { execSync, exec as execCb } from "child_process";
2
+ import { existsSync, statSync } from "fs";
3
+ import { join } from "path";
2
4
  import { promisify } from "util";
3
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
6
  import {
@@ -12,9 +14,29 @@ import { formatError } from "../docker.js";
12
14
 
13
15
  const execAsync = promisify(execCb);
14
16
 
17
+ const COMPOSE_FILE_NAMES = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
18
+
19
+ function resolveComposePath(inputPath: string): string {
20
+ // If it's already a file that exists, use it directly
21
+ if (existsSync(inputPath) && statSync(inputPath).isFile()) {
22
+ return inputPath;
23
+ }
24
+ // If it's a directory, look for compose files inside it
25
+ if (existsSync(inputPath) && statSync(inputPath).isDirectory()) {
26
+ for (const name of COMPOSE_FILE_NAMES) {
27
+ const filePath = join(inputPath, name);
28
+ if (existsSync(filePath)) return filePath;
29
+ }
30
+ throw new Error(`No docker-compose.yml found in ${inputPath}. Looked for: ${COMPOSE_FILE_NAMES.join(", ")}`);
31
+ }
32
+ // Path doesn't exist — pass it through and let Docker produce the error
33
+ return inputPath;
34
+ }
35
+
15
36
  function runCompose(path: string, args: string[]): string {
37
+ const filePath = resolveComposePath(path);
16
38
  try {
17
- const result = execSync(`docker compose -f ${path} ${args.join(" ")}`, {
39
+ const result = execSync(`docker compose -f "${filePath}" ${args.join(" ")}`, {
18
40
  encoding: "utf-8",
19
41
  timeout: 30000,
20
42
  });
@@ -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
  }
package/src/types.ts CHANGED
@@ -73,20 +73,20 @@ export const RemoveImageSchema = z.object({
73
73
 
74
74
  // Docker Compose schemas
75
75
  export const ComposeUpSchema = z.object({
76
- path: z.string().describe("Path to docker-compose.yml directory"),
76
+ path: z.string().describe("Path to docker-compose.yml file or its parent directory"),
77
77
  build: z.boolean().optional().describe("Build images before starting (default: false)"),
78
78
  detach: z.boolean().optional().describe("Run in detached mode (default: true)"),
79
79
  services: z.array(z.string()).optional().describe("Specific services to start"),
80
80
  });
81
81
 
82
82
  export const ComposeDownSchema = z.object({
83
- path: z.string().describe("Path to docker-compose.yml directory"),
83
+ path: z.string().describe("Path to docker-compose.yml file or its parent directory"),
84
84
  volumes: z.boolean().optional().describe("Remove named volumes (default: false)"),
85
85
  timeout: z.number().optional().describe("Shutdown timeout in seconds (default: 10)"),
86
86
  });
87
87
 
88
88
  export const ComposePsSchema = z.object({
89
- path: z.string().describe("Path to docker-compose.yml directory"),
89
+ path: z.string().describe("Path to docker-compose.yml file or its parent directory"),
90
90
  });
91
91
 
92
92
  export const ComposeLogsSchema = z.object({
@@ -97,7 +97,7 @@ export const ComposeLogsSchema = z.object({
97
97
  });
98
98
 
99
99
  export const ComposeRestartSchema = z.object({
100
- path: z.string().describe("Path to docker-compose.yml directory"),
100
+ path: z.string().describe("Path to docker-compose.yml file or its parent directory"),
101
101
  services: z.array(z.string()).optional().describe("Specific services to restart (empty = all)"),
102
102
  timeout: z.number().optional().describe("Shutdown timeout in seconds (default: 10)"),
103
103
  });