@supernova123/docker-mcp-server 0.3.4 → 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 +21 -1
- package/README.md +2 -0
- package/dist/server.js +1 -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/types.d.ts +30 -0
- package/dist/types.js +12 -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 +1 -1
- 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/types.ts +16 -1
- 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,27 @@
|
|
|
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
|
+
|
|
5
25
|
## [0.3.3] - 2026-06-13
|
|
6
26
|
|
|
7
27
|
### Added
|
|
@@ -101,4 +121,4 @@ All notable changes to @supernova123/docker-mcp-server will be documented in thi
|
|
|
101
121
|
## [0.1.0] - 2026-06-10
|
|
102
122
|
|
|
103
123
|
### Added
|
|
104
|
-
- 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
|
@@ -13,7 +13,7 @@ import { registerTransferTools } from "./tools/transfer.js";
|
|
|
13
13
|
export function createServer(docker, options) {
|
|
14
14
|
const server = new McpServer({
|
|
15
15
|
name: "docker-mcp-server",
|
|
16
|
-
version: "0.3.
|
|
16
|
+
version: "0.3.4",
|
|
17
17
|
});
|
|
18
18
|
// Register all tool categories
|
|
19
19
|
registerContainerTools(server, docker);
|
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
|
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"]>>;
|
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({
|
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
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Docker MCP — Cloudflare Workers Hosted (MCPaaS)
|
|
2
|
+
|
|
3
|
+
Hosted version of Docker MCP server deployed on Cloudflare Workers. Users connect their Docker daemon via Cloudflare Tunnel; Nova runs the edge.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Client (Claude/Cursor/Agent)
|
|
9
|
+
→ Routing Worker (auth, CORS, rate limiting)
|
|
10
|
+
→ McpAgentDO (per-user Durable Object: tool dispatch)
|
|
11
|
+
→ User's Docker daemon via Cloudflare Tunnel
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Tiers
|
|
15
|
+
|
|
16
|
+
| Tier | Price | Tools | Rate Limit |
|
|
17
|
+
|------|-------|-------|------------|
|
|
18
|
+
| Free | $0 | Read-only (10 tools) | 50 calls/day |
|
|
19
|
+
| Standard | $19/mo | Full access (17 tools) | 500 calls/day |
|
|
20
|
+
|
|
21
|
+
## Setup (Deploy)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cd /home/nova/docker-mcp-server
|
|
25
|
+
|
|
26
|
+
# 1. Authenticate with Cloudflare
|
|
27
|
+
wrangler login
|
|
28
|
+
|
|
29
|
+
# 2. Create KV namespace for API keys
|
|
30
|
+
wrangler kv:namespace create API_KEYS
|
|
31
|
+
# Copy the namespace ID into wrangler.jsonc
|
|
32
|
+
|
|
33
|
+
# 3. Deploy
|
|
34
|
+
wrangler deploy
|
|
35
|
+
|
|
36
|
+
# 4. (Future) Add Stripe for billing
|
|
37
|
+
wrangler secret put STRIPE_KEY
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Setup (User)
|
|
41
|
+
|
|
42
|
+
Users run Docker MCP locally + expose their Docker daemon:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Install and run Docker MCP server
|
|
46
|
+
npx @supernova123/docker-mcp-server
|
|
47
|
+
|
|
48
|
+
# Expose Docker daemon via Cloudflare Tunnel
|
|
49
|
+
cloudflared tunnel --url http://localhost:2375
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then configure their MCP client with the hosted Worker URL + API key.
|
|
53
|
+
|
|
54
|
+
## Files
|
|
55
|
+
|
|
56
|
+
| File | Purpose |
|
|
57
|
+
|------|---------|
|
|
58
|
+
| `index.ts` | Routing Worker — auth via KV, CORS, DO routing |
|
|
59
|
+
| `mcp-agent.ts` | McpAgent Durable Object — 17 tools, per-user rate limits, tunnel proxy |
|
|
60
|
+
| `types.ts` | Type definitions: Env, ApiKeyRecord, UserState |
|
|
61
|
+
| `wrangler.jsonc` | Worker config: DO binding, KV binding, nodejs_compat |
|
|
62
|
+
|
|
63
|
+
## Tools (17)
|
|
64
|
+
|
|
65
|
+
`list_containers`, `inspect_container`, `start_container`, `stop_container`, `restart_container`, `remove_container`, `create_container`, `compose_up`, `compose_down`, `compose_ps`, `compose_logs`, `fleet_status`, `search_logs`, `watch_events`, `list_images`, `pull_image`, `list_networks`, `list_volumes`
|
|
66
|
+
|
|
67
|
+
## Status
|
|
68
|
+
|
|
69
|
+
- ✅ TypeScript compiles clean (both main and CF tsconfigs)
|
|
70
|
+
- ✅ wrangler deploy --dry-run passes (815 KiB bundle)
|
|
71
|
+
- ⏳ Needs Cloudflare account to deploy
|
|
72
|
+
- ⏳ KV namespace creation pending
|
|
73
|
+
- ⏳ Stripe/x402 integration for billing (Phase 4)
|