@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,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)
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { McpAgentDO } from "./mcp-agent.js";
|
|
2
|
+
import type { Env, ApiKeyRecord } from "./types.js";
|
|
3
|
+
|
|
4
|
+
// Re-export Durable Object for wrangler
|
|
5
|
+
export { McpAgentDO };
|
|
6
|
+
|
|
7
|
+
type McpAgentStub = DurableObjectStub<McpAgentDO>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Docker MCP Cloudflare Worker — Routing Layer
|
|
11
|
+
*
|
|
12
|
+
* Architecture:
|
|
13
|
+
* Client (Claude/Cursor/Agent)
|
|
14
|
+
* → Routing Worker (this file: auth, CORS, rate limiting)
|
|
15
|
+
* → McpAgentDO (per-user Durable Object: tool dispatch)
|
|
16
|
+
* → User's Docker daemon via Cloudflare Tunnel
|
|
17
|
+
*
|
|
18
|
+
* Free tier: read-only tools, 50 calls/day
|
|
19
|
+
* Standard tier ($19/mo): full access, 500 calls/day
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const CORS_HEADERS = {
|
|
23
|
+
"Access-Control-Allow-Origin": "*",
|
|
24
|
+
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
25
|
+
"Access-Control-Allow-Headers":
|
|
26
|
+
"Content-Type, Accept, mcp-session-id, mcp-protocol-version, Authorization",
|
|
27
|
+
"Access-Control-Expose-Headers": "mcp-session-id",
|
|
28
|
+
"Access-Control-Max-Age": "86400",
|
|
29
|
+
}
|
|
30
|
+
const LANDING_PAGE = `<!DOCTYPE html>
|
|
31
|
+
<html lang="en">
|
|
32
|
+
<head>
|
|
33
|
+
<meta charset="UTF-8">
|
|
34
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
35
|
+
<title>Docker MCP Hosted — Managed Docker Infrastructure for AI Agents</title>
|
|
36
|
+
<meta name="description" content="Run your AI agent's Docker tools in the cloud. Hosted MCP server with health checks, auto-restart, and per-user isolation. Free tier available.">
|
|
37
|
+
<style>
|
|
38
|
+
:root {
|
|
39
|
+
--bg: #0a0a0f;
|
|
40
|
+
--surface: #12121a;
|
|
41
|
+
--border: #1e1e2e;
|
|
42
|
+
--text: #e4e4e7;
|
|
43
|
+
--muted: #71717a;
|
|
44
|
+
--accent: #3b82f6;
|
|
45
|
+
--accent-hover: #2563eb;
|
|
46
|
+
--green: #22c55e;
|
|
47
|
+
--orange: #f59e0b;
|
|
48
|
+
}
|
|
49
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
50
|
+
body {
|
|
51
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
52
|
+
background: var(--bg);
|
|
53
|
+
color: var(--text);
|
|
54
|
+
line-height: 1.6;
|
|
55
|
+
}
|
|
56
|
+
.container { max-width: 960px; margin: 0 auto; padding: 0 24px; }
|
|
57
|
+
|
|
58
|
+
/* Nav */
|
|
59
|
+
nav {
|
|
60
|
+
border-bottom: 1px solid var(--border);
|
|
61
|
+
padding: 16px 0;
|
|
62
|
+
}
|
|
63
|
+
nav .container {
|
|
64
|
+
display: flex;
|
|
65
|
+
justify-content: space-between;
|
|
66
|
+
align-items: center;
|
|
67
|
+
}
|
|
68
|
+
.logo { font-size: 18px; font-weight: 700; color: var(--text); text-decoration: none; }
|
|
69
|
+
.logo span { color: var(--accent); }
|
|
70
|
+
nav a { color: var(--muted); text-decoration: none; font-size: 14px; }
|
|
71
|
+
nav a:hover { color: var(--text); }
|
|
72
|
+
|
|
73
|
+
/* Hero */
|
|
74
|
+
.hero {
|
|
75
|
+
padding: 80px 0 60px;
|
|
76
|
+
text-align: center;
|
|
77
|
+
}
|
|
78
|
+
.hero h1 {
|
|
79
|
+
font-size: 48px;
|
|
80
|
+
font-weight: 800;
|
|
81
|
+
line-height: 1.1;
|
|
82
|
+
margin-bottom: 20px;
|
|
83
|
+
letter-spacing: -0.02em;
|
|
84
|
+
}
|
|
85
|
+
.hero h1 span { color: var(--accent); }
|
|
86
|
+
.hero p {
|
|
87
|
+
font-size: 18px;
|
|
88
|
+
color: var(--muted);
|
|
89
|
+
max-width: 600px;
|
|
90
|
+
margin: 0 auto 32px;
|
|
91
|
+
}
|
|
92
|
+
.hero-buttons { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
|
|
93
|
+
.btn {
|
|
94
|
+
display: inline-block;
|
|
95
|
+
padding: 12px 28px;
|
|
96
|
+
border-radius: 8px;
|
|
97
|
+
font-size: 15px;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
text-decoration: none;
|
|
100
|
+
transition: all 0.15s;
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
border: none;
|
|
103
|
+
}
|
|
104
|
+
.btn-primary { background: var(--accent); color: white; }
|
|
105
|
+
.btn-primary:hover { background: var(--accent-hover); }
|
|
106
|
+
.btn-secondary { background: var(--surface); color: var(--text); border: 1px solid var(--border); }
|
|
107
|
+
.btn-secondary:hover { border-color: var(--accent); }
|
|
108
|
+
|
|
109
|
+
/* Install command */
|
|
110
|
+
.install {
|
|
111
|
+
background: var(--surface);
|
|
112
|
+
border: 1px solid var(--border);
|
|
113
|
+
border-radius: 8px;
|
|
114
|
+
padding: 16px 24px;
|
|
115
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
116
|
+
font-size: 14px;
|
|
117
|
+
color: var(--green);
|
|
118
|
+
margin-top: 24px;
|
|
119
|
+
display: inline-block;
|
|
120
|
+
}
|
|
121
|
+
.install .comment { color: var(--muted); }
|
|
122
|
+
|
|
123
|
+
/* Features */
|
|
124
|
+
.features {
|
|
125
|
+
padding: 60px 0;
|
|
126
|
+
border-top: 1px solid var(--border);
|
|
127
|
+
}
|
|
128
|
+
.features h2 {
|
|
129
|
+
text-align: center;
|
|
130
|
+
font-size: 28px;
|
|
131
|
+
margin-bottom: 48px;
|
|
132
|
+
}
|
|
133
|
+
.feature-grid {
|
|
134
|
+
display: grid;
|
|
135
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
136
|
+
gap: 24px;
|
|
137
|
+
}
|
|
138
|
+
.feature-card {
|
|
139
|
+
background: var(--surface);
|
|
140
|
+
border: 1px solid var(--border);
|
|
141
|
+
border-radius: 12px;
|
|
142
|
+
padding: 24px;
|
|
143
|
+
}
|
|
144
|
+
.feature-card h3 {
|
|
145
|
+
font-size: 16px;
|
|
146
|
+
margin-bottom: 8px;
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
gap: 8px;
|
|
150
|
+
}
|
|
151
|
+
.feature-card p { color: var(--muted); font-size: 14px; }
|
|
152
|
+
.icon { font-size: 20px; }
|
|
153
|
+
|
|
154
|
+
/* Pricing */
|
|
155
|
+
.pricing {
|
|
156
|
+
padding: 60px 0;
|
|
157
|
+
border-top: 1px solid var(--border);
|
|
158
|
+
}
|
|
159
|
+
.pricing h2 {
|
|
160
|
+
text-align: center;
|
|
161
|
+
font-size: 28px;
|
|
162
|
+
margin-bottom: 12px;
|
|
163
|
+
}
|
|
164
|
+
.pricing .subtitle {
|
|
165
|
+
text-align: center;
|
|
166
|
+
color: var(--muted);
|
|
167
|
+
margin-bottom: 48px;
|
|
168
|
+
}
|
|
169
|
+
.pricing-grid {
|
|
170
|
+
display: grid;
|
|
171
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
172
|
+
gap: 24px;
|
|
173
|
+
max-width: 700px;
|
|
174
|
+
margin: 0 auto;
|
|
175
|
+
}
|
|
176
|
+
.pricing-card {
|
|
177
|
+
background: var(--surface);
|
|
178
|
+
border: 1px solid var(--border);
|
|
179
|
+
border-radius: 12px;
|
|
180
|
+
padding: 32px;
|
|
181
|
+
}
|
|
182
|
+
.pricing-card.featured {
|
|
183
|
+
border-color: var(--accent);
|
|
184
|
+
position: relative;
|
|
185
|
+
}
|
|
186
|
+
.pricing-card.featured::before {
|
|
187
|
+
content: 'MOST POPULAR';
|
|
188
|
+
position: absolute;
|
|
189
|
+
top: -12px;
|
|
190
|
+
left: 50%;
|
|
191
|
+
transform: translateX(-50%);
|
|
192
|
+
background: var(--accent);
|
|
193
|
+
color: white;
|
|
194
|
+
font-size: 11px;
|
|
195
|
+
font-weight: 700;
|
|
196
|
+
padding: 4px 12px;
|
|
197
|
+
border-radius: 4px;
|
|
198
|
+
letter-spacing: 0.05em;
|
|
199
|
+
}
|
|
200
|
+
.pricing-card h3 { font-size: 20px; margin-bottom: 4px; }
|
|
201
|
+
.price { font-size: 36px; font-weight: 800; margin: 16px 0; }
|
|
202
|
+
.price span { font-size: 16px; font-weight: 400; color: var(--muted); }
|
|
203
|
+
.pricing-card ul { list-style: none; margin: 24px 0; }
|
|
204
|
+
.pricing-card li {
|
|
205
|
+
padding: 8px 0;
|
|
206
|
+
font-size: 14px;
|
|
207
|
+
color: var(--muted);
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
gap: 8px;
|
|
211
|
+
}
|
|
212
|
+
.pricing-card li::before { content: '✓'; color: var(--green); font-weight: 700; }
|
|
213
|
+
|
|
214
|
+
/* How it works */
|
|
215
|
+
.how {
|
|
216
|
+
padding: 60px 0;
|
|
217
|
+
border-top: 1px solid var(--border);
|
|
218
|
+
}
|
|
219
|
+
.how h2 {
|
|
220
|
+
text-align: center;
|
|
221
|
+
font-size: 28px;
|
|
222
|
+
margin-bottom: 48px;
|
|
223
|
+
}
|
|
224
|
+
.steps {
|
|
225
|
+
display: grid;
|
|
226
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
227
|
+
gap: 32px;
|
|
228
|
+
max-width: 800px;
|
|
229
|
+
margin: 0 auto;
|
|
230
|
+
}
|
|
231
|
+
.step { text-align: center; }
|
|
232
|
+
.step-num {
|
|
233
|
+
width: 40px;
|
|
234
|
+
height: 40px;
|
|
235
|
+
border-radius: 50%;
|
|
236
|
+
background: var(--accent);
|
|
237
|
+
color: white;
|
|
238
|
+
display: inline-flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
justify-content: center;
|
|
241
|
+
font-weight: 700;
|
|
242
|
+
font-size: 16px;
|
|
243
|
+
margin-bottom: 16px;
|
|
244
|
+
}
|
|
245
|
+
.step h3 { font-size: 16px; margin-bottom: 8px; }
|
|
246
|
+
.step p { color: var(--muted); font-size: 14px; }
|
|
247
|
+
|
|
248
|
+
/* FAQ */
|
|
249
|
+
.faq {
|
|
250
|
+
padding: 60px 0;
|
|
251
|
+
border-top: 1px solid var(--border);
|
|
252
|
+
}
|
|
253
|
+
.faq h2 {
|
|
254
|
+
text-align: center;
|
|
255
|
+
font-size: 28px;
|
|
256
|
+
margin-bottom: 48px;
|
|
257
|
+
}
|
|
258
|
+
.faq-item {
|
|
259
|
+
border-bottom: 1px solid var(--border);
|
|
260
|
+
padding: 20px 0;
|
|
261
|
+
}
|
|
262
|
+
.faq-item h3 { font-size: 16px; margin-bottom: 8px; }
|
|
263
|
+
.faq-item p { color: var(--muted); font-size: 14px; }
|
|
264
|
+
|
|
265
|
+
/* Footer */
|
|
266
|
+
footer {
|
|
267
|
+
border-top: 1px solid var(--border);
|
|
268
|
+
padding: 32px 0;
|
|
269
|
+
text-align: center;
|
|
270
|
+
color: var(--muted);
|
|
271
|
+
font-size: 13px;
|
|
272
|
+
}
|
|
273
|
+
footer a { color: var(--muted); text-decoration: none; }
|
|
274
|
+
footer a:hover { color: var(--text); }
|
|
275
|
+
</style>
|
|
276
|
+
</head>
|
|
277
|
+
<body>
|
|
278
|
+
<nav>
|
|
279
|
+
<div class="container">
|
|
280
|
+
<a href="/" class="logo">docker<span>mcp</span></a>
|
|
281
|
+
<div style="display:flex;gap:20px;">
|
|
282
|
+
<a href="#pricing">Pricing</a>
|
|
283
|
+
<a href="#how">Get Started</a>
|
|
284
|
+
<a href="https://github.com/friendlygeorge/docker-mcp-server">GitHub</a>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</nav>
|
|
288
|
+
|
|
289
|
+
<section class="hero">
|
|
290
|
+
<div class="container">
|
|
291
|
+
<h1>Your agent's Docker tools,<br><span>hosted in the cloud</span></h1>
|
|
292
|
+
<p>Stop managing MCP server deployments. Connect your AI agent to Docker via a managed endpoint — health checks, auto-restart, and per-user isolation included.</p>
|
|
293
|
+
<div class="hero-buttons">
|
|
294
|
+
<a href="#pricing" class="btn btn-primary">Start Free</a>
|
|
295
|
+
<a href="https://github.com/friendlygeorge/docker-mcp-server" class="btn btn-secondary">View on GitHub</a>
|
|
296
|
+
</div>
|
|
297
|
+
<div class="install">
|
|
298
|
+
<span class="comment"># Connect in one command:</span><br>
|
|
299
|
+
npx @supernova123/docker-mcp-server --hosted
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
</section>
|
|
303
|
+
|
|
304
|
+
<section class="features">
|
|
305
|
+
<div class="container">
|
|
306
|
+
<h2>Everything your agent needs to manage Docker</h2>
|
|
307
|
+
<div class="feature-grid">
|
|
308
|
+
<div class="feature-card">
|
|
309
|
+
<h3><span class="icon">🔍</span> Health Checks</h3>
|
|
310
|
+
<p>Continuous container health monitoring with configurable thresholds. Your agent knows when something is wrong before you do.</p>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="feature-card">
|
|
313
|
+
<h3><span class="icon">🔄</span> Auto-Restart</h3>
|
|
314
|
+
<p>Crashed containers restart automatically. Set restart policies per-container and let your agent handle the rest.</p>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="feature-card">
|
|
317
|
+
<h3><span class="icon">📊</span> Live Monitoring</h3>
|
|
318
|
+
<p>Real-time container stats, log streaming, and event watching. 39 tools covering the full Docker lifecycle.</p>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="feature-card">
|
|
321
|
+
<h3><span class="icon">🏗️</span> Compose Management</h3>
|
|
322
|
+
<p>Deploy, monitor, and manage multi-container applications. docker-compose up, down, logs, and restart — all via MCP.</p>
|
|
323
|
+
</div>
|
|
324
|
+
<div class="feature-card">
|
|
325
|
+
<h3><span class="icon">🔐</span> Per-User Isolation</h3>
|
|
326
|
+
<p>Each user gets their own Durable Object with isolated state and rate limits. No cross-tenant data leakage.</p>
|
|
327
|
+
</div>
|
|
328
|
+
<div class="feature-card">
|
|
329
|
+
<h3><span class="icon">⚡</span> Zero Config</h3>
|
|
330
|
+
<p>No servers to manage. No Docker daemon to expose. Just connect your agent and start working.</p>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
</section>
|
|
335
|
+
|
|
336
|
+
<section class="pricing" id="pricing">
|
|
337
|
+
<div class="container">
|
|
338
|
+
<h2>Simple, transparent pricing</h2>
|
|
339
|
+
<p class="subtitle">Start free. Upgrade when you need more.</p>
|
|
340
|
+
<div class="pricing-grid">
|
|
341
|
+
<div class="pricing-card">
|
|
342
|
+
<h3>Free</h3>
|
|
343
|
+
<div class="price">$0 <span>/month</span></div>
|
|
344
|
+
<ul>
|
|
345
|
+
<li>Read-only tools (inspect, list, logs)</li>
|
|
346
|
+
<li>50 requests/day</li>
|
|
347
|
+
<li>Community support</li>
|
|
348
|
+
<li>Single Docker host</li>
|
|
349
|
+
</ul>
|
|
350
|
+
<a href="#" class="btn btn-secondary" style="width:100%;text-align:center;">Get Started</a>
|
|
351
|
+
</div>
|
|
352
|
+
<div class="pricing-card featured">
|
|
353
|
+
<h3>Standard</h3>
|
|
354
|
+
<div class="price">$19 <span>/month</span></div>
|
|
355
|
+
<ul>
|
|
356
|
+
<li>All 39 Docker tools</li>
|
|
357
|
+
<li>500 requests/day</li>
|
|
358
|
+
<li>Health checks & auto-restart</li>
|
|
359
|
+
<li>Container exec & file transfer</li>
|
|
360
|
+
<li>Priority support</li>
|
|
361
|
+
</ul>
|
|
362
|
+
<a href="#" class="btn btn-primary" style="width:100%;text-align:center;">Subscribe</a>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
</section>
|
|
367
|
+
|
|
368
|
+
<section class="how" id="how">
|
|
369
|
+
<div class="container">
|
|
370
|
+
<h2>Up and running in 3 steps</h2>
|
|
371
|
+
<div class="steps">
|
|
372
|
+
<div class="step">
|
|
373
|
+
<div class="step-num">1</div>
|
|
374
|
+
<h3>Connect your Docker host</h3>
|
|
375
|
+
<p>Install Cloudflare Tunnel on your machine. One command exposes your Docker daemon securely.</p>
|
|
376
|
+
</div>
|
|
377
|
+
<div class="step">
|
|
378
|
+
<div class="step-num">2</div>
|
|
379
|
+
<h3>Get your API key</h3>
|
|
380
|
+
<p>Sign up and receive a unique API key. Free tier included, upgrade anytime.</p>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="step">
|
|
383
|
+
<div class="step-num">3</div>
|
|
384
|
+
<h3>Point your agent</h3>
|
|
385
|
+
<p>Add the hosted endpoint to your MCP client config. Your agent now has full Docker access.</p>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
</section>
|
|
390
|
+
|
|
391
|
+
<section class="faq">
|
|
392
|
+
<div class="container">
|
|
393
|
+
<h2>Frequently asked questions</h2>
|
|
394
|
+
<div class="faq-item">
|
|
395
|
+
<h3>How does my agent connect to my Docker host?</h3>
|
|
396
|
+
<p>Through Cloudflare Tunnel. You run a lightweight tunnel on your machine that securely proxies Docker API calls. No ports exposed, no firewall changes needed.</p>
|
|
397
|
+
</div>
|
|
398
|
+
<div class="faq-item">
|
|
399
|
+
<h3>Is my Docker data secure?</h3>
|
|
400
|
+
<p>Yes. Each user gets an isolated Durable Object on Cloudflare's edge. Your Docker daemon is only accessible through your authenticated tunnel. We never see your container data.</p>
|
|
401
|
+
</div>
|
|
402
|
+
<div class="faq-item">
|
|
403
|
+
<h3>What MCP clients are supported?</h3>
|
|
404
|
+
<p>Any client that supports the MCP protocol: Claude Desktop, Cursor, Windsurf, Continue, and more. Just point it at the hosted endpoint.</p>
|
|
405
|
+
</div>
|
|
406
|
+
<div class="faq-item">
|
|
407
|
+
<h3>Can I self-host instead?</h3>
|
|
408
|
+
<p>Absolutely. The server is open source on GitHub. The hosted version is for people who want zero-ops convenience.</p>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
</section>
|
|
412
|
+
|
|
413
|
+
<footer>
|
|
414
|
+
<div class="container">
|
|
415
|
+
<p>Built by <a href="https://github.com/friendlygeorge">Nova</a> · Open source under MIT · <a href="https://github.com/friendlygeorge/docker-mcp-server">GitHub</a></p>
|
|
416
|
+
</div>
|
|
417
|
+
</footer>
|
|
418
|
+
</body>
|
|
419
|
+
</html>`;
|
|
420
|
+
;
|
|
421
|
+
|
|
422
|
+
export default {
|
|
423
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
424
|
+
// Handle CORS preflight
|
|
425
|
+
if (request.method === "OPTIONS") {
|
|
426
|
+
return new Response(null, { headers: CORS_HEADERS });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const url = new URL(request.url);
|
|
430
|
+
|
|
431
|
+
// ── Health check ──────────────────────────────────────────
|
|
432
|
+
if (url.pathname === "/health") {
|
|
433
|
+
return new Response(
|
|
434
|
+
JSON.stringify({ status: "ok", version: "0.4.0" }),
|
|
435
|
+
{ headers: { "Content-Type": "application/json", ...CORS_HEADERS } }
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ── Landing page (no auth required) ──────────────────────
|
|
440
|
+
if (url.pathname === "/" && request.method === "GET") {
|
|
441
|
+
return new Response(LANDING_PAGE, {
|
|
442
|
+
headers: { "Content-Type": "text/html; charset=utf-8", ...CORS_HEADERS },
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ── Authentication ────────────────────────────────────────
|
|
447
|
+
const authHeader = request.headers.get("Authorization");
|
|
448
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
449
|
+
return new Response(
|
|
450
|
+
JSON.stringify({ error: "Missing or invalid Authorization header. Use: Bearer <api-key>" }),
|
|
451
|
+
{ status: 401, headers: { "Content-Type": "application/json", ...CORS_HEADERS } }
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const apiKey = authHeader.slice(7);
|
|
456
|
+
const keyRecord = await env.API_KEYS.get<ApiKeyRecord>(apiKey, "json");
|
|
457
|
+
|
|
458
|
+
if (!keyRecord || !keyRecord.active) {
|
|
459
|
+
return new Response(
|
|
460
|
+
JSON.stringify({ error: "Invalid or inactive API key" }),
|
|
461
|
+
{ status: 401, headers: { "Content-Type": "application/json", ...CORS_HEADERS } }
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ── Route to McpAgentDO ───────────────────────────────────
|
|
466
|
+
if (url.pathname === "/mcp" || url.pathname === "/mcp/") {
|
|
467
|
+
// Deterministic DO ID per user — same user always hits same DO
|
|
468
|
+
const doId = env.MCP_AGENT.idFromName(keyRecord.userId);
|
|
469
|
+
const stub = env.MCP_AGENT.get(doId) as McpAgentStub;
|
|
470
|
+
|
|
471
|
+
// Initialize the DO with user info (idempotent)
|
|
472
|
+
await stub.initialize(keyRecord.userId, keyRecord.tier);
|
|
473
|
+
|
|
474
|
+
// Forward the MCP request to the DO
|
|
475
|
+
const doResponse = await stub.handleMcpRequest(request);
|
|
476
|
+
|
|
477
|
+
// Add CORS headers to DO response
|
|
478
|
+
const response = new Response(doResponse.body, {
|
|
479
|
+
status: doResponse.status,
|
|
480
|
+
headers: { ...Object.fromEntries(doResponse.headers), ...CORS_HEADERS },
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
return response;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
// ── Tunnel URL registration ─────────────────────────────
|
|
488
|
+
if (url.pathname === "/tunnel" && request.method === "POST") {
|
|
489
|
+
const body = await request.json<{ tunnelUrl: string }>();
|
|
490
|
+
if (!body?.tunnelUrl) {
|
|
491
|
+
return new Response(
|
|
492
|
+
JSON.stringify({ error: "Missing tunnelUrl in request body" }),
|
|
493
|
+
{ status: 400, headers: { "Content-Type": "application/json", ...CORS_HEADERS } }
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const doId = env.MCP_AGENT.idFromName(keyRecord.userId);
|
|
498
|
+
const stub = env.MCP_AGENT.get(doId) as McpAgentStub;
|
|
499
|
+
await stub.initialize(keyRecord.userId, keyRecord.tier);
|
|
500
|
+
const result = await stub.setTunnelUrl(body.tunnelUrl);
|
|
501
|
+
|
|
502
|
+
if (!result.ok) {
|
|
503
|
+
return new Response(
|
|
504
|
+
JSON.stringify({ error: "Invalid tunnel URL. Must be a valid HTTPS URL." }),
|
|
505
|
+
{ status: 400, headers: { "Content-Type": "application/json", ...CORS_HEADERS } }
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return new Response(
|
|
510
|
+
JSON.stringify({ ok: true, tunnelUrl: result.tunnelUrl }),
|
|
511
|
+
{ headers: { "Content-Type": "application/json", ...CORS_HEADERS } }
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ── Get tunnel URL ──────────────────────────────────────
|
|
516
|
+
if (url.pathname === "/tunnel" && request.method === "GET") {
|
|
517
|
+
const doId = env.MCP_AGENT.idFromName(keyRecord.userId);
|
|
518
|
+
const stub = env.MCP_AGENT.get(doId) as McpAgentStub;
|
|
519
|
+
await stub.initialize(keyRecord.userId, keyRecord.tier);
|
|
520
|
+
const tunnelUrl = await stub.getTunnelUrl();
|
|
521
|
+
|
|
522
|
+
return new Response(
|
|
523
|
+
JSON.stringify({ tunnelUrl: tunnelUrl || null }),
|
|
524
|
+
{ headers: { "Content-Type": "application/json", ...CORS_HEADERS } }
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ── API key info (for debugging) ──────────────────────────
|
|
529
|
+
if (url.pathname === "/me") {
|
|
530
|
+
return new Response(
|
|
531
|
+
JSON.stringify({
|
|
532
|
+
userId: keyRecord.userId,
|
|
533
|
+
tier: keyRecord.tier,
|
|
534
|
+
active: keyRecord.active,
|
|
535
|
+
}),
|
|
536
|
+
{ headers: { "Content-Type": "application/json", ...CORS_HEADERS } }
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ── 404 ───────────────────────────────────────────────────
|
|
541
|
+
return new Response(
|
|
542
|
+
JSON.stringify({ error: "Not found. Use POST /mcp for MCP requests." }),
|
|
543
|
+
{ status: 404, headers: { "Content-Type": "application/json", ...CORS_HEADERS } }
|
|
544
|
+
);
|
|
545
|
+
},
|
|
546
|
+
};
|