@supernova123/docker-mcp-server 0.1.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.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/docker.d.ts +12 -0
- package/dist/docker.js +58 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +16 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +24 -0
- package/dist/tools/compose.d.ts +3 -0
- package/dist/tools/compose.js +95 -0
- package/dist/tools/container.d.ts +4 -0
- package/dist/tools/container.js +140 -0
- package/dist/tools/exec.d.ts +4 -0
- package/dist/tools/exec.js +39 -0
- package/dist/tools/health.d.ts +4 -0
- package/dist/tools/health.js +129 -0
- package/dist/tools/image.d.ts +4 -0
- package/dist/tools/image.js +67 -0
- package/dist/tools/logs.d.ts +4 -0
- package/dist/tools/logs.js +69 -0
- package/dist/tools/network.d.ts +4 -0
- package/dist/tools/network.js +47 -0
- package/dist/types.d.ts +309 -0
- package/dist/types.js +128 -0
- package/package.json +48 -0
- package/src/docker.ts +65 -0
- package/src/index.ts +20 -0
- package/src/server.ts +27 -0
- package/src/tools/compose.ts +114 -0
- package/src/tools/container.ts +193 -0
- package/src/tools/exec.ts +48 -0
- package/src/tools/health.ts +150 -0
- package/src/tools/image.ts +92 -0
- package/src/tools/logs.ts +85 -0
- package/src/tools/network.ts +60 -0
- package/src/types.ts +152 -0
- package/tests/container.test.ts +192 -0
- package/tests/image.test.ts +160 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +8 -0
package/dist/types.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Container lifecycle schemas
|
|
3
|
+
export const ListContainersSchema = z.object({
|
|
4
|
+
all: z.boolean().optional().describe("Include stopped containers (default: false)"),
|
|
5
|
+
label: z.array(z.string()).optional().describe("Filter by label (e.g., 'app=web')"),
|
|
6
|
+
name: z.string().optional().describe("Filter by name (partial match)"),
|
|
7
|
+
state: z.enum(["running", "stopped", "paused", "exited", "created", "restarting"]).optional().describe("Filter by state"),
|
|
8
|
+
});
|
|
9
|
+
export const InspectContainerSchema = z.object({
|
|
10
|
+
container_id: z.string().describe("Container ID or name"),
|
|
11
|
+
});
|
|
12
|
+
export const StartContainerSchema = z.object({
|
|
13
|
+
container_id: z.string().describe("Container ID or name"),
|
|
14
|
+
});
|
|
15
|
+
export const StopContainerSchema = z.object({
|
|
16
|
+
container_id: z.string().describe("Container ID or name"),
|
|
17
|
+
timeout: z.number().optional().describe("Seconds to wait before killing (default: 10)"),
|
|
18
|
+
});
|
|
19
|
+
export const RestartContainerSchema = z.object({
|
|
20
|
+
container_id: z.string().describe("Container ID or name"),
|
|
21
|
+
timeout: z.number().optional().describe("Seconds to wait before killing (default: 10)"),
|
|
22
|
+
});
|
|
23
|
+
export const RemoveContainerSchema = z.object({
|
|
24
|
+
container_id: z.string().describe("Container ID or name"),
|
|
25
|
+
force: z.boolean().optional().describe("Force removal even if running (default: false)"),
|
|
26
|
+
});
|
|
27
|
+
export const RecreateContainerSchema = z.object({
|
|
28
|
+
container_id: z.string().describe("Container ID or name"),
|
|
29
|
+
timeout: z.number().optional().describe("Seconds to wait before killing (default: 10)"),
|
|
30
|
+
});
|
|
31
|
+
export const RunContainerSchema = z.object({
|
|
32
|
+
image: z.string().describe("Image name (e.g., 'nginx:latest')"),
|
|
33
|
+
name: z.string().optional().describe("Container name"),
|
|
34
|
+
env: z.record(z.string()).optional().describe("Environment variables"),
|
|
35
|
+
ports: z.record(z.string()).optional().describe("Port mappings (e.g., {'8080/tcp': '80/tcp'})"),
|
|
36
|
+
volumes: z.array(z.string()).optional().describe("Volume mounts (e.g., ['/host/path:/container/path'])"),
|
|
37
|
+
restart_policy: z.enum(["no", "always", "unless-stopped", "on-failure"]).optional().describe("Restart policy"),
|
|
38
|
+
command: z.array(z.string()).optional().describe("Override command"),
|
|
39
|
+
detach: z.boolean().optional().describe("Run in detached mode (default: true)"),
|
|
40
|
+
});
|
|
41
|
+
// Image management schemas
|
|
42
|
+
export const ListImagesSchema = z.object({
|
|
43
|
+
all: z.boolean().optional().describe("Include intermediate images (default: false)"),
|
|
44
|
+
filter: z.string().optional().describe("Filter by reference"),
|
|
45
|
+
});
|
|
46
|
+
export const PullImageSchema = z.object({
|
|
47
|
+
image: z.string().describe("Image to pull (e.g., 'nginx:latest')"),
|
|
48
|
+
tag: z.string().optional().describe("Tag to pull (default: 'latest')"),
|
|
49
|
+
});
|
|
50
|
+
export const BuildImageSchema = z.object({
|
|
51
|
+
context: z.string().describe("Build context path or Dockerfile content"),
|
|
52
|
+
tag: z.string().describe("Tag for the built image (e.g., 'myapp:v1')"),
|
|
53
|
+
dockerfile: z.string().optional().describe("Dockerfile name relative to context (default: 'Dockerfile')"),
|
|
54
|
+
build_args: z.record(z.string()).optional().describe("Build arguments"),
|
|
55
|
+
target: z.string().optional().describe("Target build stage"),
|
|
56
|
+
});
|
|
57
|
+
export const RemoveImageSchema = z.object({
|
|
58
|
+
image: z.string().describe("Image name or ID"),
|
|
59
|
+
force: z.boolean().optional().describe("Force removal (default: false)"),
|
|
60
|
+
});
|
|
61
|
+
// Docker Compose schemas
|
|
62
|
+
export const ComposeUpSchema = z.object({
|
|
63
|
+
path: z.string().describe("Path to docker-compose.yml directory"),
|
|
64
|
+
build: z.boolean().optional().describe("Build images before starting (default: false)"),
|
|
65
|
+
detach: z.boolean().optional().describe("Run in detached mode (default: true)"),
|
|
66
|
+
services: z.array(z.string()).optional().describe("Specific services to start"),
|
|
67
|
+
});
|
|
68
|
+
export const ComposeDownSchema = z.object({
|
|
69
|
+
path: z.string().describe("Path to docker-compose.yml directory"),
|
|
70
|
+
volumes: z.boolean().optional().describe("Remove named volumes (default: false)"),
|
|
71
|
+
timeout: z.number().optional().describe("Shutdown timeout in seconds (default: 10)"),
|
|
72
|
+
});
|
|
73
|
+
export const ComposePsSchema = z.object({
|
|
74
|
+
path: z.string().describe("Path to docker-compose.yml directory"),
|
|
75
|
+
});
|
|
76
|
+
export const ComposeLogsSchema = z.object({
|
|
77
|
+
path: z.string().describe("Path to docker-compose.yml directory"),
|
|
78
|
+
services: z.array(z.string()).optional().describe("Specific services to tail"),
|
|
79
|
+
tail: z.number().optional().describe("Number of lines to show (default: 100)"),
|
|
80
|
+
follow: z.boolean().optional().describe("Follow log output (default: false)"),
|
|
81
|
+
});
|
|
82
|
+
export const ComposeRestartSchema = z.object({
|
|
83
|
+
path: z.string().describe("Path to docker-compose.yml directory"),
|
|
84
|
+
services: z.array(z.string()).optional().describe("Specific services to restart (empty = all)"),
|
|
85
|
+
timeout: z.number().optional().describe("Shutdown timeout in seconds (default: 10)"),
|
|
86
|
+
});
|
|
87
|
+
// Health schemas
|
|
88
|
+
export const CheckHealthSchema = z.object({
|
|
89
|
+
container_id: z.string().describe("Container ID or name"),
|
|
90
|
+
type: z.enum(["http", "tcp", "exec"]).optional().describe("Probe type (default: auto-detect from HEALTHCHECK)"),
|
|
91
|
+
endpoint: z.string().optional().describe("HTTP endpoint or TCP port"),
|
|
92
|
+
command: z.array(z.string()).optional().describe("Command for exec probe"),
|
|
93
|
+
});
|
|
94
|
+
export const WatchHealthSchema = z.object({
|
|
95
|
+
container_id: z.string().describe("Container ID or name"),
|
|
96
|
+
timeout: z.number().optional().describe("Max seconds to wait (default: 60)"),
|
|
97
|
+
interval: z.number().optional().describe("Seconds between polls (default: 5)"),
|
|
98
|
+
});
|
|
99
|
+
export const SetRestartPolicySchema = z.object({
|
|
100
|
+
container_id: z.string().describe("Container ID or name"),
|
|
101
|
+
policy: z.enum(["no", "always", "unless-stopped", "on-failure"]).describe("Restart policy"),
|
|
102
|
+
max_retry_count: z.number().optional().describe("Max retry count for on-failure (default: 0)"),
|
|
103
|
+
});
|
|
104
|
+
// Logs schemas
|
|
105
|
+
export const StreamLogsSchema = z.object({
|
|
106
|
+
container_id: z.string().describe("Container ID or name"),
|
|
107
|
+
tail: z.number().optional().describe("Number of lines to show (default: 100)"),
|
|
108
|
+
since: z.string().optional().describe("Show logs since timestamp (e.g., '2026-01-01T00:00:00Z')"),
|
|
109
|
+
follow: z.boolean().optional().describe("Follow log output (default: false)"),
|
|
110
|
+
});
|
|
111
|
+
export const ContainerStatsSchema = z.object({
|
|
112
|
+
container_id: z.string().describe("Container ID or name"),
|
|
113
|
+
});
|
|
114
|
+
// Exec schema
|
|
115
|
+
export const ExecInContainerSchema = z.object({
|
|
116
|
+
container_id: z.string().describe("Container ID or name"),
|
|
117
|
+
command: z.array(z.string()).describe("Command to execute"),
|
|
118
|
+
working_dir: z.string().optional().describe("Working directory inside container"),
|
|
119
|
+
env: z.record(z.string()).optional().describe("Environment variables"),
|
|
120
|
+
});
|
|
121
|
+
// Network/Volume schemas
|
|
122
|
+
export const ListNetworksSchema = z.object({
|
|
123
|
+
filter: z.string().optional().describe("Filter by name or driver"),
|
|
124
|
+
});
|
|
125
|
+
export const ListVolumesSchema = z.object({
|
|
126
|
+
filter: z.string().optional().describe("Filter by name or driver"),
|
|
127
|
+
});
|
|
128
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@supernova123/docker-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Docker MCP server designed for agents that need their containers to stay running. Health checks, auto-restart, Compose lifecycle, log streaming.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"docker-mcp-server": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsc && node dist/index.js",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"docker",
|
|
20
|
+
"container",
|
|
21
|
+
"compose",
|
|
22
|
+
"health-check",
|
|
23
|
+
"devops",
|
|
24
|
+
"ai-agent",
|
|
25
|
+
"model-context-protocol"
|
|
26
|
+
],
|
|
27
|
+
"author": "Nova",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/friendlygeorge/docker-mcp-server"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"postInstallInstructions": "Run with: npx @supernova123/docker-mcp-server",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
39
|
+
"dockerode": "^4.0.4",
|
|
40
|
+
"zod": "^3.24.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/dockerode": "^3.3.31",
|
|
44
|
+
"@types/node": "^22.0.0",
|
|
45
|
+
"typescript": "^5.7.0",
|
|
46
|
+
"vitest": "^3.1.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/docker.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
export function formatBytes(bytes: number): string {
|
|
60
|
+
if (bytes === 0) return "0 B";
|
|
61
|
+
const k = 1024;
|
|
62
|
+
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
|
63
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
64
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
65
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { createDockerClient } from "./docker.js";
|
|
5
|
+
import { createServer } from "./server.js";
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
const docker = createDockerClient();
|
|
9
|
+
const server = createServer(docker);
|
|
10
|
+
|
|
11
|
+
const transport = new StdioServerTransport();
|
|
12
|
+
await server.connect(transport);
|
|
13
|
+
|
|
14
|
+
process.stderr.write("Docker MCP Server running on stdio\n");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
main().catch((error) => {
|
|
18
|
+
process.stderr.write(`Fatal error: ${error}\n`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import Dockerode from "dockerode";
|
|
3
|
+
import { registerContainerTools } from "./tools/container.js";
|
|
4
|
+
import { registerImageTools } from "./tools/image.js";
|
|
5
|
+
import { registerComposeTools } from "./tools/compose.js";
|
|
6
|
+
import { registerHealthTools } from "./tools/health.js";
|
|
7
|
+
import { registerLogsTools } from "./tools/logs.js";
|
|
8
|
+
import { registerExecTools } from "./tools/exec.js";
|
|
9
|
+
import { registerNetworkTools } from "./tools/network.js";
|
|
10
|
+
|
|
11
|
+
export function createServer(docker: Dockerode): McpServer {
|
|
12
|
+
const server = new McpServer({
|
|
13
|
+
name: "docker-mcp-server",
|
|
14
|
+
version: "0.1.0",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Register all tool categories
|
|
18
|
+
registerContainerTools(server, docker);
|
|
19
|
+
registerImageTools(server, docker);
|
|
20
|
+
registerComposeTools(server);
|
|
21
|
+
registerHealthTools(server, docker);
|
|
22
|
+
registerLogsTools(server, docker);
|
|
23
|
+
registerExecTools(server, docker);
|
|
24
|
+
registerNetworkTools(server, docker);
|
|
25
|
+
|
|
26
|
+
return server;
|
|
27
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { execSync, exec as execCb } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import {
|
|
5
|
+
ComposeUpSchema,
|
|
6
|
+
ComposeDownSchema,
|
|
7
|
+
ComposePsSchema,
|
|
8
|
+
ComposeLogsSchema,
|
|
9
|
+
ComposeRestartSchema,
|
|
10
|
+
} from "../types.js";
|
|
11
|
+
import { formatError } from "../docker.js";
|
|
12
|
+
|
|
13
|
+
const execAsync = promisify(execCb);
|
|
14
|
+
|
|
15
|
+
function runCompose(path: string, args: string[]): string {
|
|
16
|
+
try {
|
|
17
|
+
const result = execSync(`docker compose -f ${path} ${args.join(" ")}`, {
|
|
18
|
+
encoding: "utf-8",
|
|
19
|
+
timeout: 30000,
|
|
20
|
+
});
|
|
21
|
+
return result.trim();
|
|
22
|
+
} catch (error: unknown) {
|
|
23
|
+
const err = error as { stderr?: string; stdout?: string };
|
|
24
|
+
throw new Error(err.stderr || err.stdout || formatError(error));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function registerComposeTools(server: McpServer): void {
|
|
29
|
+
server.tool(
|
|
30
|
+
"compose_up",
|
|
31
|
+
"Bring up Docker Compose services from a docker-compose.yml file. Optionally build images first.",
|
|
32
|
+
ComposeUpSchema.shape,
|
|
33
|
+
async (params) => {
|
|
34
|
+
try {
|
|
35
|
+
const args = ["up", "-d"];
|
|
36
|
+
if (params.build) args.push("--build");
|
|
37
|
+
if (params.services?.length) args.push(...params.services);
|
|
38
|
+
const output = runCompose(params.path, args);
|
|
39
|
+
return { content: [{ type: "text", text: output || "Compose services started." }] };
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
server.tool(
|
|
47
|
+
"compose_down",
|
|
48
|
+
"Tear down Docker Compose services. Optionally remove named volumes.",
|
|
49
|
+
ComposeDownSchema.shape,
|
|
50
|
+
async (params) => {
|
|
51
|
+
try {
|
|
52
|
+
const args = ["down"];
|
|
53
|
+
if (params.volumes) args.push("-v");
|
|
54
|
+
if (params.timeout) args.push(`--timeout ${params.timeout}`);
|
|
55
|
+
const output = runCompose(params.path, args);
|
|
56
|
+
return { content: [{ type: "text", text: output || "Compose services stopped." }] };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
server.tool(
|
|
64
|
+
"compose_ps",
|
|
65
|
+
"List service states across a Docker Compose stack.",
|
|
66
|
+
ComposePsSchema.shape,
|
|
67
|
+
async (params) => {
|
|
68
|
+
try {
|
|
69
|
+
const output = runCompose(params.path, ["ps", "--format", "json"]);
|
|
70
|
+
const lines = output.split("\n").filter(Boolean);
|
|
71
|
+
const services = lines.map((line) => {
|
|
72
|
+
try { return JSON.parse(line); } catch { return { raw: line }; }
|
|
73
|
+
});
|
|
74
|
+
return { content: [{ type: "text", text: JSON.stringify(services, null, 2) }] };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
server.tool(
|
|
82
|
+
"compose_logs",
|
|
83
|
+
"Tail logs from Docker Compose services. Supports filtering by service and line count.",
|
|
84
|
+
ComposeLogsSchema.shape,
|
|
85
|
+
async (params) => {
|
|
86
|
+
try {
|
|
87
|
+
const args = ["logs", "--tail", String(params.tail ?? 100)];
|
|
88
|
+
if (params.follow) args.push("-f");
|
|
89
|
+
if (params.services?.length) args.push(...params.services);
|
|
90
|
+
const output = runCompose(params.path, args);
|
|
91
|
+
return { content: [{ type: "text", text: output || "No logs found." }] };
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
server.tool(
|
|
99
|
+
"compose_restart",
|
|
100
|
+
"Restart Docker Compose services. Restart specific services or the entire stack.",
|
|
101
|
+
ComposeRestartSchema.shape,
|
|
102
|
+
async (params) => {
|
|
103
|
+
try {
|
|
104
|
+
const args = ["restart"];
|
|
105
|
+
if (params.timeout) args.push(`--timeout ${params.timeout}`);
|
|
106
|
+
if (params.services?.length) args.push(...params.services);
|
|
107
|
+
const output = runCompose(params.path, args);
|
|
108
|
+
return { content: [{ type: "text", text: output || "Compose services restarted." }] };
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import Dockerode from "dockerode";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import {
|
|
4
|
+
ListContainersSchema,
|
|
5
|
+
InspectContainerSchema,
|
|
6
|
+
StartContainerSchema,
|
|
7
|
+
StopContainerSchema,
|
|
8
|
+
RestartContainerSchema,
|
|
9
|
+
RemoveContainerSchema,
|
|
10
|
+
RecreateContainerSchema,
|
|
11
|
+
RunContainerSchema,
|
|
12
|
+
} from "../types.js";
|
|
13
|
+
import { formatContainer, formatError } from "../docker.js";
|
|
14
|
+
|
|
15
|
+
export function registerContainerTools(server: McpServer, docker: Dockerode): void {
|
|
16
|
+
server.tool(
|
|
17
|
+
"list_containers",
|
|
18
|
+
"List Docker containers with optional filters (state, label, name). Returns container IDs, names, images, states, ports, and labels.",
|
|
19
|
+
ListContainersSchema.shape,
|
|
20
|
+
async (params) => {
|
|
21
|
+
try {
|
|
22
|
+
const containers = await docker.listContainers({
|
|
23
|
+
all: params.all ?? false,
|
|
24
|
+
filters: JSON.stringify({
|
|
25
|
+
...(params.label ? { label: params.label } : {}),
|
|
26
|
+
...(params.name ? { name: [`/${params.name}`] } : {}),
|
|
27
|
+
...(params.state ? { status: [params.state] } : {}),
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
const results = containers.map(formatContainer);
|
|
31
|
+
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
server.tool(
|
|
39
|
+
"inspect_container",
|
|
40
|
+
"Get detailed configuration and state of a Docker container by ID or name.",
|
|
41
|
+
InspectContainerSchema.shape,
|
|
42
|
+
async (params) => {
|
|
43
|
+
try {
|
|
44
|
+
const container = docker.getContainer(params.container_id);
|
|
45
|
+
const info = await container.inspect();
|
|
46
|
+
return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
server.tool(
|
|
54
|
+
"start_container",
|
|
55
|
+
"Start a stopped Docker container by ID or name.",
|
|
56
|
+
StartContainerSchema.shape,
|
|
57
|
+
async (params) => {
|
|
58
|
+
try {
|
|
59
|
+
const container = docker.getContainer(params.container_id);
|
|
60
|
+
await container.start();
|
|
61
|
+
return { content: [{ type: "text", text: `Container ${params.container_id} started.` }] };
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
server.tool(
|
|
69
|
+
"stop_container",
|
|
70
|
+
"Stop a running Docker container by ID or name with optional timeout.",
|
|
71
|
+
StopContainerSchema.shape,
|
|
72
|
+
async (params) => {
|
|
73
|
+
try {
|
|
74
|
+
const container = docker.getContainer(params.container_id);
|
|
75
|
+
await container.stop({ t: params.timeout ?? 10 });
|
|
76
|
+
return { content: [{ type: "text", text: `Container ${params.container_id} stopped.` }] };
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
server.tool(
|
|
84
|
+
"restart_container",
|
|
85
|
+
"Restart a Docker container by ID or name with optional timeout.",
|
|
86
|
+
RestartContainerSchema.shape,
|
|
87
|
+
async (params) => {
|
|
88
|
+
try {
|
|
89
|
+
const container = docker.getContainer(params.container_id);
|
|
90
|
+
await container.restart({ t: params.timeout ?? 10 });
|
|
91
|
+
return { content: [{ type: "text", text: `Container ${params.container_id} restarted.` }] };
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
server.tool(
|
|
99
|
+
"remove_container",
|
|
100
|
+
"Remove a Docker container by ID or name. Use force to remove running containers.",
|
|
101
|
+
RemoveContainerSchema.shape,
|
|
102
|
+
async (params) => {
|
|
103
|
+
try {
|
|
104
|
+
const container = docker.getContainer(params.container_id);
|
|
105
|
+
await container.remove({ force: params.force ?? false });
|
|
106
|
+
return { content: [{ type: "text", text: `Container ${params.container_id} removed.` }] };
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
server.tool(
|
|
114
|
+
"recreate_container",
|
|
115
|
+
"Recreate a container with the same configuration (stop, remove, re-create). Useful for applying config changes.",
|
|
116
|
+
RecreateContainerSchema.shape,
|
|
117
|
+
async (params) => {
|
|
118
|
+
try {
|
|
119
|
+
const container = docker.getContainer(params.container_id);
|
|
120
|
+
const info = await container.inspect();
|
|
121
|
+
|
|
122
|
+
// Stop and remove
|
|
123
|
+
try { await container.stop({ t: params.timeout ?? 10 }); } catch { /* already stopped */ }
|
|
124
|
+
await container.remove({ force: true });
|
|
125
|
+
|
|
126
|
+
// Re-create with same config
|
|
127
|
+
const createOpts: Dockerode.ContainerCreateOptions = {
|
|
128
|
+
name: info.Name.replace(/^\//, ""),
|
|
129
|
+
Image: info.Config.Image,
|
|
130
|
+
Env: info.Config.Env,
|
|
131
|
+
Cmd: info.Config.Cmd,
|
|
132
|
+
WorkingDir: info.Config.WorkingDir,
|
|
133
|
+
Labels: info.Config.Labels || {},
|
|
134
|
+
HostConfig: {
|
|
135
|
+
Binds: info.HostConfig?.Binds,
|
|
136
|
+
PortBindings: info.HostConfig?.PortBindings,
|
|
137
|
+
RestartPolicy: info.HostConfig?.RestartPolicy,
|
|
138
|
+
NetworkMode: info.HostConfig?.NetworkMode,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const newContainer = await docker.createContainer(createOpts);
|
|
143
|
+
await newContainer.start();
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: "text", text: `Container recreated and started. New ID: ${newContainer.id.substring(0, 12)}` }],
|
|
146
|
+
};
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
server.tool(
|
|
154
|
+
"run_container",
|
|
155
|
+
"Create and start a new Docker container with one command. Supports image, env, ports, volumes, restart policy, and command override.",
|
|
156
|
+
RunContainerSchema.shape,
|
|
157
|
+
async (params) => {
|
|
158
|
+
try {
|
|
159
|
+
const createOpts: Dockerode.ContainerCreateOptions = {
|
|
160
|
+
Image: params.image,
|
|
161
|
+
name: params.name,
|
|
162
|
+
Env: params.env ? Object.entries(params.env).map(([k, v]) => `${k}=${v}`) : undefined,
|
|
163
|
+
Cmd: params.command,
|
|
164
|
+
ExposedPorts: params.ports
|
|
165
|
+
? Object.fromEntries(Object.keys(params.ports).map((k) => [k, {}]))
|
|
166
|
+
: undefined,
|
|
167
|
+
HostConfig: {
|
|
168
|
+
PortBindings: params.ports
|
|
169
|
+
? Object.fromEntries(
|
|
170
|
+
Object.entries(params.ports).map(([containerPort, hostBinding]) => [
|
|
171
|
+
containerPort,
|
|
172
|
+
[{ HostPort: hostBinding }],
|
|
173
|
+
])
|
|
174
|
+
)
|
|
175
|
+
: undefined,
|
|
176
|
+
Binds: params.volumes,
|
|
177
|
+
RestartPolicy: params.restart_policy
|
|
178
|
+
? { Name: params.restart_policy, MaximumRetryCount: 0 }
|
|
179
|
+
: undefined,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const container = await docker.createContainer(createOpts);
|
|
184
|
+
await container.start();
|
|
185
|
+
return {
|
|
186
|
+
content: [{ type: "text", text: `Container created and started. ID: ${container.id.substring(0, 12)}` }],
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Dockerode from "dockerode";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { ExecInContainerSchema } from "../types.js";
|
|
4
|
+
import { formatError } from "../docker.js";
|
|
5
|
+
|
|
6
|
+
export function registerExecTools(server: McpServer, docker: Dockerode): void {
|
|
7
|
+
server.tool(
|
|
8
|
+
"exec_in_container",
|
|
9
|
+
"Execute a command inside a running Docker container. Returns stdout, stderr, and exit code.",
|
|
10
|
+
ExecInContainerSchema.shape,
|
|
11
|
+
async (params) => {
|
|
12
|
+
try {
|
|
13
|
+
const container = docker.getContainer(params.container_id);
|
|
14
|
+
const exec = await container.exec({
|
|
15
|
+
Cmd: params.command,
|
|
16
|
+
AttachStdout: true,
|
|
17
|
+
AttachStderr: true,
|
|
18
|
+
WorkingDir: params.working_dir,
|
|
19
|
+
Env: params.env ? Object.entries(params.env).map(([k, v]) => `${k}=${v}`) : undefined,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const stream = await exec.start({});
|
|
23
|
+
const output = await new Promise<string>((resolve) => {
|
|
24
|
+
let data = "";
|
|
25
|
+
stream.on("data", (chunk: Buffer) => { data += chunk.toString(); });
|
|
26
|
+
stream.on("end", () => resolve(data));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const inspect = await exec.inspect();
|
|
30
|
+
const cleanOutput = output.replace(/^[\x00-\x0f]{8}/gm, "");
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
content: [{
|
|
34
|
+
type: "text",
|
|
35
|
+
text: JSON.stringify({
|
|
36
|
+
exitCode: inspect.ExitCode,
|
|
37
|
+
stdout: cleanOutput,
|
|
38
|
+
stderr: "",
|
|
39
|
+
}, null, 2),
|
|
40
|
+
}],
|
|
41
|
+
isError: inspect.ExitCode !== 0,
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
}
|