@supernova123/docker-mcp-server 0.1.5 → 0.2.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/.dockerignore +9 -0
- package/Dockerfile +29 -0
- package/README.md +21 -0
- package/dist/server.js +3 -1
- package/dist/tools/container.js +23 -2
- package/dist/tools/monitoring.d.ts +4 -0
- package/dist/tools/monitoring.js +309 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.js +24 -0
- package/package.json +3 -3
- package/src/server.ts +3 -1
- package/src/tools/container.ts +20 -2
- package/src/tools/monitoring.ts +369 -0
- package/src/types.ts +31 -0
- package/tests/monitoring.test.ts +411 -0
package/.dockerignore
ADDED
package/Dockerfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
FROM node:22-slim AS builder
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Copy package files first for better layer caching
|
|
6
|
+
COPY package.json package-lock.json ./
|
|
7
|
+
RUN npm ci --ignore-scripts
|
|
8
|
+
|
|
9
|
+
# Copy source and build
|
|
10
|
+
COPY tsconfig.json ./
|
|
11
|
+
COPY src/ ./src/
|
|
12
|
+
RUN npm run build
|
|
13
|
+
|
|
14
|
+
# Production stage
|
|
15
|
+
FROM node:22-slim
|
|
16
|
+
|
|
17
|
+
WORKDIR /app
|
|
18
|
+
|
|
19
|
+
# Copy package files and install production deps only
|
|
20
|
+
COPY package.json package-lock.json ./
|
|
21
|
+
RUN npm ci --omit=dev --ignore-scripts
|
|
22
|
+
|
|
23
|
+
# Copy built output
|
|
24
|
+
COPY --from=builder /app/dist/ ./dist/
|
|
25
|
+
|
|
26
|
+
# The server communicates via stdio (MCP transport)
|
|
27
|
+
# No EXPOSE needed — stdio transport doesn't listen on a port
|
|
28
|
+
|
|
29
|
+
ENTRYPOINT ["node", "dist/index.js"]
|
package/README.md
CHANGED
|
@@ -24,8 +24,19 @@ There are 11+ Docker MCP servers on npm. Most are stale, GPL-licensed, or only c
|
|
|
24
24
|
| **Auto-restart** | ✅ set_restart_policy | ❌ | ❌ |
|
|
25
25
|
| **Compose lifecycle** | ✅ up/down/ps/logs/restart | ❌ | ❌ |
|
|
26
26
|
| **Log streaming** | ✅ tail + timestamp filter | Basic | Basic |
|
|
27
|
+
| **Fleet monitoring** | ✅ 6 fleet tools (status, stats, events, logs, thresholds, dashboard) | ❌ | ❌ |
|
|
27
28
|
| **Agent positioning** | ✅ Built for agents | Generic Docker | Registry API |
|
|
28
29
|
|
|
30
|
+
## Use Cases
|
|
31
|
+
|
|
32
|
+
**Agent-managed deployments:** Your agent deploys a new version, checks health, waits for readiness, then switches traffic. If the health check fails, it auto-rolls back.
|
|
33
|
+
|
|
34
|
+
**Self-healing infrastructure:** Set `restart: always` on critical containers. Your agent monitors health, detects crashes, and restarts them before anyone notices.
|
|
35
|
+
|
|
36
|
+
**Compose stack orchestration:** Your agent brings up a full stack (app + db + redis), monitors service states, tails logs for errors, and tears down cleanly when done.
|
|
37
|
+
|
|
38
|
+
**Debugging sessions:** Your agent execs into a container, runs diagnostics, streams logs with timestamp filters, and captures stats — all without SSH.
|
|
39
|
+
|
|
29
40
|
## Quick Start
|
|
30
41
|
|
|
31
42
|
One command to run:
|
|
@@ -86,6 +97,16 @@ claude mcp add docker -- npx -y @supernova123/docker-mcp-server
|
|
|
86
97
|
| `compose_logs` | Tail Compose service logs |
|
|
87
98
|
| `compose_restart` | Restart Compose services |
|
|
88
99
|
|
|
100
|
+
### Fleet Monitoring
|
|
101
|
+
| Tool | Description |
|
|
102
|
+
|------|-------------|
|
|
103
|
+
| `fleet_status` | Health status of all running containers (state, health, uptime, restart count) |
|
|
104
|
+
| `fleet_stats` | Resource usage (CPU%, memory%, network I/O) for all running containers |
|
|
105
|
+
| `watch_events` | Collect Docker events (start, stop, die, restart, health) over a time window |
|
|
106
|
+
| `search_logs` | Search logs across multiple containers with regex/grep pattern |
|
|
107
|
+
| `check_thresholds` | Check containers against CPU/memory/restart thresholds, return violations |
|
|
108
|
+
| `monitor_dashboard` | Single-call fleet summary: health, top consumers, recent events, violations |
|
|
109
|
+
|
|
89
110
|
### Health & Self-Healing
|
|
90
111
|
| Tool | Description |
|
|
91
112
|
|------|-------------|
|
package/dist/server.js
CHANGED
|
@@ -6,10 +6,11 @@ import { registerHealthTools } from "./tools/health.js";
|
|
|
6
6
|
import { registerLogsTools } from "./tools/logs.js";
|
|
7
7
|
import { registerExecTools } from "./tools/exec.js";
|
|
8
8
|
import { registerNetworkTools } from "./tools/network.js";
|
|
9
|
+
import { registerMonitoringTools } from "./tools/monitoring.js";
|
|
9
10
|
export function createServer(docker) {
|
|
10
11
|
const server = new McpServer({
|
|
11
12
|
name: "docker-mcp-server",
|
|
12
|
-
version: "0.
|
|
13
|
+
version: "0.2.0",
|
|
13
14
|
});
|
|
14
15
|
// Register all tool categories
|
|
15
16
|
registerContainerTools(server, docker);
|
|
@@ -19,6 +20,7 @@ export function createServer(docker) {
|
|
|
19
20
|
registerLogsTools(server, docker);
|
|
20
21
|
registerExecTools(server, docker);
|
|
21
22
|
registerNetworkTools(server, docker);
|
|
23
|
+
registerMonitoringTools(server, docker);
|
|
22
24
|
return server;
|
|
23
25
|
}
|
|
24
26
|
//# sourceMappingURL=server.js.map
|
package/dist/tools/container.js
CHANGED
|
@@ -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.", RunContainerSchema.shape, async (params) => {
|
|
110
|
+
server.tool("run_container", "Create and start a new Docker container with one command. Supports image, env, ports, volumes, restart policy, and command override. Auto-pulls missing images.", RunContainerSchema.shape, async (params) => {
|
|
111
111
|
try {
|
|
112
112
|
const createOpts = {
|
|
113
113
|
Image: params.image,
|
|
@@ -130,7 +130,28 @@ export function registerContainerTools(server, docker) {
|
|
|
130
130
|
: undefined,
|
|
131
131
|
},
|
|
132
132
|
};
|
|
133
|
-
|
|
133
|
+
let container;
|
|
134
|
+
try {
|
|
135
|
+
container = await docker.createContainer(createOpts);
|
|
136
|
+
}
|
|
137
|
+
catch (createError) {
|
|
138
|
+
// Auto-pull if image not found (HTTP 404)
|
|
139
|
+
if (createError?.statusCode === 404 && /no such image|No such image/i.test(createError.message || "")) {
|
|
140
|
+
const stream = await docker.pull(params.image);
|
|
141
|
+
await new Promise((resolve, reject) => {
|
|
142
|
+
docker.modem.followProgress(stream, (err) => {
|
|
143
|
+
if (err)
|
|
144
|
+
reject(err);
|
|
145
|
+
else
|
|
146
|
+
resolve();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
container = await docker.createContainer(createOpts);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
throw createError;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
134
155
|
await container.start();
|
|
135
156
|
return {
|
|
136
157
|
content: [{ type: "text", text: `Container created and started. ID: ${container.id.substring(0, 12)}` }],
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { FleetStatusSchema, FleetStatsSchema, WatchEventsSchema, SearchLogsSchema, CheckThresholdsSchema, MonitorDashboardSchema, } from "../types.js";
|
|
2
|
+
export function registerMonitoringTools(server, docker) {
|
|
3
|
+
// 1. fleet_status — health status of all running containers
|
|
4
|
+
server.tool("fleet_status", "Get health status of all running containers. Returns name, state, health, uptime, and restart count for each.", FleetStatusSchema.shape, async (params) => {
|
|
5
|
+
try {
|
|
6
|
+
const containers = await docker.listContainers({ all: false });
|
|
7
|
+
const results = await Promise.all(containers.map(async (c) => {
|
|
8
|
+
const info = await docker.getContainer(c.Id).inspect();
|
|
9
|
+
return {
|
|
10
|
+
name: c.Names[0]?.replace(/^\//, "") || c.Id.slice(0, 12),
|
|
11
|
+
id: c.Id.slice(0, 12),
|
|
12
|
+
state: c.State,
|
|
13
|
+
status: c.Status,
|
|
14
|
+
health: info.State.Health?.Status || "no-healthcheck",
|
|
15
|
+
uptime: info.State.StartedAt,
|
|
16
|
+
restartCount: info.RestartCount,
|
|
17
|
+
image: c.Image,
|
|
18
|
+
};
|
|
19
|
+
}));
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
27
|
+
isError: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
// 2. fleet_stats — resource usage for all running containers
|
|
32
|
+
server.tool("fleet_stats", "Get resource usage (CPU%, memory%, network I/O) for all running containers. Sorted by usage.", FleetStatsSchema.shape, async (params) => {
|
|
33
|
+
try {
|
|
34
|
+
const containers = await docker.listContainers({ all: false });
|
|
35
|
+
const results = await Promise.all(containers.map(async (c) => {
|
|
36
|
+
const stats = await docker.getContainer(c.Id).stats({ stream: false });
|
|
37
|
+
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - (stats.precpu_stats?.cpu_usage?.total_usage ?? 0);
|
|
38
|
+
const systemDelta = stats.cpu_stats.system_cpu_usage - (stats.precpu_stats?.system_cpu_usage ?? 0);
|
|
39
|
+
const cpuCount = stats.cpu_stats.online_cpus ?? 1;
|
|
40
|
+
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * cpuCount * 100 : 0;
|
|
41
|
+
const memUsage = stats.memory_stats?.usage ?? 0;
|
|
42
|
+
const memLimit = stats.memory_stats?.limit ?? 1;
|
|
43
|
+
const memPercent = (memUsage / memLimit) * 100;
|
|
44
|
+
const netRx = Object.values(stats.networks ?? {}).reduce((sum, n) => sum + (n.rx_bytes ?? 0), 0);
|
|
45
|
+
const netTx = Object.values(stats.networks ?? {}).reduce((sum, n) => sum + (n.tx_bytes ?? 0), 0);
|
|
46
|
+
return {
|
|
47
|
+
name: c.Names[0]?.replace(/^\//, "") || c.Id.slice(0, 12),
|
|
48
|
+
id: c.Id.slice(0, 12),
|
|
49
|
+
cpu_percent: Math.round(cpuPercent * 100) / 100,
|
|
50
|
+
memory_usage_mb: Math.round((memUsage / 1024 / 1024) * 100) / 100,
|
|
51
|
+
memory_percent: Math.round(memPercent * 100) / 100,
|
|
52
|
+
network_rx_mb: Math.round((netRx / 1024 / 1024) * 100) / 100,
|
|
53
|
+
network_tx_mb: Math.round((netTx / 1024 / 1024) * 100) / 100,
|
|
54
|
+
};
|
|
55
|
+
}));
|
|
56
|
+
const sortBy = params.sort_by || "cpu";
|
|
57
|
+
results.sort((a, b) => {
|
|
58
|
+
if (sortBy === "cpu")
|
|
59
|
+
return b.cpu_percent - a.cpu_percent;
|
|
60
|
+
if (sortBy === "memory")
|
|
61
|
+
return b.memory_percent - a.memory_percent;
|
|
62
|
+
return (b.network_rx_mb + b.network_tx_mb) - (a.network_rx_mb + a.network_tx_mb);
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
71
|
+
isError: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
// 3. watch_events — stream Docker events (simplified: collect events for a duration)
|
|
76
|
+
server.tool("watch_events", "Collect Docker events (start, stop, die, restart, health_status) over a time window. Filter by container or event type.", WatchEventsSchema.shape, async (params) => {
|
|
77
|
+
try {
|
|
78
|
+
const durationMs = (params.duration || 30) * 1000;
|
|
79
|
+
const filter = {};
|
|
80
|
+
if (params.container)
|
|
81
|
+
filter.container = [params.container];
|
|
82
|
+
if (params.event_type && params.event_type !== "all")
|
|
83
|
+
filter.event = [params.event_type];
|
|
84
|
+
if (params.since)
|
|
85
|
+
filter.since = [params.since];
|
|
86
|
+
const events = [];
|
|
87
|
+
const stream = await docker.getEvents(filter);
|
|
88
|
+
await new Promise((resolve) => {
|
|
89
|
+
const timeout = setTimeout(() => {
|
|
90
|
+
resolve();
|
|
91
|
+
}, durationMs);
|
|
92
|
+
stream.on("data", (chunk) => {
|
|
93
|
+
try {
|
|
94
|
+
const event = JSON.parse(chunk.toString());
|
|
95
|
+
events.push({
|
|
96
|
+
type: event.Type,
|
|
97
|
+
action: event.Action,
|
|
98
|
+
container: event.Actor?.Attributes?.name || event.Actor?.ID?.slice(0, 12),
|
|
99
|
+
time: new Date(event.time * 1000).toISOString(),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch { }
|
|
103
|
+
});
|
|
104
|
+
stream.on("error", () => {
|
|
105
|
+
clearTimeout(timeout);
|
|
106
|
+
resolve();
|
|
107
|
+
});
|
|
108
|
+
stream.on("end", () => {
|
|
109
|
+
clearTimeout(timeout);
|
|
110
|
+
resolve();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
content: [{ type: "text", text: events.length ? JSON.stringify(events, null, 2) : "No events captured in the time window." }],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
120
|
+
isError: true,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
// 4. search_logs — search logs across multiple containers
|
|
125
|
+
server.tool("search_logs", "Search logs across multiple containers with regex/grep pattern. Returns matching lines with container name and timestamp.", SearchLogsSchema.shape, async (params) => {
|
|
126
|
+
try {
|
|
127
|
+
const targetContainers = params.containers || [];
|
|
128
|
+
let containers;
|
|
129
|
+
if (targetContainers.length > 0) {
|
|
130
|
+
containers = await Promise.all(targetContainers.map(async (id) => {
|
|
131
|
+
const info = await docker.getContainer(id).inspect();
|
|
132
|
+
return { id, name: info.Name.replace(/^\//, "") };
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const list = await docker.listContainers({ all: false });
|
|
137
|
+
containers = list.map((c) => ({ id: c.Id, name: c.Names[0]?.replace(/^\//, "") || c.Id.slice(0, 12) }));
|
|
138
|
+
}
|
|
139
|
+
const regex = new RegExp(params.pattern, params.ignore_case ? "i" : "");
|
|
140
|
+
const matches = [];
|
|
141
|
+
for (const container of containers) {
|
|
142
|
+
try {
|
|
143
|
+
const logStream = await docker.getContainer(container.id).logs({
|
|
144
|
+
stdout: true,
|
|
145
|
+
stderr: true,
|
|
146
|
+
tail: params.tail || 500,
|
|
147
|
+
since: params.since ? Math.floor(new Date(params.since).getTime() / 1000) : undefined,
|
|
148
|
+
});
|
|
149
|
+
const output = logStream.toString("utf-8").replace(/^[\x00-\x0f]{8}/gm, "");
|
|
150
|
+
const lines = output.split("\n");
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
if (regex.test(line)) {
|
|
153
|
+
matches.push({ container: container.name, line: line.trim() });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch { }
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
content: [{ type: "text", text: matches.length ? JSON.stringify(matches, null, 2) : "No matches found." }],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
return {
|
|
165
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
166
|
+
isError: true,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
// 5. check_thresholds — check all containers against thresholds
|
|
171
|
+
server.tool("check_thresholds", "Check all containers against defined thresholds (CPU > X%, memory > Y%, restarts > Z). Returns violations.", CheckThresholdsSchema.shape, async (params) => {
|
|
172
|
+
try {
|
|
173
|
+
const cpuThreshold = params.cpu_percent ?? 80;
|
|
174
|
+
const memThreshold = params.memory_percent ?? 80;
|
|
175
|
+
const restartThreshold = params.restart_count ?? 5;
|
|
176
|
+
const containers = await docker.listContainers({ all: false });
|
|
177
|
+
const violations = [];
|
|
178
|
+
for (const c of containers) {
|
|
179
|
+
const info = await docker.getContainer(c.Id).inspect();
|
|
180
|
+
const issues = [];
|
|
181
|
+
// Check restart count
|
|
182
|
+
if (info.RestartCount > restartThreshold) {
|
|
183
|
+
issues.push(`restarts: ${info.RestartCount} > ${restartThreshold}`);
|
|
184
|
+
}
|
|
185
|
+
// Check CPU and memory
|
|
186
|
+
try {
|
|
187
|
+
const stats = await docker.getContainer(c.Id).stats({ stream: false });
|
|
188
|
+
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - (stats.precpu_stats?.cpu_usage?.total_usage ?? 0);
|
|
189
|
+
const systemDelta = stats.cpu_stats.system_cpu_usage - (stats.precpu_stats?.system_cpu_usage ?? 0);
|
|
190
|
+
const cpuCount = stats.cpu_stats.online_cpus ?? 1;
|
|
191
|
+
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * cpuCount * 100 : 0;
|
|
192
|
+
const memUsage = stats.memory_stats?.usage ?? 0;
|
|
193
|
+
const memLimit = stats.memory_stats?.limit ?? 1;
|
|
194
|
+
const memPercent = (memUsage / memLimit) * 100;
|
|
195
|
+
if (cpuPercent > cpuThreshold)
|
|
196
|
+
issues.push(`cpu: ${Math.round(cpuPercent)}% > ${cpuThreshold}%`);
|
|
197
|
+
if (memPercent > memThreshold)
|
|
198
|
+
issues.push(`memory: ${Math.round(memPercent)}% > ${memThreshold}%`);
|
|
199
|
+
}
|
|
200
|
+
catch { }
|
|
201
|
+
if (issues.length > 0) {
|
|
202
|
+
violations.push({
|
|
203
|
+
container: c.Names[0]?.replace(/^\//, "") || c.Id.slice(0, 12),
|
|
204
|
+
id: c.Id.slice(0, 12),
|
|
205
|
+
issues,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
content: [{
|
|
211
|
+
type: "text",
|
|
212
|
+
text: violations.length
|
|
213
|
+
? JSON.stringify({ violations, checked: containers.length }, null, 2)
|
|
214
|
+
: JSON.stringify({ message: "All containers within thresholds.", checked: containers.length }),
|
|
215
|
+
}],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
221
|
+
isError: true,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
// 6. monitor_dashboard — single-call fleet summary
|
|
226
|
+
server.tool("monitor_dashboard", "Single-call fleet summary: health status, top resource consumers, recent events, threshold violations. Designed for agent quick-assessment.", MonitorDashboardSchema.shape, async (params) => {
|
|
227
|
+
try {
|
|
228
|
+
const containers = await docker.listContainers({ all: false });
|
|
229
|
+
// Fleet health
|
|
230
|
+
const health = await Promise.all(containers.map(async (c) => {
|
|
231
|
+
const info = await docker.getContainer(c.Id).inspect();
|
|
232
|
+
return {
|
|
233
|
+
name: c.Names[0]?.replace(/^\//, "") || c.Id.slice(0, 12),
|
|
234
|
+
state: c.State,
|
|
235
|
+
health: info.State.Health?.Status || "no-healthcheck",
|
|
236
|
+
restartCount: info.RestartCount,
|
|
237
|
+
};
|
|
238
|
+
}));
|
|
239
|
+
// Resource usage (top 5 by CPU)
|
|
240
|
+
const stats = await Promise.all(containers.map(async (c) => {
|
|
241
|
+
try {
|
|
242
|
+
const s = await docker.getContainer(c.Id).stats({ stream: false });
|
|
243
|
+
const cpuDelta = s.cpu_stats.cpu_usage.total_usage - (s.precpu_stats?.cpu_usage?.total_usage ?? 0);
|
|
244
|
+
const systemDelta = s.cpu_stats.system_cpu_usage - (s.precpu_stats?.system_cpu_usage ?? 0);
|
|
245
|
+
const cpuCount = s.cpu_stats.online_cpus ?? 1;
|
|
246
|
+
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * cpuCount * 100 : 0;
|
|
247
|
+
const memUsage = s.memory_stats?.usage ?? 0;
|
|
248
|
+
const memLimit = s.memory_stats?.limit ?? 1;
|
|
249
|
+
const memPercent = (memUsage / memLimit) * 100;
|
|
250
|
+
return {
|
|
251
|
+
name: c.Names[0]?.replace(/^\//, "") || c.Id.slice(0, 12),
|
|
252
|
+
cpu_percent: Math.round(cpuPercent * 100) / 100,
|
|
253
|
+
memory_percent: Math.round(memPercent * 100) / 100,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}));
|
|
260
|
+
const topConsumers = stats.filter(Boolean).sort((a, b) => b.cpu_percent - a.cpu_percent).slice(0, 5);
|
|
261
|
+
// Recent events (last 5 minutes) - use simple approach
|
|
262
|
+
const recentEvents = [];
|
|
263
|
+
try {
|
|
264
|
+
const sinceTs = Math.floor((Date.now() - 5 * 60 * 1000) / 1000);
|
|
265
|
+
const eventStream = await docker.getEvents({ since: sinceTs });
|
|
266
|
+
await new Promise((resolve) => {
|
|
267
|
+
const timeout = setTimeout(() => { resolve(); }, 2000);
|
|
268
|
+
eventStream.on("data", (chunk) => {
|
|
269
|
+
try {
|
|
270
|
+
const e = JSON.parse(chunk.toString());
|
|
271
|
+
recentEvents.push({
|
|
272
|
+
action: e.Action,
|
|
273
|
+
container: e.Actor?.Attributes?.name || e.Actor?.ID?.slice(0, 12),
|
|
274
|
+
time: new Date(e.time * 1000).toISOString(),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
catch { }
|
|
278
|
+
});
|
|
279
|
+
eventStream.on("error", () => { clearTimeout(timeout); resolve(); });
|
|
280
|
+
eventStream.on("end", () => { clearTimeout(timeout); resolve(); });
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
catch { }
|
|
284
|
+
// Threshold violations
|
|
285
|
+
const violations = stats.filter(Boolean).filter((s) => s.cpu_percent > 80 || s.memory_percent > 80);
|
|
286
|
+
const dashboard = {
|
|
287
|
+
summary: {
|
|
288
|
+
total_containers: containers.length,
|
|
289
|
+
running: containers.filter((c) => c.State === "running").length,
|
|
290
|
+
unhealthy: health.filter((h) => h.health === "unhealthy").length,
|
|
291
|
+
},
|
|
292
|
+
health,
|
|
293
|
+
top_cpu_consumers: topConsumers,
|
|
294
|
+
recent_events: recentEvents.slice(0, 10),
|
|
295
|
+
threshold_violations: violations,
|
|
296
|
+
};
|
|
297
|
+
return {
|
|
298
|
+
content: [{ type: "text", text: JSON.stringify(dashboard, null, 2) }],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
return {
|
|
303
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
304
|
+
isError: true,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
//# sourceMappingURL=monitoring.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -306,4 +306,61 @@ export declare const ListVolumesSchema: z.ZodObject<{
|
|
|
306
306
|
}, {
|
|
307
307
|
filter?: string | undefined;
|
|
308
308
|
}>;
|
|
309
|
+
export declare const FleetStatusSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
310
|
+
export declare const FleetStatsSchema: z.ZodObject<{
|
|
311
|
+
sort_by: z.ZodOptional<z.ZodEnum<["cpu", "memory", "network"]>>;
|
|
312
|
+
}, "strip", z.ZodTypeAny, {
|
|
313
|
+
sort_by?: "cpu" | "memory" | "network" | undefined;
|
|
314
|
+
}, {
|
|
315
|
+
sort_by?: "cpu" | "memory" | "network" | undefined;
|
|
316
|
+
}>;
|
|
317
|
+
export declare const WatchEventsSchema: z.ZodObject<{
|
|
318
|
+
container: z.ZodOptional<z.ZodString>;
|
|
319
|
+
event_type: z.ZodOptional<z.ZodEnum<["start", "stop", "die", "restart", "health_status", "oom", "all"]>>;
|
|
320
|
+
since: z.ZodOptional<z.ZodString>;
|
|
321
|
+
duration: z.ZodOptional<z.ZodNumber>;
|
|
322
|
+
}, "strip", z.ZodTypeAny, {
|
|
323
|
+
since?: string | undefined;
|
|
324
|
+
container?: string | undefined;
|
|
325
|
+
event_type?: "all" | "start" | "stop" | "die" | "restart" | "health_status" | "oom" | undefined;
|
|
326
|
+
duration?: number | undefined;
|
|
327
|
+
}, {
|
|
328
|
+
since?: string | undefined;
|
|
329
|
+
container?: string | undefined;
|
|
330
|
+
event_type?: "all" | "start" | "stop" | "die" | "restart" | "health_status" | "oom" | undefined;
|
|
331
|
+
duration?: number | undefined;
|
|
332
|
+
}>;
|
|
333
|
+
export declare const SearchLogsSchema: z.ZodObject<{
|
|
334
|
+
pattern: z.ZodString;
|
|
335
|
+
containers: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
336
|
+
tail: z.ZodOptional<z.ZodNumber>;
|
|
337
|
+
since: z.ZodOptional<z.ZodString>;
|
|
338
|
+
ignore_case: z.ZodOptional<z.ZodBoolean>;
|
|
339
|
+
}, "strip", z.ZodTypeAny, {
|
|
340
|
+
pattern: string;
|
|
341
|
+
tail?: number | undefined;
|
|
342
|
+
since?: string | undefined;
|
|
343
|
+
containers?: string[] | undefined;
|
|
344
|
+
ignore_case?: boolean | undefined;
|
|
345
|
+
}, {
|
|
346
|
+
pattern: string;
|
|
347
|
+
tail?: number | undefined;
|
|
348
|
+
since?: string | undefined;
|
|
349
|
+
containers?: string[] | undefined;
|
|
350
|
+
ignore_case?: boolean | undefined;
|
|
351
|
+
}>;
|
|
352
|
+
export declare const CheckThresholdsSchema: z.ZodObject<{
|
|
353
|
+
cpu_percent: z.ZodOptional<z.ZodNumber>;
|
|
354
|
+
memory_percent: z.ZodOptional<z.ZodNumber>;
|
|
355
|
+
restart_count: z.ZodOptional<z.ZodNumber>;
|
|
356
|
+
}, "strip", z.ZodTypeAny, {
|
|
357
|
+
cpu_percent?: number | undefined;
|
|
358
|
+
memory_percent?: number | undefined;
|
|
359
|
+
restart_count?: number | undefined;
|
|
360
|
+
}, {
|
|
361
|
+
cpu_percent?: number | undefined;
|
|
362
|
+
memory_percent?: number | undefined;
|
|
363
|
+
restart_count?: number | undefined;
|
|
364
|
+
}>;
|
|
365
|
+
export declare const MonitorDashboardSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
309
366
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
CHANGED
|
@@ -125,4 +125,28 @@ export const ListNetworksSchema = z.object({
|
|
|
125
125
|
export const ListVolumesSchema = z.object({
|
|
126
126
|
filter: z.string().optional().describe("Filter by name or driver"),
|
|
127
127
|
});
|
|
128
|
+
// Monitoring schemas (v0.2.0)
|
|
129
|
+
export const FleetStatusSchema = z.object({});
|
|
130
|
+
export const FleetStatsSchema = z.object({
|
|
131
|
+
sort_by: z.enum(["cpu", "memory", "network"]).optional().describe("Sort results by metric (default: cpu)"),
|
|
132
|
+
});
|
|
133
|
+
export const WatchEventsSchema = z.object({
|
|
134
|
+
container: z.string().optional().describe("Filter by container name or ID"),
|
|
135
|
+
event_type: z.enum(["start", "stop", "die", "restart", "health_status", "oom", "all"]).optional().describe("Filter by event type (default: all)"),
|
|
136
|
+
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)"),
|
|
138
|
+
});
|
|
139
|
+
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)"),
|
|
143
|
+
since: z.string().optional().describe("Only search logs since timestamp"),
|
|
144
|
+
ignore_case: z.boolean().optional().describe("Case-insensitive search (default: false)"),
|
|
145
|
+
});
|
|
146
|
+
export const CheckThresholdsSchema = z.object({
|
|
147
|
+
cpu_percent: z.number().optional().describe("Alert if CPU usage exceeds this % (default: 80)"),
|
|
148
|
+
memory_percent: z.number().optional().describe("Alert if memory usage exceeds this % (default: 80)"),
|
|
149
|
+
restart_count: z.number().optional().describe("Alert if restart count exceeds this (default: 5)"),
|
|
150
|
+
});
|
|
151
|
+
export const MonitorDashboardSchema = z.object({});
|
|
128
152
|
//# sourceMappingURL=types.js.map
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supernova123/docker-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"mcpName": "io.github.friendlygeorge/docker-mcp-server",
|
|
5
|
-
"description": "MCP server for Docker
|
|
5
|
+
"description": "MCP server for Docker \u2014 container management, health checks, auto-restart, Compose lifecycle, and log streaming for Claude, Cursor, and AI agents",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"bin": {
|
|
@@ -53,4 +53,4 @@
|
|
|
53
53
|
"typescript": "^5.7.0",
|
|
54
54
|
"vitest": "^3.1.0"
|
|
55
55
|
}
|
|
56
|
-
}
|
|
56
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -7,11 +7,12 @@ import { registerHealthTools } from "./tools/health.js";
|
|
|
7
7
|
import { registerLogsTools } from "./tools/logs.js";
|
|
8
8
|
import { registerExecTools } from "./tools/exec.js";
|
|
9
9
|
import { registerNetworkTools } from "./tools/network.js";
|
|
10
|
+
import { registerMonitoringTools } from "./tools/monitoring.js";
|
|
10
11
|
|
|
11
12
|
export function createServer(docker: Dockerode): McpServer {
|
|
12
13
|
const server = new McpServer({
|
|
13
14
|
name: "docker-mcp-server",
|
|
14
|
-
version: "0.
|
|
15
|
+
version: "0.2.0",
|
|
15
16
|
});
|
|
16
17
|
|
|
17
18
|
// Register all tool categories
|
|
@@ -22,6 +23,7 @@ export function createServer(docker: Dockerode): McpServer {
|
|
|
22
23
|
registerLogsTools(server, docker);
|
|
23
24
|
registerExecTools(server, docker);
|
|
24
25
|
registerNetworkTools(server, docker);
|
|
26
|
+
registerMonitoringTools(server, docker);
|
|
25
27
|
|
|
26
28
|
return server;
|
|
27
29
|
}
|
package/src/tools/container.ts
CHANGED
|
@@ -156,7 +156,7 @@ export function registerContainerTools(server: McpServer, docker: Dockerode): vo
|
|
|
156
156
|
|
|
157
157
|
server.tool(
|
|
158
158
|
"run_container",
|
|
159
|
-
"Create and start a new Docker container with one command. Supports image, env, ports, volumes, restart policy, and command override.",
|
|
159
|
+
"Create and start a new Docker container with one command. Supports image, env, ports, volumes, restart policy, and command override. Auto-pulls missing images.",
|
|
160
160
|
RunContainerSchema.shape,
|
|
161
161
|
async (params) => {
|
|
162
162
|
try {
|
|
@@ -184,7 +184,25 @@ export function registerContainerTools(server: McpServer, docker: Dockerode): vo
|
|
|
184
184
|
},
|
|
185
185
|
};
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
let container;
|
|
188
|
+
try {
|
|
189
|
+
container = await docker.createContainer(createOpts);
|
|
190
|
+
} catch (createError: any) {
|
|
191
|
+
// Auto-pull if image not found (HTTP 404)
|
|
192
|
+
if (createError?.statusCode === 404 && /no such image|No such image/i.test(createError.message || "")) {
|
|
193
|
+
const stream = await docker.pull(params.image);
|
|
194
|
+
await new Promise<void>((resolve, reject) => {
|
|
195
|
+
docker.modem.followProgress(stream, (err: Error | null) => {
|
|
196
|
+
if (err) reject(err);
|
|
197
|
+
else resolve();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
container = await docker.createContainer(createOpts);
|
|
201
|
+
} else {
|
|
202
|
+
throw createError;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
188
206
|
await container.start();
|
|
189
207
|
return {
|
|
190
208
|
content: [{ type: "text", text: `Container created and started. ID: ${container.id.substring(0, 12)}` }],
|