@supernova123/docker-mcp-server 0.2.3 → 0.2.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.
- package/README.md +11 -1
- package/SECURITY.md +52 -0
- package/dist/docker.d.ts +5 -0
- package/dist/docker.js +19 -0
- package/dist/tools/compose.js +7 -6
- package/dist/tools/container.js +7 -7
- package/dist/tools/exec.js +3 -3
- package/dist/tools/health.js +3 -3
- package/dist/tools/image.js +4 -4
- package/dist/tools/logs.js +4 -3
- package/dist/tools/monitoring.js +8 -7
- package/dist/tools/network.js +2 -2
- package/dist/types.d.ts +2 -2
- package/dist/types.js +23 -15
- package/glama.json +12 -0
- package/package.json +1 -1
- package/src/docker.ts +20 -0
- package/src/tools/compose.ts +8 -3
- package/src/tools/container.ts +8 -1
- package/src/tools/exec.ts +4 -3
- package/src/tools/health.ts +4 -1
- package/src/tools/image.ts +5 -1
- package/src/tools/logs.ts +5 -3
- package/src/tools/monitoring.ts +14 -7
- package/src/tools/network.ts +3 -1
- package/src/types.ts +30 -15
package/README.md
CHANGED
|
@@ -141,11 +141,21 @@ claude mcp add docker -- npx -y @supernova123/docker-mcp-server
|
|
|
141
141
|
|
|
142
142
|
## Security
|
|
143
143
|
|
|
144
|
+
This server has **full Docker daemon access** via the Docker socket. It is designed for local development and trusted environments.
|
|
145
|
+
|
|
144
146
|
- **Read-only by default**: all container and image tools read state; write operations (start/stop/remove) require explicit tool calls
|
|
145
|
-
- **No API keys needed**: connects to local Docker
|
|
147
|
+
- **No API keys needed**: connects to local Docker socket (`/var/run/docker.sock`), not remote API tokens
|
|
146
148
|
- **No network access**: all operations are local Docker API calls
|
|
149
|
+
- **Input validation**: Zod schemas on every tool parameter — command injection, path traversal, and env injection are rejected at the schema level
|
|
150
|
+
- **Output sanitization**: ANSI escape codes, invisible Unicode, and Docker stream headers are stripped from all tool output
|
|
151
|
+
- **Output size caps**: log output capped at 100KB, general output at 1MB, to prevent LLM context overflow
|
|
152
|
+
- **Parameter bounds**: command arrays limited to 50 args, env to 50 vars, log tail to 10K lines, timeouts enforced (600s health, 300s events)
|
|
147
153
|
- **MIT License**: fully auditable
|
|
148
154
|
|
|
155
|
+
**Threat model**: any tool that calls Docker through this server can start any container with any flags, including privileged. The threat model is the same as giving a user shell access to the Docker socket. Do not expose this server to untrusted users.
|
|
156
|
+
|
|
157
|
+
For vulnerability reports, see [SECURITY.md](SECURITY.md).
|
|
158
|
+
|
|
149
159
|
## License
|
|
150
160
|
|
|
151
161
|
MIT
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
If you discover a security vulnerability in this project, please report it responsibly.
|
|
6
|
+
|
|
7
|
+
**Email:** security@friendlygeorge.org
|
|
8
|
+
|
|
9
|
+
Please do not file public GitHub issues for security bugs.
|
|
10
|
+
|
|
11
|
+
## Supported Versions
|
|
12
|
+
|
|
13
|
+
The latest release on npm (`@supernova123/docker-mcp-server`) is the only supported version. Security fixes are applied to the latest release only.
|
|
14
|
+
|
|
15
|
+
## Threat Model
|
|
16
|
+
|
|
17
|
+
This server has **full Docker daemon access** via the Docker socket (`/var/run/docker.sock`). It is designed for **local development and trusted environments**.
|
|
18
|
+
|
|
19
|
+
**In scope:**
|
|
20
|
+
- Input validation on all tool parameters (command injection, path traversal, env injection)
|
|
21
|
+
- Output sanitization (ANSI escape stripping, invisible Unicode removal, output size caps)
|
|
22
|
+
- Schema-level bounds on all numeric and array parameters
|
|
23
|
+
|
|
24
|
+
**Out of scope:**
|
|
25
|
+
- Exposing this server to untrusted users or the public internet
|
|
26
|
+
- Running in multi-tenant environments without additional isolation
|
|
27
|
+
- Docker socket access control (by design, the server needs full daemon access)
|
|
28
|
+
|
|
29
|
+
**Key security properties:**
|
|
30
|
+
- No embedded credentials — the server uses the Docker socket, not API tokens
|
|
31
|
+
- No network egress — all communication is local Docker API calls
|
|
32
|
+
- Zod input validation on every tool parameter
|
|
33
|
+
- Output sanitization prevents prompt injection via command output
|
|
34
|
+
- Read-only tools marked with `readOnlyHint: true` in MCP metadata
|
|
35
|
+
|
|
36
|
+
## Security Hardening (v0.2.5+)
|
|
37
|
+
|
|
38
|
+
Starting with v0.2.5, the server includes:
|
|
39
|
+
- **Command validation:** `exec_in_container` rejects shell metacharacters and enforces POSIX path rules
|
|
40
|
+
- **Path validation:** `build_image` restricts build context to local absolute paths (no URLs)
|
|
41
|
+
- **Output sanitization:** ANSI escapes, invisible Unicode, and Docker stream headers are stripped
|
|
42
|
+
- **Output size caps:** Log output capped at 100KB, general output at 1MB
|
|
43
|
+
- **Parameter bounds:** Command arrays limited to 50 args, env to 50 vars, log tail to 10K lines
|
|
44
|
+
- **Timeout enforcement:** Health watch (600s max), event listening (300s max)
|
|
45
|
+
|
|
46
|
+
## Dependencies
|
|
47
|
+
|
|
48
|
+
Run `npm audit` regularly. The CI pipeline includes `npm audit --audit-level=high` on every push.
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
MIT — see [LICENSE](LICENSE) for details.
|
package/dist/docker.d.ts
CHANGED
|
@@ -8,5 +8,10 @@ export declare function createDockerClient(options?: DockerClientOptions): Docke
|
|
|
8
8
|
export declare function formatError(error: unknown): string;
|
|
9
9
|
export declare function formatContainer(container: Dockerode.ContainerInfo): Record<string, unknown>;
|
|
10
10
|
export declare function formatImage(image: Dockerode.ImageInfo): Record<string, unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* Sanitize tool output: strip ANSI escapes, invisible Unicode, and truncate.
|
|
13
|
+
* Prevents prompt injection via output and caps LLM context cost.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sanitizeOutput(text: string, maxLength?: number): string;
|
|
11
16
|
export declare function formatBytes(bytes: number): string;
|
|
12
17
|
//# sourceMappingURL=docker.d.ts.map
|
package/dist/docker.js
CHANGED
|
@@ -47,6 +47,25 @@ export function formatImage(image) {
|
|
|
47
47
|
created: new Date(image.Created).toISOString(),
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Sanitize tool output: strip ANSI escapes, invisible Unicode, and truncate.
|
|
52
|
+
* Prevents prompt injection via output and caps LLM context cost.
|
|
53
|
+
*/
|
|
54
|
+
export function sanitizeOutput(text, maxLength = 1_000_000) {
|
|
55
|
+
// Strip ANSI escape codes (CSI, OSC, simple sequences)
|
|
56
|
+
text = text.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "");
|
|
57
|
+
text = text.replace(/\x1b\][^\x07]*\x07/g, ""); // OSC terminated by BEL
|
|
58
|
+
text = text.replace(/\x1b[@-Z\\-_]/g, ""); // Single-char escapes
|
|
59
|
+
// Strip invisible Unicode (Tag chars, bidi overrides, zero-width)
|
|
60
|
+
text = text.replace(/[\uE0001-\uE007F\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/g, "");
|
|
61
|
+
// Strip Docker stream-frame headers (8-byte prefix per frame)
|
|
62
|
+
text = text.replace(/^[\x00-\x0f]{8}/gm, "");
|
|
63
|
+
// Truncate to cap memory and LLM context cost
|
|
64
|
+
if (text.length > maxLength) {
|
|
65
|
+
return text.slice(0, maxLength) + `\n... [output truncated at ${maxLength} chars]`;
|
|
66
|
+
}
|
|
67
|
+
return text;
|
|
68
|
+
}
|
|
50
69
|
export function formatBytes(bytes) {
|
|
51
70
|
if (bytes === 0)
|
|
52
71
|
return "0 B";
|
package/dist/tools/compose.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, statSync } from "fs";
|
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { promisify } from "util";
|
|
5
5
|
import { ComposeUpSchema, ComposeDownSchema, ComposePsSchema, ComposeLogsSchema, ComposeRestartSchema, } from "../types.js";
|
|
6
|
-
import { formatError } from "../docker.js";
|
|
6
|
+
import { formatError, sanitizeOutput } from "../docker.js";
|
|
7
7
|
const execAsync = promisify(execCb);
|
|
8
8
|
const COMPOSE_FILE_NAMES = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
|
|
9
9
|
function resolveComposePath(inputPath) {
|
|
@@ -38,7 +38,7 @@ function runCompose(path, args) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
export function registerComposeTools(server) {
|
|
41
|
-
server.tool("compose_up", "Bring up Docker Compose services from a docker-compose.yml file. Optionally build images first.", ComposeUpSchema.shape, async (params) => {
|
|
41
|
+
server.tool("compose_up", "Bring up Docker Compose services from a docker-compose.yml file. Optionally build images first.", ComposeUpSchema.shape, { idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
42
42
|
try {
|
|
43
43
|
const args = ["up", "-d"];
|
|
44
44
|
if (params.build)
|
|
@@ -52,7 +52,7 @@ export function registerComposeTools(server) {
|
|
|
52
52
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
|
-
server.tool("compose_down", "Tear down Docker Compose services. Optionally remove named volumes.", ComposeDownSchema.shape, async (params) => {
|
|
55
|
+
server.tool("compose_down", "Tear down Docker Compose services. Optionally remove named volumes.", ComposeDownSchema.shape, { destructiveHint: true, openWorldHint: false }, async (params) => {
|
|
56
56
|
try {
|
|
57
57
|
const args = ["down"];
|
|
58
58
|
if (params.volumes)
|
|
@@ -66,7 +66,7 @@ export function registerComposeTools(server) {
|
|
|
66
66
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
67
67
|
}
|
|
68
68
|
});
|
|
69
|
-
server.tool("compose_ps", "List service states across a Docker Compose stack.", ComposePsSchema.shape, async (params) => {
|
|
69
|
+
server.tool("compose_ps", "List service states across a Docker Compose stack.", ComposePsSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
70
70
|
try {
|
|
71
71
|
const output = runCompose(params.path, ["ps", "--format", "json"]);
|
|
72
72
|
const lines = output.split("\n").filter(Boolean);
|
|
@@ -92,13 +92,14 @@ export function registerComposeTools(server) {
|
|
|
92
92
|
if (params.services?.length)
|
|
93
93
|
args.push(...params.services);
|
|
94
94
|
const output = runCompose(params.path, args);
|
|
95
|
-
|
|
95
|
+
// Use 100KB cap for log output to keep LLM context small
|
|
96
|
+
return { content: [{ type: "text", text: sanitizeOutput(output, 100_000) || "No logs found." }] };
|
|
96
97
|
}
|
|
97
98
|
catch (error) {
|
|
98
99
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
99
100
|
}
|
|
100
101
|
});
|
|
101
|
-
server.tool("compose_restart", "Restart Docker Compose services. Restart specific services or the entire stack.", ComposeRestartSchema.shape, async (params) => {
|
|
102
|
+
server.tool("compose_restart", "Restart Docker Compose services. Restart specific services or the entire stack.", ComposeRestartSchema.shape, { destructiveHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
102
103
|
try {
|
|
103
104
|
const args = ["restart"];
|
|
104
105
|
if (params.timeout)
|
package/dist/tools/container.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ListContainersSchema, InspectContainerSchema, StartContainerSchema, StopContainerSchema, RestartContainerSchema, RemoveContainerSchema, RecreateContainerSchema, RunContainerSchema, } from "../types.js";
|
|
2
2
|
import { formatContainer, formatError } from "../docker.js";
|
|
3
3
|
export function registerContainerTools(server, docker) {
|
|
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, async (params) => {
|
|
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
6
|
const containers = await docker.listContainers({
|
|
7
7
|
all: params.all ?? false,
|
|
@@ -18,7 +18,7 @@ export function registerContainerTools(server, docker) {
|
|
|
18
18
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
|
-
server.tool("inspect_container", "Get detailed configuration and state of a Docker container by ID or name.", InspectContainerSchema.shape, async (params) => {
|
|
21
|
+
server.tool("inspect_container", "Get detailed configuration and state of a Docker container by ID or name.", InspectContainerSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
22
22
|
try {
|
|
23
23
|
const container = docker.getContainer(params.container_id);
|
|
24
24
|
const info = await container.inspect();
|
|
@@ -28,7 +28,7 @@ export function registerContainerTools(server, docker) {
|
|
|
28
28
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
29
29
|
}
|
|
30
30
|
});
|
|
31
|
-
server.tool("start_container", "Start a stopped Docker container by ID or name.", StartContainerSchema.shape, async (params) => {
|
|
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
34
|
await container.start();
|
|
@@ -38,7 +38,7 @@ export function registerContainerTools(server, docker) {
|
|
|
38
38
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
39
39
|
}
|
|
40
40
|
});
|
|
41
|
-
server.tool("stop_container", "Stop a running Docker container by ID or name with optional timeout.", StopContainerSchema.shape, async (params) => {
|
|
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
44
|
await container.stop({ t: params.timeout ?? 10 });
|
|
@@ -62,7 +62,7 @@ export function registerContainerTools(server, docker) {
|
|
|
62
62
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
63
63
|
}
|
|
64
64
|
});
|
|
65
|
-
server.tool("remove_container", "Remove a Docker container by ID or name. Use force to remove running containers.", RemoveContainerSchema.shape, async (params) => {
|
|
65
|
+
server.tool("remove_container", "Remove a Docker container by ID or name. Use force to remove running containers.", RemoveContainerSchema.shape, { destructiveHint: true, openWorldHint: false }, async (params) => {
|
|
66
66
|
try {
|
|
67
67
|
const container = docker.getContainer(params.container_id);
|
|
68
68
|
await container.remove({ force: params.force ?? false });
|
|
@@ -72,7 +72,7 @@ export function registerContainerTools(server, docker) {
|
|
|
72
72
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
73
73
|
}
|
|
74
74
|
});
|
|
75
|
-
server.tool("recreate_container", "Recreate a container with the same configuration (stop, remove, re-create). Useful for applying config changes.", RecreateContainerSchema.shape, async (params) => {
|
|
75
|
+
server.tool("recreate_container", "Recreate a container with the same configuration (stop, remove, re-create). Useful for applying config changes.", RecreateContainerSchema.shape, { destructiveHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
76
76
|
try {
|
|
77
77
|
const container = docker.getContainer(params.container_id);
|
|
78
78
|
const info = await container.inspect();
|
|
@@ -107,7 +107,7 @@ export function registerContainerTools(server, docker) {
|
|
|
107
107
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
108
108
|
}
|
|
109
109
|
});
|
|
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) => {
|
|
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, { openWorldHint: false }, async (params) => {
|
|
111
111
|
try {
|
|
112
112
|
const createOpts = {
|
|
113
113
|
Image: params.image,
|
package/dist/tools/exec.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ExecInContainerSchema } from "../types.js";
|
|
2
|
-
import { formatError } from "../docker.js";
|
|
2
|
+
import { formatError, sanitizeOutput } from "../docker.js";
|
|
3
3
|
export function registerExecTools(server, docker) {
|
|
4
|
-
server.tool("exec_in_container", "Execute a command inside a running Docker container. Returns stdout, stderr, and exit code.", ExecInContainerSchema.shape, async (params) => {
|
|
4
|
+
server.tool("exec_in_container", "Execute a command inside a running Docker container. Returns stdout, stderr, and exit code.", ExecInContainerSchema.shape, { openWorldHint: false }, async (params) => {
|
|
5
5
|
try {
|
|
6
6
|
const container = docker.getContainer(params.container_id);
|
|
7
7
|
const exec = await container.exec({
|
|
@@ -18,7 +18,7 @@ export function registerExecTools(server, docker) {
|
|
|
18
18
|
stream.on("end", () => resolve(data));
|
|
19
19
|
});
|
|
20
20
|
const inspect = await exec.inspect();
|
|
21
|
-
const cleanOutput = output
|
|
21
|
+
const cleanOutput = sanitizeOutput(output);
|
|
22
22
|
return {
|
|
23
23
|
content: [{
|
|
24
24
|
type: "text",
|
package/dist/tools/health.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CheckHealthSchema, WatchHealthSchema, SetRestartPolicySchema } from "../types.js";
|
|
2
2
|
import { formatError } from "../docker.js";
|
|
3
3
|
export function registerHealthTools(server, docker) {
|
|
4
|
-
server.tool("check_health", "Run a health probe against a container. Supports HTTP, TCP, and exec probes. Auto-detects from container HEALTHCHECK if available.", CheckHealthSchema.shape, async (params) => {
|
|
4
|
+
server.tool("check_health", "Run a health probe against a container. Supports HTTP, TCP, and exec probes. Auto-detects from container HEALTHCHECK if available.", CheckHealthSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
5
5
|
try {
|
|
6
6
|
const container = docker.getContainer(params.container_id);
|
|
7
7
|
const info = await container.inspect();
|
|
@@ -62,7 +62,7 @@ export function registerHealthTools(server, docker) {
|
|
|
62
62
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
63
63
|
}
|
|
64
64
|
});
|
|
65
|
-
server.tool("watch_health", "Poll a container's health status until it becomes healthy or times out. Useful for waiting on service startup.", WatchHealthSchema.shape, async (params) => {
|
|
65
|
+
server.tool("watch_health", "Poll a container's health status until it becomes healthy or times out. Useful for waiting on service startup.", WatchHealthSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
66
66
|
try {
|
|
67
67
|
const container = docker.getContainer(params.container_id);
|
|
68
68
|
const timeout = (params.timeout ?? 60) * 1000;
|
|
@@ -108,7 +108,7 @@ export function registerHealthTools(server, docker) {
|
|
|
108
108
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
109
109
|
}
|
|
110
110
|
});
|
|
111
|
-
server.tool("set_restart_policy", "Change the restart policy of a running container without recreating it.", SetRestartPolicySchema.shape, async (params) => {
|
|
111
|
+
server.tool("set_restart_policy", "Change the restart policy of a running container without recreating it.", SetRestartPolicySchema.shape, { idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
112
112
|
try {
|
|
113
113
|
const container = docker.getContainer(params.container_id);
|
|
114
114
|
await container.update({
|
package/dist/tools/image.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ListImagesSchema, PullImageSchema, BuildImageSchema, RemoveImageSchema, } from "../types.js";
|
|
2
2
|
import { formatImage, formatError } from "../docker.js";
|
|
3
3
|
export function registerImageTools(server, docker) {
|
|
4
|
-
server.tool("list_images", "List Docker images with optional filters. Returns image IDs, tags, sizes, and creation dates.", ListImagesSchema.shape, async (params) => {
|
|
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
6
|
const images = await docker.listImages({
|
|
7
7
|
all: params.all ?? false,
|
|
@@ -14,7 +14,7 @@ export function registerImageTools(server, docker) {
|
|
|
14
14
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
15
15
|
}
|
|
16
16
|
});
|
|
17
|
-
server.tool("pull_image", "Pull a Docker image from a registry. Returns pull progress events.", PullImageSchema.shape, async (params) => {
|
|
17
|
+
server.tool("pull_image", "Pull a Docker image from a registry. Returns pull progress events.", PullImageSchema.shape, { idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
18
18
|
try {
|
|
19
19
|
const imageRef = params.tag ? `${params.image}:${params.tag}` : params.image;
|
|
20
20
|
const stream = await docker.pull(imageRef);
|
|
@@ -33,7 +33,7 @@ export function registerImageTools(server, docker) {
|
|
|
33
33
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
|
-
server.tool("build_image", "Build a Docker image from a Dockerfile or build context path.", BuildImageSchema.shape, async (params) => {
|
|
36
|
+
server.tool("build_image", "Build a Docker image from a Dockerfile or build context path.", BuildImageSchema.shape, { idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
37
37
|
try {
|
|
38
38
|
const stream = await docker.buildImage({
|
|
39
39
|
context: params.context,
|
|
@@ -53,7 +53,7 @@ export function registerImageTools(server, docker) {
|
|
|
53
53
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
54
54
|
}
|
|
55
55
|
});
|
|
56
|
-
server.tool("remove_image", "Remove a Docker image by name or ID. Use force to remove even if tagged.", RemoveImageSchema.shape, async (params) => {
|
|
56
|
+
server.tool("remove_image", "Remove a Docker image by name or ID. Use force to remove even if tagged.", RemoveImageSchema.shape, { destructiveHint: true, openWorldHint: false }, async (params) => {
|
|
57
57
|
try {
|
|
58
58
|
const image = docker.getImage(params.image);
|
|
59
59
|
await image.remove({ force: params.force ?? false });
|
package/dist/tools/logs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StreamLogsSchema, ContainerStatsSchema } from "../types.js";
|
|
2
|
-
import { formatError, formatBytes } from "../docker.js";
|
|
2
|
+
import { formatError, formatBytes, sanitizeOutput } 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 {
|
|
@@ -12,14 +12,15 @@ export function registerLogsTools(server, docker) {
|
|
|
12
12
|
follow: false,
|
|
13
13
|
});
|
|
14
14
|
// Dockerode returns a Buffer with multiplexed stream headers
|
|
15
|
-
|
|
15
|
+
// Use 100KB cap for logs to keep LLM context small
|
|
16
|
+
const output = sanitizeOutput(logs.toString("utf-8"), 100_000);
|
|
16
17
|
return { content: [{ type: "text", text: output || "No logs found." }] };
|
|
17
18
|
}
|
|
18
19
|
catch (error) {
|
|
19
20
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
20
21
|
}
|
|
21
22
|
});
|
|
22
|
-
server.tool("container_stats", "Get real-time resource usage statistics for a Docker container (CPU, memory, network, I/O).", ContainerStatsSchema.shape, async (params) => {
|
|
23
|
+
server.tool("container_stats", "Get real-time resource usage statistics for a Docker container (CPU, memory, network, I/O).", ContainerStatsSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
23
24
|
try {
|
|
24
25
|
const container = docker.getContainer(params.container_id);
|
|
25
26
|
const stats = await container.stats({ stream: false });
|
package/dist/tools/monitoring.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ContainerHealthStatusSchema, ContainerResourceUsageSchema, WatchEventsSchema, SearchLogsSchema, ResourceAlertCheckSchema, MonitorDashboardSchema, } from "../types.js";
|
|
2
|
+
import { sanitizeOutput } from "../docker.js";
|
|
2
3
|
export function registerMonitoringTools(server, docker) {
|
|
3
4
|
// 1. fleet_status — health status of all running containers
|
|
4
|
-
server.tool("container_health_status", "Check health status, uptime, and restart count for all running Docker containers. Returns JSON with container name, state, health probe status, and restart count.", ContainerHealthStatusSchema.shape, async (params) => {
|
|
5
|
+
server.tool("container_health_status", "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.", ContainerHealthStatusSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
5
6
|
try {
|
|
6
7
|
const containers = await docker.listContainers({ all: false });
|
|
7
8
|
const results = await Promise.all(containers.map(async (c) => {
|
|
@@ -29,7 +30,7 @@ export function registerMonitoringTools(server, docker) {
|
|
|
29
30
|
}
|
|
30
31
|
});
|
|
31
32
|
// 2. fleet_stats — resource usage for all running containers
|
|
32
|
-
server.tool("container_resource_usage", "Monitor CPU, memory, and network I/O across all running Docker containers. Returns sorted resource usage metrics with percentage breakdowns.", ContainerResourceUsageSchema.shape, async (params) => {
|
|
33
|
+
server.tool("container_resource_usage", "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.", ContainerResourceUsageSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
33
34
|
try {
|
|
34
35
|
const containers = await docker.listContainers({ all: false });
|
|
35
36
|
const results = await Promise.all(containers.map(async (c) => {
|
|
@@ -73,7 +74,7 @@ export function registerMonitoringTools(server, docker) {
|
|
|
73
74
|
}
|
|
74
75
|
});
|
|
75
76
|
// 3. watch_events — stream Docker events (simplified: collect events for a duration)
|
|
76
|
-
server.tool("watch_events", "Stream Docker container events (start, stop, die, restart, health_status) over a configurable time window. Filter by specific container or event type.", WatchEventsSchema.shape, async (params) => {
|
|
77
|
+
server.tool("watch_events", "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.", WatchEventsSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
77
78
|
try {
|
|
78
79
|
const durationMs = (params.duration || 30) * 1000;
|
|
79
80
|
const filter = {};
|
|
@@ -122,7 +123,7 @@ export function registerMonitoringTools(server, docker) {
|
|
|
122
123
|
}
|
|
123
124
|
});
|
|
124
125
|
// 4. search_logs — search logs across multiple containers
|
|
125
|
-
server.tool("search_logs", "Search Docker container logs across multiple containers using regex pattern matching. Returns matching log lines with container name and
|
|
126
|
+
server.tool("search_logs", "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.", SearchLogsSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
126
127
|
try {
|
|
127
128
|
const targetContainers = params.containers || [];
|
|
128
129
|
let containers;
|
|
@@ -146,7 +147,7 @@ export function registerMonitoringTools(server, docker) {
|
|
|
146
147
|
tail: params.tail || 500,
|
|
147
148
|
since: params.since ? Math.floor(new Date(params.since).getTime() / 1000) : undefined,
|
|
148
149
|
});
|
|
149
|
-
const output = logStream.toString("utf-8")
|
|
150
|
+
const output = sanitizeOutput(logStream.toString("utf-8"), 100_000);
|
|
150
151
|
const lines = output.split("\n");
|
|
151
152
|
for (const line of lines) {
|
|
152
153
|
if (regex.test(line)) {
|
|
@@ -168,7 +169,7 @@ export function registerMonitoringTools(server, docker) {
|
|
|
168
169
|
}
|
|
169
170
|
});
|
|
170
171
|
// 5. check_thresholds — check all containers against thresholds
|
|
171
|
-
server.tool("resource_alert_check", "
|
|
172
|
+
server.tool("resource_alert_check", "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.", ResourceAlertCheckSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
172
173
|
try {
|
|
173
174
|
const cpuThreshold = params.cpu_percent ?? 80;
|
|
174
175
|
const memThreshold = params.memory_percent ?? 80;
|
|
@@ -223,7 +224,7 @@ export function registerMonitoringTools(server, docker) {
|
|
|
223
224
|
}
|
|
224
225
|
});
|
|
225
226
|
// 6. monitor_dashboard — single-call fleet summary
|
|
226
|
-
server.tool("monitor_dashboard", "Comprehensive Docker fleet dashboard in a single API call.
|
|
227
|
+
server.tool("monitor_dashboard", "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.", MonitorDashboardSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
227
228
|
try {
|
|
228
229
|
const containers = await docker.listContainers({ all: false });
|
|
229
230
|
// Fleet health
|
package/dist/tools/network.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ListNetworksSchema, ListVolumesSchema } from "../types.js";
|
|
2
2
|
import { formatError } from "../docker.js";
|
|
3
3
|
export function registerNetworkTools(server, docker) {
|
|
4
|
-
server.tool("list_networks", "List Docker networks with optional filter. Returns network IDs, names, drivers, and scopes.", ListNetworksSchema.shape, async (params) => {
|
|
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
6
|
const networks = await docker.listNetworks({
|
|
7
7
|
filters: params.filter ? JSON.stringify({ name: [params.filter] }) : undefined,
|
|
@@ -25,7 +25,7 @@ export function registerNetworkTools(server, docker) {
|
|
|
25
25
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
26
26
|
}
|
|
27
27
|
});
|
|
28
|
-
server.tool("list_volumes", "List Docker volumes with optional filter. Returns volume names, drivers, mount points, and labels.", ListVolumesSchema.shape, async (params) => {
|
|
28
|
+
server.tool("list_volumes", "List Docker volumes with optional filter. Returns volume names, drivers, mount points, and labels.", ListVolumesSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
29
29
|
try {
|
|
30
30
|
const result = await docker.listVolumes({
|
|
31
31
|
filters: params.filter ? JSON.stringify({ name: [params.filter] }) : undefined,
|
package/dist/types.d.ts
CHANGED
|
@@ -320,15 +320,15 @@ export declare const WatchEventsSchema: z.ZodObject<{
|
|
|
320
320
|
since: z.ZodOptional<z.ZodString>;
|
|
321
321
|
duration: z.ZodOptional<z.ZodNumber>;
|
|
322
322
|
}, "strip", z.ZodTypeAny, {
|
|
323
|
+
duration?: number | undefined;
|
|
323
324
|
since?: string | undefined;
|
|
324
325
|
container?: string | undefined;
|
|
325
326
|
event_type?: "all" | "start" | "stop" | "die" | "restart" | "health_status" | "oom" | undefined;
|
|
326
|
-
duration?: number | undefined;
|
|
327
327
|
}, {
|
|
328
|
+
duration?: number | undefined;
|
|
328
329
|
since?: string | undefined;
|
|
329
330
|
container?: string | undefined;
|
|
330
331
|
event_type?: "all" | "start" | "stop" | "die" | "restart" | "health_status" | "oom" | undefined;
|
|
331
|
-
duration?: number | undefined;
|
|
332
332
|
}>;
|
|
333
333
|
export declare const SearchLogsSchema: z.ZodObject<{
|
|
334
334
|
pattern: z.ZodString;
|
package/dist/types.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
// Validation regexes (shared across schemas)
|
|
3
|
+
const SAFE_CMD_ARG = /^[A-Za-z0-9_./:@%+=,\-]+$/;
|
|
4
|
+
const SAFE_PATH = /^\/[A-Za-z0-9_./\-]+$/;
|
|
5
|
+
const SAFE_ENV_KEY = /^[A-Z_][A-Z0-9_]*$/;
|
|
6
|
+
const SAFE_BUILD_CONTEXT = /^\/[A-Za-z0-9_./\-]+$/;
|
|
2
7
|
// Container lifecycle schemas
|
|
3
8
|
export const ListContainersSchema = z.object({
|
|
4
9
|
all: z.boolean().optional().describe("Include stopped containers (default: false)"),
|
|
@@ -48,11 +53,12 @@ export const PullImageSchema = z.object({
|
|
|
48
53
|
tag: z.string().optional().describe("Tag to pull (default: 'latest')"),
|
|
49
54
|
});
|
|
50
55
|
export const BuildImageSchema = z.object({
|
|
51
|
-
context: z.string().
|
|
56
|
+
context: z.string().regex(SAFE_BUILD_CONTEXT, "Build context must be a local absolute path (no URLs, no '..')")
|
|
57
|
+
.max(4096).describe("Build context path (local absolute path only)"),
|
|
52
58
|
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"),
|
|
59
|
+
dockerfile: z.string().max(256).optional().describe("Dockerfile name relative to context (default: 'Dockerfile')"),
|
|
60
|
+
build_args: z.record(z.string().regex(SAFE_ENV_KEY, "Build arg key must be POSIX-style").max(256), z.string().max(4096)).optional().describe("Build arguments (keys must be POSIX-style)"),
|
|
61
|
+
target: z.string().max(256).optional().describe("Target build stage"),
|
|
56
62
|
});
|
|
57
63
|
export const RemoveImageSchema = z.object({
|
|
58
64
|
image: z.string().describe("Image name or ID"),
|
|
@@ -93,8 +99,8 @@ export const CheckHealthSchema = z.object({
|
|
|
93
99
|
});
|
|
94
100
|
export const WatchHealthSchema = z.object({
|
|
95
101
|
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)"),
|
|
102
|
+
timeout: z.number().max(600).optional().describe("Max seconds to wait (default: 60, max: 600)"),
|
|
103
|
+
interval: z.number().max(60).optional().describe("Seconds between polls (default: 5, max: 60)"),
|
|
98
104
|
});
|
|
99
105
|
export const SetRestartPolicySchema = z.object({
|
|
100
106
|
container_id: z.string().describe("Container ID or name"),
|
|
@@ -104,19 +110,21 @@ export const SetRestartPolicySchema = z.object({
|
|
|
104
110
|
// Logs schemas
|
|
105
111
|
export const StreamLogsSchema = z.object({
|
|
106
112
|
container_id: z.string().describe("Container ID or name"),
|
|
107
|
-
tail: z.number().optional().describe("Number of lines to show (default: 100)"),
|
|
113
|
+
tail: z.number().max(10000).optional().describe("Number of lines to show (default: 100, max: 10000)"),
|
|
108
114
|
since: z.string().optional().describe("Show logs since timestamp (e.g., '2026-01-01T00:00:00Z')"),
|
|
109
115
|
follow: z.boolean().optional().describe("Follow log output (default: false)"),
|
|
110
116
|
});
|
|
111
117
|
export const ContainerStatsSchema = z.object({
|
|
112
118
|
container_id: z.string().describe("Container ID or name"),
|
|
113
119
|
});
|
|
114
|
-
// Exec schema
|
|
120
|
+
// Exec schema (validated per Finding 8.1 — command/working_dir/env constraints)
|
|
115
121
|
export const ExecInContainerSchema = z.object({
|
|
116
122
|
container_id: z.string().describe("Container ID or name"),
|
|
117
|
-
command: z.array(z.string()).
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
command: z.array(z.string().max(500).regex(SAFE_CMD_ARG, "Command arg contains disallowed characters"))
|
|
124
|
+
.min(1).max(50).describe("Command to execute (max 50 args, alphanumeric + safe chars only)"),
|
|
125
|
+
working_dir: z.string().regex(SAFE_PATH, "Working directory must be an absolute path without '..'")
|
|
126
|
+
.max(1000).optional().describe("Working directory inside container (absolute path)"),
|
|
127
|
+
env: z.record(z.string().regex(SAFE_ENV_KEY, "Env key must be POSIX-style (A-Z, 0-9, _)").max(100), z.string().max(1000)).optional().describe("Environment variables (keys must be POSIX-style)"),
|
|
120
128
|
});
|
|
121
129
|
// Network/Volume schemas
|
|
122
130
|
export const ListNetworksSchema = z.object({
|
|
@@ -134,12 +142,12 @@ export const WatchEventsSchema = z.object({
|
|
|
134
142
|
container: z.string().optional().describe("Filter by container name or ID"),
|
|
135
143
|
event_type: z.enum(["start", "stop", "die", "restart", "health_status", "oom", "all"]).optional().describe("Filter by event type (default: all)"),
|
|
136
144
|
since: z.string().optional().describe("Show events since timestamp (e.g., '2026-01-01T00:00:00Z')"),
|
|
137
|
-
duration: z.number().optional().describe("Max seconds to listen (default: 30)"),
|
|
145
|
+
duration: z.number().max(300).optional().describe("Max seconds to listen (default: 30, max: 300)"),
|
|
138
146
|
});
|
|
139
147
|
export const SearchLogsSchema = z.object({
|
|
140
|
-
pattern: z.string().describe("Regex or grep pattern to search for"),
|
|
141
|
-
containers: z.array(z.string()).optional().describe("Specific containers to search (default: all running)"),
|
|
142
|
-
tail: z.number().optional().describe("Max lines to scan per container (default: 500)"),
|
|
148
|
+
pattern: z.string().max(1000).describe("Regex or grep pattern to search for"),
|
|
149
|
+
containers: z.array(z.string()).max(50).optional().describe("Specific containers to search (default: all running, max: 50)"),
|
|
150
|
+
tail: z.number().max(10000).optional().describe("Max lines to scan per container (default: 500, max: 10000)"),
|
|
143
151
|
since: z.string().optional().describe("Only search logs since timestamp"),
|
|
144
152
|
ignore_case: z.boolean().optional().describe("Case-insensitive search (default: false)"),
|
|
145
153
|
});
|
package/glama.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://glama.ai/schemas/glama.json",
|
|
3
|
+
"maintainers": ["friendlygeorge"],
|
|
4
|
+
"title": "Docker MCP Server",
|
|
5
|
+
"description": "31 tools for AI agent Docker management — container lifecycle, Compose stack operations, health checks, log streaming, and fleet monitoring through the Model Context Protocol.",
|
|
6
|
+
"tags": ["docker", "containers", "mcp", "compose", "health-checks", "monitoring", "devops", "ai-agents", "typescript", "self-healing"],
|
|
7
|
+
"repository": "https://github.com/friendlygeorge/docker-mcp-server",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"categories": ["virtualization", "developer-tools"],
|
|
10
|
+
"language": "TypeScript",
|
|
11
|
+
"runtime": "node"
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supernova123/docker-mcp-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.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",
|
package/src/docker.ts
CHANGED
|
@@ -56,6 +56,26 @@ export function formatImage(image: Dockerode.ImageInfo): Record<string, unknown>
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
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
|
+
|
|
59
79
|
export function formatBytes(bytes: number): string {
|
|
60
80
|
if (bytes === 0) return "0 B";
|
|
61
81
|
const k = 1024;
|
package/src/tools/compose.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
ComposeLogsSchema,
|
|
11
11
|
ComposeRestartSchema,
|
|
12
12
|
} from "../types.js";
|
|
13
|
-
import { formatError } from "../docker.js";
|
|
13
|
+
import { formatError, sanitizeOutput } from "../docker.js";
|
|
14
14
|
|
|
15
15
|
const execAsync = promisify(execCb);
|
|
16
16
|
|
|
@@ -52,6 +52,7 @@ export function registerComposeTools(server: McpServer): void {
|
|
|
52
52
|
"compose_up",
|
|
53
53
|
"Bring up Docker Compose services from a docker-compose.yml file. Optionally build images first.",
|
|
54
54
|
ComposeUpSchema.shape,
|
|
55
|
+
{ idempotentHint: true, openWorldHint: false },
|
|
55
56
|
async (params) => {
|
|
56
57
|
try {
|
|
57
58
|
const args = ["up", "-d"];
|
|
@@ -69,6 +70,7 @@ export function registerComposeTools(server: McpServer): void {
|
|
|
69
70
|
"compose_down",
|
|
70
71
|
"Tear down Docker Compose services. Optionally remove named volumes.",
|
|
71
72
|
ComposeDownSchema.shape,
|
|
73
|
+
{ destructiveHint: true, openWorldHint: false },
|
|
72
74
|
async (params) => {
|
|
73
75
|
try {
|
|
74
76
|
const args = ["down"];
|
|
@@ -86,6 +88,7 @@ export function registerComposeTools(server: McpServer): void {
|
|
|
86
88
|
"compose_ps",
|
|
87
89
|
"List service states across a Docker Compose stack.",
|
|
88
90
|
ComposePsSchema.shape,
|
|
91
|
+
{ readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
89
92
|
async (params) => {
|
|
90
93
|
try {
|
|
91
94
|
const output = runCompose(params.path, ["ps", "--format", "json"]);
|
|
@@ -111,7 +114,8 @@ export function registerComposeTools(server: McpServer): void {
|
|
|
111
114
|
if (params.follow) args.push("-f");
|
|
112
115
|
if (params.services?.length) args.push(...params.services);
|
|
113
116
|
const output = runCompose(params.path, args);
|
|
114
|
-
|
|
117
|
+
// Use 100KB cap for log output to keep LLM context small
|
|
118
|
+
return { content: [{ type: "text", text: sanitizeOutput(output, 100_000) || "No logs found." }] };
|
|
115
119
|
} catch (error) {
|
|
116
120
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
117
121
|
}
|
|
@@ -122,6 +126,7 @@ export function registerComposeTools(server: McpServer): void {
|
|
|
122
126
|
"compose_restart",
|
|
123
127
|
"Restart Docker Compose services. Restart specific services or the entire stack.",
|
|
124
128
|
ComposeRestartSchema.shape,
|
|
129
|
+
{ destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
125
130
|
async (params) => {
|
|
126
131
|
try {
|
|
127
132
|
const args = ["restart"];
|
|
@@ -134,4 +139,4 @@ export function registerComposeTools(server: McpServer): void {
|
|
|
134
139
|
}
|
|
135
140
|
}
|
|
136
141
|
);
|
|
137
|
-
}
|
|
142
|
+
}
|
package/src/tools/container.ts
CHANGED
|
@@ -17,6 +17,7 @@ export function registerContainerTools(server: McpServer, docker: Dockerode): vo
|
|
|
17
17
|
"list_containers",
|
|
18
18
|
"List Docker containers with optional filters (state, label, name). Returns container IDs, names, images, states, ports, and labels.",
|
|
19
19
|
ListContainersSchema.shape,
|
|
20
|
+
{ readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
20
21
|
async (params) => {
|
|
21
22
|
try {
|
|
22
23
|
const containers = await docker.listContainers({
|
|
@@ -39,6 +40,7 @@ export function registerContainerTools(server: McpServer, docker: Dockerode): vo
|
|
|
39
40
|
"inspect_container",
|
|
40
41
|
"Get detailed configuration and state of a Docker container by ID or name.",
|
|
41
42
|
InspectContainerSchema.shape,
|
|
43
|
+
{ readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
42
44
|
async (params) => {
|
|
43
45
|
try {
|
|
44
46
|
const container = docker.getContainer(params.container_id);
|
|
@@ -54,6 +56,7 @@ export function registerContainerTools(server: McpServer, docker: Dockerode): vo
|
|
|
54
56
|
"start_container",
|
|
55
57
|
"Start a stopped Docker container by ID or name.",
|
|
56
58
|
StartContainerSchema.shape,
|
|
59
|
+
{ idempotentHint: true, openWorldHint: false },
|
|
57
60
|
async (params) => {
|
|
58
61
|
try {
|
|
59
62
|
const container = docker.getContainer(params.container_id);
|
|
@@ -69,6 +72,7 @@ export function registerContainerTools(server: McpServer, docker: Dockerode): vo
|
|
|
69
72
|
"stop_container",
|
|
70
73
|
"Stop a running Docker container by ID or name with optional timeout.",
|
|
71
74
|
StopContainerSchema.shape,
|
|
75
|
+
{ destructiveHint: true, openWorldHint: false },
|
|
72
76
|
async (params) => {
|
|
73
77
|
try {
|
|
74
78
|
const container = docker.getContainer(params.container_id);
|
|
@@ -104,6 +108,7 @@ export function registerContainerTools(server: McpServer, docker: Dockerode): vo
|
|
|
104
108
|
"remove_container",
|
|
105
109
|
"Remove a Docker container by ID or name. Use force to remove running containers.",
|
|
106
110
|
RemoveContainerSchema.shape,
|
|
111
|
+
{ destructiveHint: true, openWorldHint: false },
|
|
107
112
|
async (params) => {
|
|
108
113
|
try {
|
|
109
114
|
const container = docker.getContainer(params.container_id);
|
|
@@ -119,6 +124,7 @@ export function registerContainerTools(server: McpServer, docker: Dockerode): vo
|
|
|
119
124
|
"recreate_container",
|
|
120
125
|
"Recreate a container with the same configuration (stop, remove, re-create). Useful for applying config changes.",
|
|
121
126
|
RecreateContainerSchema.shape,
|
|
127
|
+
{ destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
122
128
|
async (params) => {
|
|
123
129
|
try {
|
|
124
130
|
const container = docker.getContainer(params.container_id);
|
|
@@ -159,6 +165,7 @@ export function registerContainerTools(server: McpServer, docker: Dockerode): vo
|
|
|
159
165
|
"run_container",
|
|
160
166
|
"Create and start a new Docker container with one command. Supports image, env, ports, volumes, restart policy, and command override. Auto-pulls missing images.",
|
|
161
167
|
RunContainerSchema.shape,
|
|
168
|
+
{ openWorldHint: false },
|
|
162
169
|
async (params) => {
|
|
163
170
|
try {
|
|
164
171
|
const createOpts: Dockerode.ContainerCreateOptions = {
|
|
@@ -213,4 +220,4 @@ export function registerContainerTools(server: McpServer, docker: Dockerode): vo
|
|
|
213
220
|
}
|
|
214
221
|
}
|
|
215
222
|
);
|
|
216
|
-
}
|
|
223
|
+
}
|
package/src/tools/exec.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import Dockerode from "dockerode";
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { ExecInContainerSchema } from "../types.js";
|
|
4
|
-
import { formatError } from "../docker.js";
|
|
4
|
+
import { formatError, sanitizeOutput } from "../docker.js";
|
|
5
5
|
|
|
6
6
|
export function registerExecTools(server: McpServer, docker: Dockerode): void {
|
|
7
7
|
server.tool(
|
|
8
8
|
"exec_in_container",
|
|
9
9
|
"Execute a command inside a running Docker container. Returns stdout, stderr, and exit code.",
|
|
10
10
|
ExecInContainerSchema.shape,
|
|
11
|
+
{ openWorldHint: false },
|
|
11
12
|
async (params) => {
|
|
12
13
|
try {
|
|
13
14
|
const container = docker.getContainer(params.container_id);
|
|
@@ -27,7 +28,7 @@ export function registerExecTools(server: McpServer, docker: Dockerode): void {
|
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
const inspect = await exec.inspect();
|
|
30
|
-
const cleanOutput = output
|
|
31
|
+
const cleanOutput = sanitizeOutput(output);
|
|
31
32
|
|
|
32
33
|
return {
|
|
33
34
|
content: [{
|
|
@@ -45,4 +46,4 @@ export function registerExecTools(server: McpServer, docker: Dockerode): void {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
);
|
|
48
|
-
}
|
|
49
|
+
}
|
package/src/tools/health.ts
CHANGED
|
@@ -8,6 +8,7 @@ export function registerHealthTools(server: McpServer, docker: Dockerode): void
|
|
|
8
8
|
"check_health",
|
|
9
9
|
"Run a health probe against a container. Supports HTTP, TCP, and exec probes. Auto-detects from container HEALTHCHECK if available.",
|
|
10
10
|
CheckHealthSchema.shape,
|
|
11
|
+
{ readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
11
12
|
async (params) => {
|
|
12
13
|
try {
|
|
13
14
|
const container = docker.getContainer(params.container_id);
|
|
@@ -77,6 +78,7 @@ export function registerHealthTools(server: McpServer, docker: Dockerode): void
|
|
|
77
78
|
"watch_health",
|
|
78
79
|
"Poll a container's health status until it becomes healthy or times out. Useful for waiting on service startup.",
|
|
79
80
|
WatchHealthSchema.shape,
|
|
81
|
+
{ readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
80
82
|
async (params) => {
|
|
81
83
|
try {
|
|
82
84
|
const container = docker.getContainer(params.container_id);
|
|
@@ -130,6 +132,7 @@ export function registerHealthTools(server: McpServer, docker: Dockerode): void
|
|
|
130
132
|
"set_restart_policy",
|
|
131
133
|
"Change the restart policy of a running container without recreating it.",
|
|
132
134
|
SetRestartPolicySchema.shape,
|
|
135
|
+
{ idempotentHint: true, openWorldHint: false },
|
|
133
136
|
async (params) => {
|
|
134
137
|
try {
|
|
135
138
|
const container = docker.getContainer(params.container_id);
|
|
@@ -147,4 +150,4 @@ export function registerHealthTools(server: McpServer, docker: Dockerode): void
|
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
);
|
|
150
|
-
}
|
|
153
|
+
}
|
package/src/tools/image.ts
CHANGED
|
@@ -13,6 +13,7 @@ export function registerImageTools(server: McpServer, docker: Dockerode): void {
|
|
|
13
13
|
"list_images",
|
|
14
14
|
"List Docker images with optional filters. Returns image IDs, tags, sizes, and creation dates.",
|
|
15
15
|
ListImagesSchema.shape,
|
|
16
|
+
{ readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
16
17
|
async (params) => {
|
|
17
18
|
try {
|
|
18
19
|
const images = await docker.listImages({
|
|
@@ -31,6 +32,7 @@ export function registerImageTools(server: McpServer, docker: Dockerode): void {
|
|
|
31
32
|
"pull_image",
|
|
32
33
|
"Pull a Docker image from a registry. Returns pull progress events.",
|
|
33
34
|
PullImageSchema.shape,
|
|
35
|
+
{ idempotentHint: true, openWorldHint: false },
|
|
34
36
|
async (params) => {
|
|
35
37
|
try {
|
|
36
38
|
const imageRef = params.tag ? `${params.image}:${params.tag}` : params.image;
|
|
@@ -53,6 +55,7 @@ export function registerImageTools(server: McpServer, docker: Dockerode): void {
|
|
|
53
55
|
"build_image",
|
|
54
56
|
"Build a Docker image from a Dockerfile or build context path.",
|
|
55
57
|
BuildImageSchema.shape,
|
|
58
|
+
{ idempotentHint: true, openWorldHint: false },
|
|
56
59
|
async (params) => {
|
|
57
60
|
try {
|
|
58
61
|
const stream = await docker.buildImage(
|
|
@@ -79,6 +82,7 @@ export function registerImageTools(server: McpServer, docker: Dockerode): void {
|
|
|
79
82
|
"remove_image",
|
|
80
83
|
"Remove a Docker image by name or ID. Use force to remove even if tagged.",
|
|
81
84
|
RemoveImageSchema.shape,
|
|
85
|
+
{ destructiveHint: true, openWorldHint: false },
|
|
82
86
|
async (params) => {
|
|
83
87
|
try {
|
|
84
88
|
const image = docker.getImage(params.image);
|
|
@@ -89,4 +93,4 @@ export function registerImageTools(server: McpServer, docker: Dockerode): void {
|
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
95
|
);
|
|
92
|
-
}
|
|
96
|
+
}
|
package/src/tools/logs.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Dockerode from "dockerode";
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StreamLogsSchema, ContainerStatsSchema } from "../types.js";
|
|
4
|
-
import { formatError, formatBytes } from "../docker.js";
|
|
4
|
+
import { formatError, formatBytes, sanitizeOutput } from "../docker.js";
|
|
5
5
|
|
|
6
6
|
export function registerLogsTools(server: McpServer, docker: Dockerode): void {
|
|
7
7
|
server.tool(
|
|
@@ -20,7 +20,8 @@ export function registerLogsTools(server: McpServer, docker: Dockerode): void {
|
|
|
20
20
|
follow: false as const,
|
|
21
21
|
});
|
|
22
22
|
// Dockerode returns a Buffer with multiplexed stream headers
|
|
23
|
-
|
|
23
|
+
// Use 100KB cap for logs to keep LLM context small
|
|
24
|
+
const output = sanitizeOutput(logs.toString("utf-8"), 100_000);
|
|
24
25
|
return { content: [{ type: "text", text: output || "No logs found." }] };
|
|
25
26
|
} catch (error) {
|
|
26
27
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
@@ -32,6 +33,7 @@ export function registerLogsTools(server: McpServer, docker: Dockerode): void {
|
|
|
32
33
|
"container_stats",
|
|
33
34
|
"Get real-time resource usage statistics for a Docker container (CPU, memory, network, I/O).",
|
|
34
35
|
ContainerStatsSchema.shape,
|
|
36
|
+
{ readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
35
37
|
async (params) => {
|
|
36
38
|
try {
|
|
37
39
|
const container = docker.getContainer(params.container_id);
|
|
@@ -83,4 +85,4 @@ export function registerLogsTools(server: McpServer, docker: Dockerode): void {
|
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
);
|
|
86
|
-
}
|
|
88
|
+
}
|
package/src/tools/monitoring.ts
CHANGED
|
@@ -8,13 +8,15 @@ import {
|
|
|
8
8
|
ResourceAlertCheckSchema,
|
|
9
9
|
MonitorDashboardSchema,
|
|
10
10
|
} from "../types.js";
|
|
11
|
+
import { sanitizeOutput } from "../docker.js";
|
|
11
12
|
|
|
12
13
|
export function registerMonitoringTools(server: McpServer, docker: Dockerode): void {
|
|
13
14
|
// 1. fleet_status — health status of all running containers
|
|
14
15
|
server.tool(
|
|
15
16
|
"container_health_status",
|
|
16
|
-
"Check health status, uptime, and restart count for all running Docker containers. Returns JSON with container name, state, health probe status, and restart count.",
|
|
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.",
|
|
17
18
|
ContainerHealthStatusSchema.shape,
|
|
19
|
+
{ readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
18
20
|
async (params) => {
|
|
19
21
|
try {
|
|
20
22
|
const containers = await docker.listContainers({ all: false });
|
|
@@ -48,8 +50,9 @@ export function registerMonitoringTools(server: McpServer, docker: Dockerode): v
|
|
|
48
50
|
// 2. fleet_stats — resource usage for all running containers
|
|
49
51
|
server.tool(
|
|
50
52
|
"container_resource_usage",
|
|
51
|
-
"Monitor CPU, memory, and network I/O across all running Docker containers. Returns sorted resource usage metrics with percentage breakdowns.",
|
|
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.",
|
|
52
54
|
ContainerResourceUsageSchema.shape,
|
|
55
|
+
{ readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
53
56
|
async (params) => {
|
|
54
57
|
try {
|
|
55
58
|
const containers = await docker.listContainers({ all: false });
|
|
@@ -100,8 +103,9 @@ export function registerMonitoringTools(server: McpServer, docker: Dockerode): v
|
|
|
100
103
|
// 3. watch_events — stream Docker events (simplified: collect events for a duration)
|
|
101
104
|
server.tool(
|
|
102
105
|
"watch_events",
|
|
103
|
-
"Stream Docker container events (start, stop, die, restart, health_status) over a configurable time window. Filter by specific container or event type.",
|
|
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.",
|
|
104
107
|
WatchEventsSchema.shape,
|
|
108
|
+
{ readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
105
109
|
async (params) => {
|
|
106
110
|
try {
|
|
107
111
|
const durationMs = (params.duration || 30) * 1000;
|
|
@@ -156,8 +160,9 @@ export function registerMonitoringTools(server: McpServer, docker: Dockerode): v
|
|
|
156
160
|
// 4. search_logs — search logs across multiple containers
|
|
157
161
|
server.tool(
|
|
158
162
|
"search_logs",
|
|
159
|
-
"Search Docker container logs across multiple containers using regex pattern matching. Returns matching log lines with container name and
|
|
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.",
|
|
160
164
|
SearchLogsSchema.shape,
|
|
165
|
+
{ readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
161
166
|
async (params) => {
|
|
162
167
|
try {
|
|
163
168
|
const targetContainers = params.containers || [];
|
|
@@ -186,7 +191,7 @@ export function registerMonitoringTools(server: McpServer, docker: Dockerode): v
|
|
|
186
191
|
tail: params.tail || 500,
|
|
187
192
|
since: params.since ? Math.floor(new Date(params.since).getTime() / 1000) : undefined,
|
|
188
193
|
});
|
|
189
|
-
const output = logStream.toString("utf-8")
|
|
194
|
+
const output = sanitizeOutput(logStream.toString("utf-8"), 100_000);
|
|
190
195
|
const lines = output.split("\n");
|
|
191
196
|
for (const line of lines) {
|
|
192
197
|
if (regex.test(line)) {
|
|
@@ -211,8 +216,9 @@ export function registerMonitoringTools(server: McpServer, docker: Dockerode): v
|
|
|
211
216
|
// 5. check_thresholds — check all containers against thresholds
|
|
212
217
|
server.tool(
|
|
213
218
|
"resource_alert_check",
|
|
214
|
-
"
|
|
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.",
|
|
215
220
|
ResourceAlertCheckSchema.shape,
|
|
221
|
+
{ readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
216
222
|
async (params) => {
|
|
217
223
|
try {
|
|
218
224
|
const cpuThreshold = params.cpu_percent ?? 80;
|
|
@@ -274,8 +280,9 @@ export function registerMonitoringTools(server: McpServer, docker: Dockerode): v
|
|
|
274
280
|
// 6. monitor_dashboard — single-call fleet summary
|
|
275
281
|
server.tool(
|
|
276
282
|
"monitor_dashboard",
|
|
277
|
-
"Comprehensive Docker fleet dashboard in a single API call.
|
|
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.",
|
|
278
284
|
MonitorDashboardSchema.shape,
|
|
285
|
+
{ readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
279
286
|
async (params) => {
|
|
280
287
|
try {
|
|
281
288
|
const containers = await docker.listContainers({ all: false });
|
package/src/tools/network.ts
CHANGED
|
@@ -8,6 +8,7 @@ export function registerNetworkTools(server: McpServer, docker: Dockerode): void
|
|
|
8
8
|
"list_networks",
|
|
9
9
|
"List Docker networks with optional filter. Returns network IDs, names, drivers, and scopes.",
|
|
10
10
|
ListNetworksSchema.shape,
|
|
11
|
+
{ readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
11
12
|
async (params) => {
|
|
12
13
|
try {
|
|
13
14
|
const networks = await docker.listNetworks({
|
|
@@ -39,6 +40,7 @@ export function registerNetworkTools(server: McpServer, docker: Dockerode): void
|
|
|
39
40
|
"list_volumes",
|
|
40
41
|
"List Docker volumes with optional filter. Returns volume names, drivers, mount points, and labels.",
|
|
41
42
|
ListVolumesSchema.shape,
|
|
43
|
+
{ readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
42
44
|
async (params) => {
|
|
43
45
|
try {
|
|
44
46
|
const result = await docker.listVolumes({
|
|
@@ -57,4 +59,4 @@ export function registerNetworkTools(server: McpServer, docker: Dockerode): void
|
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
);
|
|
60
|
-
}
|
|
62
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
// Validation regexes (shared across schemas)
|
|
4
|
+
const SAFE_CMD_ARG = /^[A-Za-z0-9_./:@%+=,\-]+$/;
|
|
5
|
+
const SAFE_PATH = /^\/[A-Za-z0-9_./\-]+$/;
|
|
6
|
+
const SAFE_ENV_KEY = /^[A-Z_][A-Z0-9_]*$/;
|
|
7
|
+
const SAFE_BUILD_CONTEXT = /^\/[A-Za-z0-9_./\-]+$/;
|
|
8
|
+
|
|
3
9
|
// Container lifecycle schemas
|
|
4
10
|
export const ListContainersSchema = z.object({
|
|
5
11
|
all: z.boolean().optional().describe("Include stopped containers (default: false)"),
|
|
@@ -59,11 +65,15 @@ export const PullImageSchema = z.object({
|
|
|
59
65
|
});
|
|
60
66
|
|
|
61
67
|
export const BuildImageSchema = z.object({
|
|
62
|
-
context: z.string().
|
|
68
|
+
context: z.string().regex(SAFE_BUILD_CONTEXT, "Build context must be a local absolute path (no URLs, no '..')")
|
|
69
|
+
.max(4096).describe("Build context path (local absolute path only)"),
|
|
63
70
|
tag: z.string().describe("Tag for the built image (e.g., 'myapp:v1')"),
|
|
64
|
-
dockerfile: z.string().optional().describe("Dockerfile name relative to context (default: 'Dockerfile')"),
|
|
65
|
-
build_args: z.record(
|
|
66
|
-
|
|
71
|
+
dockerfile: z.string().max(256).optional().describe("Dockerfile name relative to context (default: 'Dockerfile')"),
|
|
72
|
+
build_args: z.record(
|
|
73
|
+
z.string().regex(SAFE_ENV_KEY, "Build arg key must be POSIX-style").max(256),
|
|
74
|
+
z.string().max(4096)
|
|
75
|
+
).optional().describe("Build arguments (keys must be POSIX-style)"),
|
|
76
|
+
target: z.string().max(256).optional().describe("Target build stage"),
|
|
67
77
|
});
|
|
68
78
|
|
|
69
79
|
export const RemoveImageSchema = z.object({
|
|
@@ -112,8 +122,8 @@ export const CheckHealthSchema = z.object({
|
|
|
112
122
|
|
|
113
123
|
export const WatchHealthSchema = z.object({
|
|
114
124
|
container_id: z.string().describe("Container ID or name"),
|
|
115
|
-
timeout: z.number().optional().describe("Max seconds to wait (default: 60)"),
|
|
116
|
-
interval: z.number().optional().describe("Seconds between polls (default: 5)"),
|
|
125
|
+
timeout: z.number().max(600).optional().describe("Max seconds to wait (default: 60, max: 600)"),
|
|
126
|
+
interval: z.number().max(60).optional().describe("Seconds between polls (default: 5, max: 60)"),
|
|
117
127
|
});
|
|
118
128
|
|
|
119
129
|
export const SetRestartPolicySchema = z.object({
|
|
@@ -125,7 +135,7 @@ export const SetRestartPolicySchema = z.object({
|
|
|
125
135
|
// Logs schemas
|
|
126
136
|
export const StreamLogsSchema = z.object({
|
|
127
137
|
container_id: z.string().describe("Container ID or name"),
|
|
128
|
-
tail: z.number().optional().describe("Number of lines to show (default: 100)"),
|
|
138
|
+
tail: z.number().max(10000).optional().describe("Number of lines to show (default: 100, max: 10000)"),
|
|
129
139
|
since: z.string().optional().describe("Show logs since timestamp (e.g., '2026-01-01T00:00:00Z')"),
|
|
130
140
|
follow: z.boolean().optional().describe("Follow log output (default: false)"),
|
|
131
141
|
});
|
|
@@ -134,12 +144,17 @@ export const ContainerStatsSchema = z.object({
|
|
|
134
144
|
container_id: z.string().describe("Container ID or name"),
|
|
135
145
|
});
|
|
136
146
|
|
|
137
|
-
// Exec schema
|
|
147
|
+
// Exec schema (validated per Finding 8.1 — command/working_dir/env constraints)
|
|
138
148
|
export const ExecInContainerSchema = z.object({
|
|
139
149
|
container_id: z.string().describe("Container ID or name"),
|
|
140
|
-
command: z.array(z.string()).
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
command: z.array(z.string().max(500).regex(SAFE_CMD_ARG, "Command arg contains disallowed characters"))
|
|
151
|
+
.min(1).max(50).describe("Command to execute (max 50 args, alphanumeric + safe chars only)"),
|
|
152
|
+
working_dir: z.string().regex(SAFE_PATH, "Working directory must be an absolute path without '..'")
|
|
153
|
+
.max(1000).optional().describe("Working directory inside container (absolute path)"),
|
|
154
|
+
env: z.record(
|
|
155
|
+
z.string().regex(SAFE_ENV_KEY, "Env key must be POSIX-style (A-Z, 0-9, _)").max(100),
|
|
156
|
+
z.string().max(1000)
|
|
157
|
+
).optional().describe("Environment variables (keys must be POSIX-style)"),
|
|
143
158
|
});
|
|
144
159
|
|
|
145
160
|
// Network/Volume schemas
|
|
@@ -163,13 +178,13 @@ export const WatchEventsSchema = z.object({
|
|
|
163
178
|
container: z.string().optional().describe("Filter by container name or ID"),
|
|
164
179
|
event_type: z.enum(["start", "stop", "die", "restart", "health_status", "oom", "all"]).optional().describe("Filter by event type (default: all)"),
|
|
165
180
|
since: z.string().optional().describe("Show events since timestamp (e.g., '2026-01-01T00:00:00Z')"),
|
|
166
|
-
duration: z.number().optional().describe("Max seconds to listen (default: 30)"),
|
|
181
|
+
duration: z.number().max(300).optional().describe("Max seconds to listen (default: 30, max: 300)"),
|
|
167
182
|
});
|
|
168
183
|
|
|
169
184
|
export const SearchLogsSchema = z.object({
|
|
170
|
-
pattern: z.string().describe("Regex or grep pattern to search for"),
|
|
171
|
-
containers: z.array(z.string()).optional().describe("Specific containers to search (default: all running)"),
|
|
172
|
-
tail: z.number().optional().describe("Max lines to scan per container (default: 500)"),
|
|
185
|
+
pattern: z.string().max(1000).describe("Regex or grep pattern to search for"),
|
|
186
|
+
containers: z.array(z.string()).max(50).optional().describe("Specific containers to search (default: all running, max: 50)"),
|
|
187
|
+
tail: z.number().max(10000).optional().describe("Max lines to scan per container (default: 500, max: 10000)"),
|
|
173
188
|
since: z.string().optional().describe("Only search logs since timestamp"),
|
|
174
189
|
ignore_case: z.boolean().optional().describe("Case-insensitive search (default: false)"),
|
|
175
190
|
});
|