@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.
@@ -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.3",
16
+ version: "0.3.4",
17
17
  });
18
18
  // Register all tool categories
19
19
  registerContainerTools(server, docker);
@@ -38,7 +38,7 @@ function runCompose(path, args) {
38
38
  }
39
39
  }
40
40
  export function registerComposeTools(server) {
41
- server.tool("compose_up", "Bring up Docker Compose services from a docker-compose.yml file. Optionally build images first.", ComposeUpSchema.shape, { idempotentHint: true, openWorldHint: false }, async (params) => {
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);
@@ -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
@@ -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({
@@ -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": ["friendlygeorge"],
3
+ "maintainers": [
4
+ "friendlygeorge"
5
+ ],
4
6
  "title": "Docker MCP Server",
5
- "description": "31 tools for AI agent Docker management container lifecycle, Compose stack operations, health checks, log streaming, and fleet monitoring through the Model Context Protocol.",
6
- "tags": ["docker", "containers", "mcp", "compose", "health-checks", "monitoring", "devops", "ai-agents", "typescript", "self-healing"],
7
+ "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": ["virtualization", "developer-tools"],
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.4",
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)