@supernova123/docker-mcp-server 0.3.3 → 0.3.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/.github/workflows/ci.yml +23 -0
- package/CHANGELOG.md +27 -1
- package/README.md +2 -0
- package/dist/server.js +3 -1
- package/dist/tools/compose.js +2 -2
- package/dist/tools/container.js +86 -1
- package/dist/tools/health.js +1 -1
- package/dist/tools/image.js +44 -2
- package/dist/tools/transfer.d.ts +4 -0
- package/dist/tools/transfer.js +175 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +23 -0
- package/glama.json +28 -5
- package/package.json +9 -4
- package/src/cf-worker/README.md +73 -0
- package/src/cf-worker/index.ts +546 -0
- package/src/cf-worker/landing.html +390 -0
- package/src/cf-worker/mcp-agent.ts +362 -0
- package/src/cf-worker/types.ts +38 -0
- package/src/server.ts +4 -2
- package/src/tools/compose.ts +2 -2
- package/src/tools/container.ts +106 -0
- package/src/tools/health.ts +1 -1
- package/src/tools/image.ts +54 -1
- package/src/tools/transfer.ts +245 -0
- package/src/types.ts +29 -1
- package/tests/transfer.test.ts +176 -0
- package/tsconfig.cf.json +17 -0
- package/tsconfig.json +1 -1
- package/wrangler.jsonc +19 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
node-version: [18, 20, 22]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: ${{ matrix.node-version }}
|
|
20
|
+
cache: npm
|
|
21
|
+
- run: npm ci
|
|
22
|
+
- run: npm test
|
|
23
|
+
- run: npx tsc --noEmit
|
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.5] - 2026-06-14
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `prune_containers` — Remove all stopped Docker containers with optional label filters
|
|
7
|
+
- `prune_images` — Remove unused Docker images (dangling and unreferenced) with optional filters
|
|
8
|
+
- `update_container` — Update container resource limits (CPU, memory, CPU shares)
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Improved tool descriptions for Glama Quality optimization (commit 255cf60)
|
|
12
|
+
- Added CI workflow (GitHub Actions, Node 18/20/22)
|
|
13
|
+
- Added `relatedServers` to glama.json
|
|
14
|
+
|
|
15
|
+
|
|
3
16
|
All notable changes to @supernova123/docker-mcp-server will be documented in this file.
|
|
4
17
|
|
|
18
|
+
## [0.3.4] - 2026-06-14
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **copy_from_container** tool — copy files from a container to the host filesystem
|
|
22
|
+
- **copy_to_container** tool — copy files from the host to a container filesystem
|
|
23
|
+
- Glama "Try it now" link in README for zero-install tool testing
|
|
24
|
+
|
|
25
|
+
## [0.3.3] - 2026-06-13
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **docker_info** tool — Docker daemon system information (version, OS, kernel, CPU, memory, storage driver, container/image counts)
|
|
29
|
+
- **disk_usage** tool — Disk usage breakdown by images, containers, volumes, and build cache with human-readable sizes
|
|
30
|
+
|
|
5
31
|
## [0.3.2] - 2026-06-13
|
|
6
32
|
|
|
7
33
|
### Added
|
|
@@ -95,4 +121,4 @@ All notable changes to @supernova123/docker-mcp-server will be documented in thi
|
|
|
95
121
|
## [0.1.0] - 2026-06-10
|
|
96
122
|
|
|
97
123
|
### Added
|
|
98
|
-
- Initial release: 25 tools across container, compose, exec, health, logs, image, and network modules
|
|
124
|
+
- Initial release: 25 tools across container, compose, exec, health, logs, image, and network modules
|
package/README.md
CHANGED
|
@@ -55,6 +55,8 @@ Here's what an agent actually does with this server during a deployment:
|
|
|
55
55
|
|
|
56
56
|
If the health check fails at step 2, your agent catches it immediately — no 3am alerts, no user complaints. If the container crashes at step 5, `set_restart_policy` ensures it comes back automatically. The agent doesn't just deploy containers — it keeps them running.
|
|
57
57
|
|
|
58
|
+
**[▶ Try it now on Glama](https://glama.ai/mcp/servers/friendlygeorge/docker-mcp-server)** — test all 31 tools in your browser, no install required.
|
|
59
|
+
|
|
58
60
|
## Quick Start
|
|
59
61
|
|
|
60
62
|
One command to run:
|
package/dist/server.js
CHANGED
|
@@ -9,10 +9,11 @@ import { registerNetworkTools } from "./tools/network.js";
|
|
|
9
9
|
import { registerVolumeTools } from "./tools/volume.js";
|
|
10
10
|
import { registerMonitoringTools } from "./tools/monitoring.js";
|
|
11
11
|
import { registerSystemTools } from "./tools/system.js";
|
|
12
|
+
import { registerTransferTools } from "./tools/transfer.js";
|
|
12
13
|
export function createServer(docker, options) {
|
|
13
14
|
const server = new McpServer({
|
|
14
15
|
name: "docker-mcp-server",
|
|
15
|
-
version: "0.3.
|
|
16
|
+
version: "0.3.4",
|
|
16
17
|
});
|
|
17
18
|
// Register all tool categories
|
|
18
19
|
registerContainerTools(server, docker);
|
|
@@ -25,6 +26,7 @@ export function createServer(docker, options) {
|
|
|
25
26
|
registerVolumeTools(server, docker);
|
|
26
27
|
registerMonitoringTools(server, docker);
|
|
27
28
|
registerSystemTools(server, docker);
|
|
29
|
+
registerTransferTools(server, docker);
|
|
28
30
|
return server;
|
|
29
31
|
}
|
|
30
32
|
//# sourceMappingURL=server.js.map
|
package/dist/tools/compose.js
CHANGED
|
@@ -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
|
|
41
|
+
server.tool("compose_up", "Bring up Docker Compose services from a docker-compose.yml file at path. Use compose_ps to check service states after bringing them up; use compose_logs to inspect output. Optionally rebuild images before starting (build=true). Returns a confirmation string listing which services were started. Idempotent: already-running services are left untouched. Returns an error string if the Compose file is missing or invalid.", ComposeUpSchema.shape, { idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
42
42
|
try {
|
|
43
43
|
const args = ["up", "-d"];
|
|
44
44
|
if (params.build)
|
|
@@ -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, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
69
|
+
server.tool("compose_ps", "List service states across a Docker Compose stack defined by docker-compose.yml at path. Returns an array of services with name, state (running, exited, etc.), health status, and port mappings. Use compose_up to start services; use compose_logs to inspect output. Read-only and safe to call repeatedly. Returns an error string if the Compose file is missing.", 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);
|
package/dist/tools/container.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ListContainersSchema, InspectContainerSchema, StartContainerSchema, StopContainerSchema, RestartContainerSchema, RemoveContainerSchema, RecreateContainerSchema, RunContainerSchema, } from "../types.js";
|
|
1
|
+
import { ListContainersSchema, InspectContainerSchema, StartContainerSchema, StopContainerSchema, RestartContainerSchema, RemoveContainerSchema, RecreateContainerSchema, RunContainerSchema, PruneContainersSchema, UpdateContainerSchema, } from "../types.js";
|
|
2
2
|
import { formatContainer, formatError, withRetry } from "../docker.js";
|
|
3
3
|
export function registerContainerTools(server, docker) {
|
|
4
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) => {
|
|
@@ -161,5 +161,90 @@ export function registerContainerTools(server, docker) {
|
|
|
161
161
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
162
162
|
}
|
|
163
163
|
});
|
|
164
|
+
// prune_containers — remove stopped containers
|
|
165
|
+
server.tool("prune_containers", "Remove all stopped Docker containers. Returns the number of containers removed and reclaimed disk space. This is a destructive operation — stopped containers and their non-persisted data will be deleted. Use list_containers first to see what will be removed. Useful for cleanup after deployments or when disk space is low.", PruneContainersSchema.shape, { readOnlyHint: false, idempotentHint: false, openWorldHint: false }, async (params) => {
|
|
166
|
+
try {
|
|
167
|
+
const filterObj = {};
|
|
168
|
+
if (params.filter) {
|
|
169
|
+
const parts = params.filter.split('=');
|
|
170
|
+
if (parts.length === 2) {
|
|
171
|
+
filterObj[parts[0]] = [parts[1]];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const result = await withRetry(() => docker.pruneContainers({ filters: filterObj }), { label: "prune_containers" });
|
|
175
|
+
return {
|
|
176
|
+
content: [{
|
|
177
|
+
type: "text",
|
|
178
|
+
text: JSON.stringify({
|
|
179
|
+
containers_deleted: (result.ContainersDeleted || []).length,
|
|
180
|
+
space_reclaimed: result.SpaceReclaimed || 0,
|
|
181
|
+
space_reclaimed_human: formatBytes(result.SpaceReclaimed || 0),
|
|
182
|
+
deleted_ids: (result.ContainersDeleted || []).map((id) => id.substring(0, 12)),
|
|
183
|
+
}, null, 2),
|
|
184
|
+
}],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
// update_container — update container resource limits
|
|
192
|
+
server.tool("update_container", "Update a Docker container's resource limits (CPU, memory, CPU shares). Requires the container to be stopped first. Returns the updated resource limits. Use this to right-size containers based on actual usage — set CPU limits to prevent runaway processes and memory limits to prevent OOM kills.", UpdateContainerSchema.shape, { readOnlyHint: false, idempotentHint: false, openWorldHint: false }, async (params) => {
|
|
193
|
+
try {
|
|
194
|
+
const updateConfig = {};
|
|
195
|
+
if (params.cpu_limit !== undefined) {
|
|
196
|
+
updateConfig.NanoCpus = Math.round(params.cpu_limit * 1e9);
|
|
197
|
+
}
|
|
198
|
+
if (params.memory_limit !== undefined) {
|
|
199
|
+
updateConfig.Memory = parseMemory(params.memory_limit);
|
|
200
|
+
}
|
|
201
|
+
if (params.cpu_shares !== undefined) {
|
|
202
|
+
updateConfig.CpuShares = params.cpu_shares;
|
|
203
|
+
}
|
|
204
|
+
if (Object.keys(updateConfig).length === 0) {
|
|
205
|
+
return { content: [{ type: "text", text: "Error: No resource limits specified. Provide at least one of: cpu_limit, memory_limit, cpu_shares." }], isError: true };
|
|
206
|
+
}
|
|
207
|
+
const container = docker.getContainer(params.container_id);
|
|
208
|
+
await withRetry(() => container.update(updateConfig), { label: "update_container" });
|
|
209
|
+
// Inspect to return current state
|
|
210
|
+
const info = await withRetry(() => container.inspect(), { label: "update_container_inspect" });
|
|
211
|
+
const hostConfig = info.HostConfig || {};
|
|
212
|
+
return {
|
|
213
|
+
content: [{
|
|
214
|
+
type: "text",
|
|
215
|
+
text: JSON.stringify({
|
|
216
|
+
container: params.container_id,
|
|
217
|
+
state: info.State?.Status,
|
|
218
|
+
resource_limits: {
|
|
219
|
+
cpu_limit_cores: hostConfig.NanoCpus ? hostConfig.NanoCpus / 1e9 : null,
|
|
220
|
+
memory_limit: hostConfig.Memory || null,
|
|
221
|
+
memory_limit_human: hostConfig.Memory ? formatBytes(hostConfig.Memory) : null,
|
|
222
|
+
cpu_shares: hostConfig.CpuShares || null,
|
|
223
|
+
},
|
|
224
|
+
}, null, 2),
|
|
225
|
+
}],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function formatBytes(bytes) {
|
|
234
|
+
if (bytes === 0)
|
|
235
|
+
return '0 B';
|
|
236
|
+
const k = 1024;
|
|
237
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
238
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
239
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
|
|
240
|
+
}
|
|
241
|
+
function parseMemory(mem) {
|
|
242
|
+
const match = mem.match(/^(\d+)(b|k|m|g|t)?$/i);
|
|
243
|
+
if (!match)
|
|
244
|
+
throw new Error(`Invalid memory format: ${mem}`);
|
|
245
|
+
const value = parseInt(match[1]);
|
|
246
|
+
const unit = (match[2] || 'b').toLowerCase();
|
|
247
|
+
const multipliers = { b: 1, k: 1024, m: 1024 ** 2, g: 1024 ** 3, t: 1024 ** 4 };
|
|
248
|
+
return value * (multipliers[unit] || 1);
|
|
164
249
|
}
|
|
165
250
|
//# sourceMappingURL=container.js.map
|
package/dist/tools/health.js
CHANGED
|
@@ -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, { idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
111
|
+
server.tool("set_restart_policy", "Change the restart policy of a running container without recreating it. Use restart_container for an immediate restart; use this tool to change the policy (always, unless-stopped, on-failure, no) for future restarts. Returns a confirmation string on success. Idempotent: setting the same policy is a no-op. Returns an error string if the container does not exist.", 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,4 +1,4 @@
|
|
|
1
|
-
import { ListImagesSchema, PullImageSchema, BuildImageSchema, RemoveImageSchema, } from "../types.js";
|
|
1
|
+
import { ListImagesSchema, PullImageSchema, BuildImageSchema, RemoveImageSchema, PruneImagesSchema, } from "../types.js";
|
|
2
2
|
import { formatImage, formatError, withRetry } from "../docker.js";
|
|
3
3
|
export function registerImageTools(server, docker) {
|
|
4
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) => {
|
|
@@ -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, { idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
17
|
+
server.tool("pull_image", "Pull a Docker image from a registry by image name (e.g. nginx:latest). Use list_images to see locally available images after pulling. Returns pull progress events as text. Idempotent: pulling an already-up-to-date image is a no-op. Returns an error string if the image does not exist on the registry or the pull fails.", 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);
|
|
@@ -63,5 +63,47 @@ export function registerImageTools(server, docker) {
|
|
|
63
63
|
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
64
64
|
}
|
|
65
65
|
});
|
|
66
|
+
// prune_images — remove unused Docker images
|
|
67
|
+
server.tool("prune_images", "Remove unused Docker images (dangling and unreferenced). Returns the number of images deleted and reclaimed disk space. Only removes images not used by any container. Use list_images first to see what will be removed. Useful for reclaiming disk space after builds or when switching base images frequently.", PruneImagesSchema.shape, { readOnlyHint: false, idempotentHint: false, openWorldHint: false }, async (params) => {
|
|
68
|
+
try {
|
|
69
|
+
const filterObj = {};
|
|
70
|
+
if (params.filter) {
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(params.filter);
|
|
73
|
+
Object.assign(filterObj, parsed);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// If not JSON, try key=value format
|
|
77
|
+
const parts = params.filter.split('=');
|
|
78
|
+
if (parts.length === 2) {
|
|
79
|
+
filterObj[parts[0]] = [parts[1]];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const result = await withRetry(() => docker.pruneImages({ filters: filterObj }), { label: "prune_images" });
|
|
84
|
+
return {
|
|
85
|
+
content: [{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: JSON.stringify({
|
|
88
|
+
images_deleted: (result.ImagesDeleted || []).length,
|
|
89
|
+
space_reclaimed: result.SpaceReclaimed || 0,
|
|
90
|
+
space_reclaimed_human: formatBytes(result.SpaceReclaimed || 0),
|
|
91
|
+
deleted_ids: (result.ImagesDeleted || []).map((img) => typeof img === 'string' ? img.substring(0, 19) : img.Deleted?.substring(0, 19) || 'unknown'),
|
|
92
|
+
}, null, 2),
|
|
93
|
+
}],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
return { content: [{ type: "text", text: `Error: ${formatError(error)}` }], isError: true };
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function formatBytes(bytes) {
|
|
102
|
+
if (bytes === 0)
|
|
103
|
+
return '0 B';
|
|
104
|
+
const k = 1024;
|
|
105
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
106
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
107
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
|
|
66
108
|
}
|
|
67
109
|
//# sourceMappingURL=image.js.map
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Readable } from "stream";
|
|
2
|
+
import { CopyFromContainerSchema, CopyToContainerSchema } from "../types.js";
|
|
3
|
+
import { formatError, withRetry } from "../docker.js";
|
|
4
|
+
/**
|
|
5
|
+
* Read a file from a container using docker exec (cat).
|
|
6
|
+
* Much simpler and more reliable than parsing getArchive tar streams.
|
|
7
|
+
*/
|
|
8
|
+
async function readFileViaExec(docker, containerId, filePath) {
|
|
9
|
+
const container = docker.getContainer(containerId);
|
|
10
|
+
// Create exec to cat the file
|
|
11
|
+
const exec = await container.exec({
|
|
12
|
+
Cmd: ["cat", filePath],
|
|
13
|
+
AttachStdout: true,
|
|
14
|
+
AttachStderr: true,
|
|
15
|
+
});
|
|
16
|
+
// Start exec and collect output
|
|
17
|
+
const stream = await exec.start({ Detach: false });
|
|
18
|
+
const chunks = [];
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
stream.on("data", (chunk) => {
|
|
21
|
+
// Docker exec streams have 8-byte headers per frame
|
|
22
|
+
// Skip the header bytes (first 8 bytes of each frame)
|
|
23
|
+
if (chunk.length > 8) {
|
|
24
|
+
chunks.push(chunk.slice(8));
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
stream.on("end", () => {
|
|
28
|
+
const content = Buffer.concat(chunks).toString("utf-8");
|
|
29
|
+
resolve({ content, size: content.length });
|
|
30
|
+
});
|
|
31
|
+
stream.on("error", reject);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get file metadata (size, permissions) via stat command.
|
|
36
|
+
*/
|
|
37
|
+
async function getFileStat(docker, containerId, filePath) {
|
|
38
|
+
const container = docker.getContainer(containerId);
|
|
39
|
+
const exec = await container.exec({
|
|
40
|
+
Cmd: ["stat", "-c", "%s %a %f", filePath],
|
|
41
|
+
AttachStdout: true,
|
|
42
|
+
AttachStderr: true,
|
|
43
|
+
});
|
|
44
|
+
const stream = await exec.start({ Detach: false });
|
|
45
|
+
const chunks = [];
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
stream.on("data", (chunk) => {
|
|
48
|
+
if (chunk.length > 8)
|
|
49
|
+
chunks.push(chunk.slice(8));
|
|
50
|
+
});
|
|
51
|
+
stream.on("end", () => {
|
|
52
|
+
const output = Buffer.concat(chunks).toString("utf-8").trim();
|
|
53
|
+
const [sizeStr, modeStr, typeStr] = output.split(" ");
|
|
54
|
+
const size = parseInt(sizeStr, 10) || 0;
|
|
55
|
+
const mode = modeStr || "644";
|
|
56
|
+
const isFile = typeStr?.startsWith("81") ?? true;
|
|
57
|
+
resolve({ size, mode: `0${mode}`, isFile });
|
|
58
|
+
});
|
|
59
|
+
stream.on("error", reject);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create a minimal tar archive buffer containing a single file.
|
|
64
|
+
* Used for putArchive to inject files into containers.
|
|
65
|
+
*/
|
|
66
|
+
function createSingleFileTar(filePath, content, mode) {
|
|
67
|
+
const contentBuffer = Buffer.from(content, "utf-8");
|
|
68
|
+
const contentBlocks = Math.ceil(contentBuffer.length / 512);
|
|
69
|
+
const totalSize = 512 + contentBlocks * 512;
|
|
70
|
+
const tar = Buffer.alloc(totalSize, 0);
|
|
71
|
+
// File name (100 bytes, null-terminated)
|
|
72
|
+
const nameBytes = Buffer.from(filePath, "utf-8");
|
|
73
|
+
nameBytes.copy(tar, 0, 0, Math.min(nameBytes.length, 100));
|
|
74
|
+
// File mode (8 bytes, octal, null-padded)
|
|
75
|
+
const modeStr = mode.toString(8).padStart(7, "0") + "\0";
|
|
76
|
+
Buffer.from(modeStr).copy(tar, 100);
|
|
77
|
+
// Owner ID (8 bytes) - 0
|
|
78
|
+
Buffer.from("0000000\0").copy(tar, 108);
|
|
79
|
+
// Group ID (8 bytes) - 0
|
|
80
|
+
Buffer.from("0000000\0").copy(tar, 116);
|
|
81
|
+
// File size (12 bytes, octal)
|
|
82
|
+
const sizeStr = contentBuffer.length.toString(8).padStart(11, "0") + "\0";
|
|
83
|
+
Buffer.from(sizeStr).copy(tar, 124);
|
|
84
|
+
// Modification time (12 bytes, octal)
|
|
85
|
+
const mtime = Math.floor(Date.now() / 1000);
|
|
86
|
+
const mtimeStr = mtime.toString(8).padStart(11, "0") + "\0";
|
|
87
|
+
Buffer.from(mtimeStr).copy(tar, 136);
|
|
88
|
+
// Type flag (1 byte) - '0' = regular file
|
|
89
|
+
tar[156] = 0x30; // '0'
|
|
90
|
+
// Checksum placeholder (8 bytes)
|
|
91
|
+
tar.fill(" ", 148, 156);
|
|
92
|
+
// Compute checksum
|
|
93
|
+
let checksum = 0;
|
|
94
|
+
for (let i = 0; i < 512; i++) {
|
|
95
|
+
checksum += tar[i];
|
|
96
|
+
}
|
|
97
|
+
const chkStr = checksum.toString(8).padStart(7, "0") + "\0";
|
|
98
|
+
Buffer.from(chkStr).copy(tar, 148);
|
|
99
|
+
// Copy content
|
|
100
|
+
contentBuffer.copy(tar, 512);
|
|
101
|
+
return tar;
|
|
102
|
+
}
|
|
103
|
+
export function registerTransferTools(server, docker) {
|
|
104
|
+
server.tool("copy_from_container", "Copy a file from a Docker container to read its contents. Returns the file content as text along with metadata (size, permissions). Useful for inspecting config files, logs, or application state inside running containers.", CopyFromContainerSchema.shape, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
105
|
+
try {
|
|
106
|
+
const { content, size } = await withRetry(() => readFileViaExec(docker, params.container_id, params.container_path), { label: "copy_from_container" });
|
|
107
|
+
// Get metadata (stat)
|
|
108
|
+
let mode = "0644";
|
|
109
|
+
try {
|
|
110
|
+
const stat = await getFileStat(docker, params.container_id, params.container_path);
|
|
111
|
+
mode = stat.mode;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// stat might fail if file doesn't exist, exec already validated it
|
|
115
|
+
}
|
|
116
|
+
const result = {
|
|
117
|
+
path: params.container_path,
|
|
118
|
+
content,
|
|
119
|
+
size,
|
|
120
|
+
mode,
|
|
121
|
+
truncated: content.length > 50000,
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text: `Error: ${formatError(error)}` }],
|
|
130
|
+
isError: true,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
server.tool("copy_to_container", "Write file content into a Docker container at the specified path. Overwrites existing files. Useful for injecting configuration files, scripts, or environment files into running or stopped containers.", CopyToContainerSchema.shape, { readOnlyHint: false, idempotentHint: false, openWorldHint: false }, async (params) => {
|
|
135
|
+
try {
|
|
136
|
+
const container = docker.getContainer(params.container_id);
|
|
137
|
+
const mode = params.mode ?? 0o644;
|
|
138
|
+
// Create tar archive with the file
|
|
139
|
+
const tarBuffer = createSingleFileTar(params.container_path, params.content, mode);
|
|
140
|
+
// putArchive expects the path to be the PARENT directory
|
|
141
|
+
const parts = params.container_path.split("/");
|
|
142
|
+
parts.pop(); // remove filename
|
|
143
|
+
const dirPath = parts.join("/") || "/";
|
|
144
|
+
const readable = Readable.from(tarBuffer);
|
|
145
|
+
// Use putArchive with promise API
|
|
146
|
+
await withRetry(() => new Promise((resolve, reject) => {
|
|
147
|
+
container
|
|
148
|
+
.putArchive(readable, { path: dirPath })
|
|
149
|
+
.then(() => resolve())
|
|
150
|
+
.catch(reject);
|
|
151
|
+
}), { label: "copy_to_container" });
|
|
152
|
+
return {
|
|
153
|
+
content: [
|
|
154
|
+
{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: JSON.stringify({
|
|
157
|
+
success: true,
|
|
158
|
+
path: params.container_path,
|
|
159
|
+
size: Buffer.byteLength(params.content, "utf-8"),
|
|
160
|
+
mode: `0${(mode & 0o777).toString(8)}`,
|
|
161
|
+
message: `File written to ${params.container_path} in container ${params.container_id}`,
|
|
162
|
+
}),
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
return {
|
|
169
|
+
content: [{ type: "text", text: `Error: ${formatError(error)}` }],
|
|
170
|
+
isError: true,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=transfer.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -346,6 +346,36 @@ export declare const PruneVolumesSchema: z.ZodObject<{
|
|
|
346
346
|
}, {
|
|
347
347
|
filter?: string | undefined;
|
|
348
348
|
}>;
|
|
349
|
+
export declare const PruneContainersSchema: z.ZodObject<{
|
|
350
|
+
filter: z.ZodOptional<z.ZodString>;
|
|
351
|
+
}, "strip", z.ZodTypeAny, {
|
|
352
|
+
filter?: string | undefined;
|
|
353
|
+
}, {
|
|
354
|
+
filter?: string | undefined;
|
|
355
|
+
}>;
|
|
356
|
+
export declare const PruneImagesSchema: z.ZodObject<{
|
|
357
|
+
filter: z.ZodOptional<z.ZodString>;
|
|
358
|
+
}, "strip", z.ZodTypeAny, {
|
|
359
|
+
filter?: string | undefined;
|
|
360
|
+
}, {
|
|
361
|
+
filter?: string | undefined;
|
|
362
|
+
}>;
|
|
363
|
+
export declare const UpdateContainerSchema: z.ZodObject<{
|
|
364
|
+
container_id: z.ZodString;
|
|
365
|
+
cpu_limit: z.ZodOptional<z.ZodNumber>;
|
|
366
|
+
memory_limit: z.ZodOptional<z.ZodString>;
|
|
367
|
+
cpu_shares: z.ZodOptional<z.ZodNumber>;
|
|
368
|
+
}, "strip", z.ZodTypeAny, {
|
|
369
|
+
container_id: string;
|
|
370
|
+
cpu_limit?: number | undefined;
|
|
371
|
+
memory_limit?: string | undefined;
|
|
372
|
+
cpu_shares?: number | undefined;
|
|
373
|
+
}, {
|
|
374
|
+
container_id: string;
|
|
375
|
+
cpu_limit?: number | undefined;
|
|
376
|
+
memory_limit?: string | undefined;
|
|
377
|
+
cpu_shares?: number | undefined;
|
|
378
|
+
}>;
|
|
349
379
|
export declare const ContainerHealthStatusSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
350
380
|
export declare const ContainerResourceUsageSchema: z.ZodObject<{
|
|
351
381
|
sort_by: z.ZodOptional<z.ZodEnum<["cpu", "memory", "network"]>>;
|
|
@@ -405,4 +435,30 @@ export declare const ResourceAlertCheckSchema: z.ZodObject<{
|
|
|
405
435
|
export declare const MonitorDashboardSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
406
436
|
export declare const DockerInfoSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
407
437
|
export declare const DiskUsageSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
438
|
+
export declare const CopyFromContainerSchema: z.ZodObject<{
|
|
439
|
+
container_id: z.ZodString;
|
|
440
|
+
container_path: z.ZodString;
|
|
441
|
+
}, "strip", z.ZodTypeAny, {
|
|
442
|
+
container_id: string;
|
|
443
|
+
container_path: string;
|
|
444
|
+
}, {
|
|
445
|
+
container_id: string;
|
|
446
|
+
container_path: string;
|
|
447
|
+
}>;
|
|
448
|
+
export declare const CopyToContainerSchema: z.ZodObject<{
|
|
449
|
+
container_id: z.ZodString;
|
|
450
|
+
container_path: z.ZodString;
|
|
451
|
+
content: z.ZodString;
|
|
452
|
+
mode: z.ZodOptional<z.ZodNumber>;
|
|
453
|
+
}, "strip", z.ZodTypeAny, {
|
|
454
|
+
container_id: string;
|
|
455
|
+
container_path: string;
|
|
456
|
+
content: string;
|
|
457
|
+
mode?: number | undefined;
|
|
458
|
+
}, {
|
|
459
|
+
container_id: string;
|
|
460
|
+
container_path: string;
|
|
461
|
+
content: string;
|
|
462
|
+
mode?: number | undefined;
|
|
463
|
+
}>;
|
|
408
464
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
CHANGED
|
@@ -149,6 +149,18 @@ export const RemoveVolumeSchema = z.object({
|
|
|
149
149
|
export const PruneVolumesSchema = z.object({
|
|
150
150
|
filter: z.string().optional().describe("Filter by label (e.g., 'label=key=value')"),
|
|
151
151
|
});
|
|
152
|
+
export const PruneContainersSchema = z.object({
|
|
153
|
+
filter: z.string().optional().describe("Filter by label (e.g., 'label=key=value')"),
|
|
154
|
+
});
|
|
155
|
+
export const PruneImagesSchema = z.object({
|
|
156
|
+
filter: z.string().optional().describe('Docker filters JSON (e.g. "dangling=true")'),
|
|
157
|
+
});
|
|
158
|
+
export const UpdateContainerSchema = z.object({
|
|
159
|
+
container_id: z.string().describe('Container ID or name'),
|
|
160
|
+
cpu_limit: z.number().optional().describe('CPU limit in cores (e.g. 1.5 for 1.5 CPUs)'),
|
|
161
|
+
memory_limit: z.string().optional().describe('Memory limit (e.g. "512m", "1g", "2048m")'),
|
|
162
|
+
cpu_shares: z.number().optional().describe('CPU shares (relative weight, 0-1024)'),
|
|
163
|
+
});
|
|
152
164
|
// Monitoring schemas (v0.2.0)
|
|
153
165
|
export const ContainerHealthStatusSchema = z.object({});
|
|
154
166
|
export const ContainerResourceUsageSchema = z.object({
|
|
@@ -176,4 +188,15 @@ export const MonitorDashboardSchema = z.object({});
|
|
|
176
188
|
// System info schemas (v0.3.3)
|
|
177
189
|
export const DockerInfoSchema = z.object({});
|
|
178
190
|
export const DiskUsageSchema = z.object({});
|
|
191
|
+
// File transfer schemas (v0.3.4)
|
|
192
|
+
export const CopyFromContainerSchema = z.object({
|
|
193
|
+
container_id: z.string().describe("Container ID or name"),
|
|
194
|
+
container_path: z.string().describe("Path inside container to copy from (e.g., '/etc/nginx/nginx.conf')"),
|
|
195
|
+
});
|
|
196
|
+
export const CopyToContainerSchema = z.object({
|
|
197
|
+
container_id: z.string().describe("Container ID or name"),
|
|
198
|
+
container_path: z.string().describe("Destination path inside container (e.g., '/app/config.json')"),
|
|
199
|
+
content: z.string().describe("File content to write (plain text)"),
|
|
200
|
+
mode: z.number().optional().describe("File permissions in octal (e.g., 0o644 = 420). Default: 0o644"),
|
|
201
|
+
});
|
|
179
202
|
//# sourceMappingURL=types.js.map
|
package/glama.json
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://glama.ai/schemas/glama.json",
|
|
3
|
-
"maintainers": [
|
|
3
|
+
"maintainers": [
|
|
4
|
+
"friendlygeorge"
|
|
5
|
+
],
|
|
4
6
|
"title": "Docker MCP Server",
|
|
5
|
-
"description": "31 tools for AI agent Docker management
|
|
6
|
-
"tags": [
|
|
7
|
+
"description": "31 tools for AI agent Docker management \u2014 container lifecycle, Compose stack operations, health checks, log streaming, and fleet monitoring through the Model Context Protocol.",
|
|
8
|
+
"tags": [
|
|
9
|
+
"docker",
|
|
10
|
+
"containers",
|
|
11
|
+
"mcp",
|
|
12
|
+
"compose",
|
|
13
|
+
"health-checks",
|
|
14
|
+
"monitoring",
|
|
15
|
+
"devops",
|
|
16
|
+
"ai-agents",
|
|
17
|
+
"typescript",
|
|
18
|
+
"self-healing"
|
|
19
|
+
],
|
|
7
20
|
"repository": "https://github.com/friendlygeorge/docker-mcp-server",
|
|
8
21
|
"license": "MIT",
|
|
9
|
-
"categories": [
|
|
22
|
+
"categories": [
|
|
23
|
+
"virtualization",
|
|
24
|
+
"developer-tools"
|
|
25
|
+
],
|
|
10
26
|
"language": "TypeScript",
|
|
11
|
-
"runtime": "node"
|
|
27
|
+
"runtime": "node",
|
|
28
|
+
"relatedServers": [
|
|
29
|
+
"https://github.com/friendlygeorge/resend-mcp-server",
|
|
30
|
+
"https://github.com/friendlygeorge/defillama-mcp-server",
|
|
31
|
+
"https://github.com/friendlygeorge/coingecko-mcp-server",
|
|
32
|
+
"https://github.com/friendlygeorge/etherscan-mcp-server",
|
|
33
|
+
"https://github.com/friendlygeorge/jobber-mcp-server"
|
|
34
|
+
]
|
|
12
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supernova123/docker-mcp-server",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
"start": "node dist/index.js",
|
|
14
14
|
"dev": "tsc && node dist/index.js",
|
|
15
15
|
"test": "vitest run",
|
|
16
|
-
"prepublishOnly": "npm run build"
|
|
16
|
+
"prepublishOnly": "npm run build",
|
|
17
|
+
"cf:dev": "wrangler dev src/cf-worker/index.ts",
|
|
18
|
+
"cf:deploy": "wrangler deploy src/cf-worker/index.ts",
|
|
19
|
+
"cf:typecheck": "tsc --noEmit src/cf-worker/*.ts"
|
|
17
20
|
},
|
|
18
21
|
"keywords": [
|
|
19
22
|
"mcp",
|
|
@@ -60,9 +63,11 @@
|
|
|
60
63
|
"zod": "^3.24.0"
|
|
61
64
|
},
|
|
62
65
|
"devDependencies": {
|
|
66
|
+
"@cloudflare/workers-types": "^4.20260613.1",
|
|
63
67
|
"@types/dockerode": "^3.3.31",
|
|
64
68
|
"@types/node": "^22.0.0",
|
|
65
69
|
"typescript": "^5.7.0",
|
|
66
|
-
"vitest": "^3.1.0"
|
|
70
|
+
"vitest": "^3.1.0",
|
|
71
|
+
"wrangler": "^4.100.0"
|
|
67
72
|
}
|
|
68
|
-
}
|
|
73
|
+
}
|