alchemax-plugin 0.1.0__tar.gz
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.
- alchemax_plugin-0.1.0/.gitignore +43 -0
- alchemax_plugin-0.1.0/Dockerfile +18 -0
- alchemax_plugin-0.1.0/PKG-INFO +10 -0
- alchemax_plugin-0.1.0/README.md +544 -0
- alchemax_plugin-0.1.0/alchemax_plugin/__init__.py +3 -0
- alchemax_plugin-0.1.0/alchemax_plugin/analyzer.py +83 -0
- alchemax_plugin-0.1.0/alchemax_plugin/builder.py +211 -0
- alchemax_plugin-0.1.0/alchemax_plugin/metadata.py +170 -0
- alchemax_plugin-0.1.0/alchemax_plugin/runner.py +86 -0
- alchemax_plugin-0.1.0/alchemax_plugin/server.py +713 -0
- alchemax_plugin-0.1.0/alchemax_plugin/terraform/docker-compose.yml.tpl +23 -0
- alchemax_plugin-0.1.0/alchemax_plugin/terraform/ebs.tf +43 -0
- alchemax_plugin-0.1.0/alchemax_plugin/terraform/ec2.tf +102 -0
- alchemax_plugin-0.1.0/alchemax_plugin/terraform/init.sh +138 -0
- alchemax_plugin-0.1.0/alchemax_plugin/terraform/main.tf +42 -0
- alchemax_plugin-0.1.0/alchemax_plugin/terraform/network.tf +98 -0
- alchemax_plugin-0.1.0/alchemax_plugin/terraform/nginx.conf.tpl +103 -0
- alchemax_plugin-0.1.0/alchemax_plugin/terraform/outputs.tf +41 -0
- alchemax_plugin-0.1.0/alchemax_plugin/terraform/variables.tf +46 -0
- alchemax_plugin-0.1.0/alchemax_plugin/tfstate.py +37 -0
- alchemax_plugin-0.1.0/alchemax_plugin/workspace.py +66 -0
- alchemax_plugin-0.1.0/api.py +79 -0
- alchemax_plugin-0.1.0/opencode.json +11 -0
- alchemax_plugin-0.1.0/pyproject.toml +30 -0
- alchemax_plugin-0.1.0/tests/__init__.py +1 -0
- alchemax_plugin-0.1.0/tests/test_analyzer.py +53 -0
- alchemax_plugin-0.1.0/tests/test_builder.py +65 -0
- alchemax_plugin-0.1.0/tests/test_metadata.py +177 -0
- alchemax_plugin-0.1.0/tests/test_workspace.py +95 -0
- alchemax_plugin-0.1.0/uv.lock +1143 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Terraform
|
|
2
|
+
*.tfstate*
|
|
3
|
+
.terraform
|
|
4
|
+
.terraform.lock.hcl
|
|
5
|
+
terraform.tfvars.json
|
|
6
|
+
*.pem
|
|
7
|
+
|
|
8
|
+
# Python
|
|
9
|
+
__pycache__
|
|
10
|
+
*.py[cod]
|
|
11
|
+
*$py.class
|
|
12
|
+
*.so
|
|
13
|
+
.Python
|
|
14
|
+
build
|
|
15
|
+
develop-eggs
|
|
16
|
+
dist
|
|
17
|
+
downloads
|
|
18
|
+
eggs
|
|
19
|
+
.eggs
|
|
20
|
+
lib
|
|
21
|
+
lib64
|
|
22
|
+
parts
|
|
23
|
+
sdist
|
|
24
|
+
var
|
|
25
|
+
wheels
|
|
26
|
+
*.egg-info
|
|
27
|
+
.installed.cfg
|
|
28
|
+
*.egg
|
|
29
|
+
.pytest_cache
|
|
30
|
+
.venv
|
|
31
|
+
venv
|
|
32
|
+
|
|
33
|
+
# IDE
|
|
34
|
+
.vscode
|
|
35
|
+
.idea
|
|
36
|
+
*.swp
|
|
37
|
+
*.swo
|
|
38
|
+
|
|
39
|
+
# OS
|
|
40
|
+
.DS_Store
|
|
41
|
+
.env
|
|
42
|
+
.env.local
|
|
43
|
+
*.tar.gz
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
FROM python:3.11-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Copy project files
|
|
6
|
+
COPY . .
|
|
7
|
+
|
|
8
|
+
# Install dependencies
|
|
9
|
+
RUN pip install --no-cache-dir -e .
|
|
10
|
+
|
|
11
|
+
# Install FastAPI and uvicorn
|
|
12
|
+
RUN pip install --no-cache-dir fastapi uvicorn[standard]
|
|
13
|
+
|
|
14
|
+
# Expose port
|
|
15
|
+
EXPOSE 8000
|
|
16
|
+
|
|
17
|
+
# Run API server
|
|
18
|
+
CMD ["python", "api.py"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: alchemax-plugin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Standalone MCP plugin for deploying Docker apps to AWS EC2
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: boto3>=1.34
|
|
7
|
+
Requires-Dist: fastapi>=0.115
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: mcp>=1.0
|
|
10
|
+
Requires-Dist: uvicorn[standard]>=0.30
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
# Alchemax Plugin
|
|
2
|
+
|
|
3
|
+
**Standalone Python MCP toolkit for deploying Docker applications to AWS EC2.**
|
|
4
|
+
|
|
5
|
+
A lightweight, infrastructure-as-code plugin that provisions a single EC2 instance per workspace and manages multi-app deployments via docker-compose + Terraform. Zero SaaS, zero dashboard—infrastructure lives in your AWS account.
|
|
6
|
+
|
|
7
|
+
**Use cases:**
|
|
8
|
+
- Deploy multiple Docker services (API, worker, scheduler) to one EC2
|
|
9
|
+
- One-command deploys with dependency ordering
|
|
10
|
+
- Infrastructure as code (Terraform) with optional app manifests (alchemax.json)
|
|
11
|
+
- Local state, no remote backend required
|
|
12
|
+
- API logging & health checks via SSM (no SSH needed)
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### Claude Code
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# No install needed — uvx runs it on demand
|
|
20
|
+
claude mcp add alchemax-plugin -- uvx alchemax-plugin
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or install permanently first:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv tool install alchemax-plugin
|
|
27
|
+
claude mcp add alchemax-plugin -- alchemax-plugin
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Opencode
|
|
31
|
+
|
|
32
|
+
No install needed — use `uvx` directly in your `opencode.json`:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"mcpServers": {
|
|
37
|
+
"alchemax-plugin": {
|
|
38
|
+
"command": "uvx",
|
|
39
|
+
"args": ["alchemax-plugin"]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or install permanently:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv tool install alchemax-plugin
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then add to your project's `opencode.json`:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"alchemax-plugin": {
|
|
57
|
+
"command": "alchemax-plugin"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or run directly without installing:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"alchemax-plugin": {
|
|
69
|
+
"command": "uvx",
|
|
70
|
+
"args": ["alchemax-plugin"]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Manual (any MCP client)
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install alchemax-plugin
|
|
80
|
+
# then point your MCP client at: alchemax-plugin
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Setup
|
|
84
|
+
|
|
85
|
+
### 1. AWS Credentials
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
aws configure
|
|
89
|
+
# or use AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY env vars
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 2. Docker Hub (for building images)
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
export DOCKER_HUB_USERNAME=your-username
|
|
96
|
+
docker login
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 3. Initialize Workspace
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
alchemax-plugin setup prod us-east-1
|
|
103
|
+
# Creates ~/.alp/prod/terraform/ with all infrastructure code
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Quick Start Examples
|
|
107
|
+
|
|
108
|
+
**Single app from Docker image:**
|
|
109
|
+
```bash
|
|
110
|
+
alchemax-plugin deploy_app prod myapp docker.io/user/myapp:v1.0.0 8000
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Single app from source (auto-build):**
|
|
114
|
+
```bash
|
|
115
|
+
alchemax-plugin deploy_from_source prod myapp /path/to/source 8000
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Multi-app project with manifest:**
|
|
119
|
+
```bash
|
|
120
|
+
# In your project root, create alchemax.json (see below)
|
|
121
|
+
alchemax-plugin deploy_all prod /path/to/project
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**List all workspaces:**
|
|
125
|
+
```bash
|
|
126
|
+
alchemax-plugin list_workspaces
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**View instance status & logs:**
|
|
130
|
+
```bash
|
|
131
|
+
alchemax-plugin get_status prod
|
|
132
|
+
alchemax-plugin get_logs prod backend 50
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Tear down:**
|
|
136
|
+
```bash
|
|
137
|
+
alchemax-plugin destroy prod
|
|
138
|
+
# EBS volume has prevent_destroy; see gotchas below for removal
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Project Manifest (alchemax.json)
|
|
142
|
+
|
|
143
|
+
Multi-app projects define an optional `alchemax.json` in the project root. Without it, use `deploy_app()` / `deploy_from_source()` for single apps.
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"name": "cloud-suite",
|
|
148
|
+
"workspace": {
|
|
149
|
+
"region": "us-east-1",
|
|
150
|
+
"instance_type": "t3.micro",
|
|
151
|
+
"ebs_volume_size": 10,
|
|
152
|
+
"env": {
|
|
153
|
+
"LOG_LEVEL": "info",
|
|
154
|
+
"DB_HOST": "localhost"
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"apps": [
|
|
158
|
+
{
|
|
159
|
+
"id": "backend",
|
|
160
|
+
"port": 8000,
|
|
161
|
+
"path": "./api",
|
|
162
|
+
"dockerfile": "Dockerfile",
|
|
163
|
+
"tag": "v1.0.0",
|
|
164
|
+
"health_check": "/api/health",
|
|
165
|
+
"env": {
|
|
166
|
+
"DB_PATH": "/data"
|
|
167
|
+
},
|
|
168
|
+
"secrets": ["DATABASE_URL", "API_KEY", "JWT_SECRET"],
|
|
169
|
+
"depends_on": []
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"id": "worker",
|
|
173
|
+
"port": 8001,
|
|
174
|
+
"path": "./worker",
|
|
175
|
+
"tag": "v1.0.0",
|
|
176
|
+
"health_check": "/health",
|
|
177
|
+
"depends_on": ["backend"]
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Schema
|
|
184
|
+
|
|
185
|
+
- **`workspace.env`** — committed env vars (LOG_LEVEL, DB_HOST, etc.). Safe to commit.
|
|
186
|
+
- **`apps[].env`** — per-app committed vars. Merged with workspace.env at deploy.
|
|
187
|
+
- **`apps[].secrets`** — list of secret names. Values pulled from:
|
|
188
|
+
1. Process environment (`export DATABASE_URL=...`)
|
|
189
|
+
2. `.env.local` file in project root (git-ignored)
|
|
190
|
+
3. AWS SSM Parameter Store (future)
|
|
191
|
+
|
|
192
|
+
**Never commit secret values to alchemax.json.**
|
|
193
|
+
|
|
194
|
+
- **`apps[].tag`** — Docker image tag. Not `:latest` (enables rollback + version tracking).
|
|
195
|
+
- **`apps[].depends_on`** — array of app IDs. Deployment order enforced + passed to docker-compose.
|
|
196
|
+
- **`apps[].health_check`** — optional HTTP endpoint for readiness checks (e.g., `/api/health`).
|
|
197
|
+
|
|
198
|
+
### Secrets Workflow
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# 1. Define secret names in manifest
|
|
202
|
+
# "secrets": ["DATABASE_URL", "API_KEY"]
|
|
203
|
+
|
|
204
|
+
# 2. Set values locally
|
|
205
|
+
export DATABASE_URL="postgres://localhost/db"
|
|
206
|
+
export API_KEY="secret-value"
|
|
207
|
+
|
|
208
|
+
# 3. Or write .env.local (git-ignored)
|
|
209
|
+
echo "DATABASE_URL=postgres://localhost/db" > .env.local
|
|
210
|
+
echo "API_KEY=secret-value" >> .env.local
|
|
211
|
+
|
|
212
|
+
# 4. Deploy — secrets injected into containers
|
|
213
|
+
alchemax-plugin deploy_all prod /path/to/project
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Architecture
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
220
|
+
│ Your Project (git repo) │
|
|
221
|
+
│ ├── api/ (FastAPI, Express, Django, etc.) │
|
|
222
|
+
│ ├── worker/ (Python, Node, Go worker) │
|
|
223
|
+
│ ├── alchemax.json (optional multi-app manifest) │
|
|
224
|
+
│ └── .env.local (git-ignored, secrets) │
|
|
225
|
+
└──────────┬──────────────────────────────────────────────────────┘
|
|
226
|
+
│ alchemax-plugin CLI or MCP
|
|
227
|
+
▼
|
|
228
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
229
|
+
│ ~/.alp/<workspace>/ │
|
|
230
|
+
│ ├── terraform/ (Terraform state + .tf files) │
|
|
231
|
+
│ ├── terraform.tfvars.json (generated from apps.json) │
|
|
232
|
+
│ └── apps.json (deployed app list + metadata) │
|
|
233
|
+
└──────────┬──────────────────────────────────────────────────────┘
|
|
234
|
+
│ Terraform Apply
|
|
235
|
+
▼
|
|
236
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
237
|
+
│ AWS Account │
|
|
238
|
+
│ ├── EC2 Instance (t3.micro, Ubuntu 22.04) │
|
|
239
|
+
│ │ ├── Docker Daemon │
|
|
240
|
+
│ │ │ ├── Container: backend (port 8000) │
|
|
241
|
+
│ │ │ ├── Container: worker (port 8001) │
|
|
242
|
+
│ │ │ └── ... │
|
|
243
|
+
│ │ ├── nginx reverse proxy (:80, :443) │
|
|
244
|
+
│ │ └── /data/ (EBS volume, prevent_destroy) │
|
|
245
|
+
│ ├── VPC + Security Group (allow 22, 80, 443, app ports) │
|
|
246
|
+
│ ├── EIP (elastic IP for stable public IP) │
|
|
247
|
+
│ ├── IAM Role (SSM access for get_logs) │
|
|
248
|
+
│ └── Route53 (optional custom domain + TLS) │
|
|
249
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
One EC2 per workspace. All apps on the same instance via docker-compose.
|
|
253
|
+
|
|
254
|
+
## Tools Reference
|
|
255
|
+
|
|
256
|
+
### Discovery
|
|
257
|
+
|
|
258
|
+
**`list_workspaces()`**
|
|
259
|
+
List all workspaces with live AWS EC2 data (state, public IP, app count).
|
|
260
|
+
```bash
|
|
261
|
+
alchemax-plugin list_workspaces
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**`get_project_info(project_path)`**
|
|
265
|
+
Read alchemax.json and return app metadata + deployment order.
|
|
266
|
+
```bash
|
|
267
|
+
alchemax-plugin get_project_info /path/to/project
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Setup
|
|
271
|
+
|
|
272
|
+
**`setup(workspace, region?)`**
|
|
273
|
+
Initialize workspace. Creates ~/.alp/<workspace>/terraform/, copies .tf files, runs `terraform init`.
|
|
274
|
+
```bash
|
|
275
|
+
alchemax-plugin setup prod us-east-1
|
|
276
|
+
alchemax-plugin setup staging eu-west-1
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Deployment
|
|
280
|
+
|
|
281
|
+
**`deploy_app(workspace, name, image, port, env?)`**
|
|
282
|
+
Deploy a Docker image to the workspace.
|
|
283
|
+
```bash
|
|
284
|
+
alchemax-plugin deploy_app prod api docker.io/user/myapi:v1.0.0 8000 \
|
|
285
|
+
'{"LOG_LEVEL": "info"}'
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**`deploy_from_source(workspace, name, source_path, port?, env?)`**
|
|
289
|
+
Build Docker image from source (auto-detects language), push to Docker Hub, deploy.
|
|
290
|
+
```bash
|
|
291
|
+
alchemax-plugin deploy_from_source prod backend /path/to/api 8000 \
|
|
292
|
+
'{"DEBUG": "false"}'
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**`deploy_all(workspace, project_path, docker_hub_user?)`**
|
|
296
|
+
Deploy all apps from alchemax.json in dependency order. Stops on first failure.
|
|
297
|
+
```bash
|
|
298
|
+
alchemax-plugin deploy_all prod /path/to/project
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Status & Logs
|
|
302
|
+
|
|
303
|
+
**`get_status(workspace)`**
|
|
304
|
+
Check EC2 instance state (running/stopped/not-found), public IP, uptime, app count.
|
|
305
|
+
```bash
|
|
306
|
+
alchemax-plugin get_status prod
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**`get_logs(workspace, app_name, lines?)`**
|
|
310
|
+
Fetch container logs via AWS SSM SendCommand (no SSH required).
|
|
311
|
+
```bash
|
|
312
|
+
alchemax-plugin get_logs prod backend 100
|
|
313
|
+
alchemax-plugin get_logs prod worker 50
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**`list_apps(workspace)`**
|
|
317
|
+
List deployed apps in a workspace.
|
|
318
|
+
```bash
|
|
319
|
+
alchemax-plugin list_apps prod
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Management
|
|
323
|
+
|
|
324
|
+
**`remove_app(workspace, name)`**
|
|
325
|
+
Remove an app from the workspace and re-apply Terraform.
|
|
326
|
+
```bash
|
|
327
|
+
alchemax-plugin remove_app prod worker
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**`configure_domain(workspace, domain)`**
|
|
331
|
+
Point custom domain at the workspace EC2 instance (Route 53 + certbot TLS).
|
|
332
|
+
```bash
|
|
333
|
+
alchemax-plugin configure_domain prod myapp.example.com
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**`destroy(workspace)`**
|
|
337
|
+
Tear down all infrastructure (EC2, VPC, EBS, IAM). **EBS volume has `prevent_destroy` to guard against data loss.**
|
|
338
|
+
```bash
|
|
339
|
+
alchemax-plugin destroy prod
|
|
340
|
+
# If destroy fails due to prevent_destroy:
|
|
341
|
+
# terraform state rm aws_ebs_volume.data
|
|
342
|
+
# alchemax-plugin destroy prod
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## IAM Policy
|
|
346
|
+
|
|
347
|
+
Attach this policy to your AWS user. Minimal permissions for alchemax-plugin:
|
|
348
|
+
|
|
349
|
+
```json
|
|
350
|
+
{
|
|
351
|
+
"Version": "2012-10-17",
|
|
352
|
+
"Statement": [
|
|
353
|
+
{
|
|
354
|
+
"Effect": "Allow",
|
|
355
|
+
"Action": [
|
|
356
|
+
"ec2:*",
|
|
357
|
+
"iam:CreateRole",
|
|
358
|
+
"iam:CreateInstanceProfile",
|
|
359
|
+
"iam:AttachRolePolicy",
|
|
360
|
+
"iam:AddRoleToInstanceProfile",
|
|
361
|
+
"iam:GetRole",
|
|
362
|
+
"iam:GetInstanceProfile",
|
|
363
|
+
"iam:DeleteRole",
|
|
364
|
+
"iam:DeleteInstanceProfile",
|
|
365
|
+
"iam:RemoveRoleFromInstanceProfile",
|
|
366
|
+
"iam:DetachRolePolicy",
|
|
367
|
+
"iam:ListInstanceProfiles"
|
|
368
|
+
],
|
|
369
|
+
"Resource": "*"
|
|
370
|
+
}
|
|
371
|
+
]
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## MCP Integration
|
|
376
|
+
|
|
377
|
+
Use as an MCP server in Claude Desktop or other MCP clients:
|
|
378
|
+
|
|
379
|
+
```json
|
|
380
|
+
// ~/.claude/mcp_servers.json
|
|
381
|
+
{
|
|
382
|
+
"alchemax-plugin": {
|
|
383
|
+
"command": "python",
|
|
384
|
+
"args": ["-m", "alchemax_plugin.server"]
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Then call tools via MCP:
|
|
390
|
+
```
|
|
391
|
+
Claude: deploy_all workspace=prod project_path=/path/to/project
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## State & Data Persistence
|
|
395
|
+
|
|
396
|
+
**State:**
|
|
397
|
+
- `~/.alp/<workspace>/terraform/` — Terraform state (local, not remote)
|
|
398
|
+
- `~/.alp/<workspace>/apps.json` — deployed app list + metadata
|
|
399
|
+
- `~/.alp/<workspace>/terraform.tfvars.json` — generated from apps.json
|
|
400
|
+
|
|
401
|
+
**Data:**
|
|
402
|
+
- `/data/<app-name>/` — EBS volume mounted in each container
|
|
403
|
+
- Persists across container restarts and even workspace teardown (prevent_destroy)
|
|
404
|
+
- To delete data: manually delete EBS volume in AWS console or via CLI
|
|
405
|
+
|
|
406
|
+
## Troubleshooting
|
|
407
|
+
|
|
408
|
+
### "Docker buildx: permission denied"
|
|
409
|
+
```bash
|
|
410
|
+
docker logout
|
|
411
|
+
docker login
|
|
412
|
+
# Re-run deploy
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### "Terraform state is locked"
|
|
416
|
+
```bash
|
|
417
|
+
# Find the lock table name in ~/.alp/<workspace>/terraform/
|
|
418
|
+
aws dynamodb delete-item \
|
|
419
|
+
--table-name alchemax-tflock-xxxxx \
|
|
420
|
+
--key '{"LockID": {"S": "..."}}'
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### "EBS volume has prevent_destroy"
|
|
424
|
+
```bash
|
|
425
|
+
cd ~/.alp/<workspace>/terraform/
|
|
426
|
+
terraform state rm aws_ebs_volume.data
|
|
427
|
+
terraform destroy # or use CLI: alchemax-plugin destroy <workspace>
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Instance not responding / containers failing
|
|
431
|
+
```bash
|
|
432
|
+
alchemax-plugin get_logs prod backend 100
|
|
433
|
+
alchemax-plugin get_status prod # check EC2 state
|
|
434
|
+
# SSH if needed:
|
|
435
|
+
ssh -i ~/.alp/<workspace>/terraform/alp-key.pem ubuntu@<public-ip>
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### App not reachable at public IP
|
|
439
|
+
1. Check instance is running: `get_status`
|
|
440
|
+
2. Check nginx routing: `get_logs prod` (look for 404 errors)
|
|
441
|
+
3. Verify port mapping in `list_apps`
|
|
442
|
+
4. Check docker-compose: SSH to instance, run `docker ps`
|
|
443
|
+
|
|
444
|
+
### Env vars not set in container
|
|
445
|
+
```bash
|
|
446
|
+
# Verify in deployed apps.json
|
|
447
|
+
cat ~/.alp/<workspace>/apps.json
|
|
448
|
+
# If missing, re-deploy:
|
|
449
|
+
alchemax-plugin deploy_app <workspace> <app> <image> <port> '<env-json>'
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## Known Limitations
|
|
453
|
+
|
|
454
|
+
- **Docker-only** — no Lambda, Cloud Run, or other runtimes. Cross-provider orchestration belongs in your orchestrator repo.
|
|
455
|
+
- **Single EC2 per workspace** — all apps share the same instance (cost-efficient for small teams, not multi-tenant).
|
|
456
|
+
- **No built-in monitoring** — hook it up to your own CloudWatch / Prometheus.
|
|
457
|
+
- **`:latest` images** — tag images explicitly in manifests for rollback safety.
|
|
458
|
+
- **No auto-scaling** — static instance. For variable load, manually scale up instance type.
|
|
459
|
+
|
|
460
|
+
## Development
|
|
461
|
+
|
|
462
|
+
### Running Tests
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
source .venv/bin/activate
|
|
466
|
+
pytest tests/ -v
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Project Structure
|
|
470
|
+
|
|
471
|
+
```
|
|
472
|
+
alchemax_plugin/
|
|
473
|
+
├── server.py # MCP server + tool definitions (12 tools)
|
|
474
|
+
├── metadata.py # alchemax.json parsing + validation
|
|
475
|
+
├── builder.py # docker buildx + registry push
|
|
476
|
+
├── runner.py # Terraform CLI wrapper
|
|
477
|
+
├── workspace.py # ~/.alp/<workspace> state management
|
|
478
|
+
├── tfstate.py # apps.json ↔ terraform.tfvars conversion
|
|
479
|
+
├── analyzer.py # language/framework detection
|
|
480
|
+
└── terraform/ # Terraform modules (6 .tf files + 2 templates)
|
|
481
|
+
├── main.tf # Providers + locals
|
|
482
|
+
├── variables.tf # All configurable vars
|
|
483
|
+
├── network.tf # VPC, subnet, security group
|
|
484
|
+
├── ec2.tf # EC2 instance + IAM role
|
|
485
|
+
├── ebs.tf # EBS volume + SSH key
|
|
486
|
+
├── outputs.tf # Terraform outputs
|
|
487
|
+
├── docker-compose.yml.tpl
|
|
488
|
+
├── nginx.conf.tpl
|
|
489
|
+
└── init.sh # EC2 user-data script
|
|
490
|
+
|
|
491
|
+
tests/
|
|
492
|
+
├── test_metadata.py # 11 tests: parsing, validation, ordering
|
|
493
|
+
├── test_workspace.py # 4 tests: state management
|
|
494
|
+
├── test_builder.py # 4 tests: Docker image analysis
|
|
495
|
+
└── test_analyzer.py # 4 tests: language detection
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Adding a New Tool
|
|
499
|
+
|
|
500
|
+
1. Add `@register_tool()` decorator with schema
|
|
501
|
+
2. Implement function in `server.py`
|
|
502
|
+
3. Add test in `tests/test_*.py`
|
|
503
|
+
4. Update README
|
|
504
|
+
|
|
505
|
+
Example:
|
|
506
|
+
```python
|
|
507
|
+
@register_tool(
|
|
508
|
+
"my_tool",
|
|
509
|
+
"Description of what it does.",
|
|
510
|
+
{
|
|
511
|
+
"type": "object",
|
|
512
|
+
"properties": {
|
|
513
|
+
"workspace": {"type": "string"},
|
|
514
|
+
},
|
|
515
|
+
"required": ["workspace"],
|
|
516
|
+
},
|
|
517
|
+
)
|
|
518
|
+
def my_tool(workspace: str) -> dict[str, Any]:
|
|
519
|
+
"""Implementation."""
|
|
520
|
+
return {"success": True, "result": "..."}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Modifying Terraform
|
|
524
|
+
|
|
525
|
+
Terraform files are ported as-is from alchemax. Key constraints:
|
|
526
|
+
- Local state only (no S3 backend)
|
|
527
|
+
- alp- prefix for all IAM roles (avoid collisions)
|
|
528
|
+
- docker-compose on t3.micro (adjust variables.tf for larger instances)
|
|
529
|
+
- EBS prevent_destroy (data safety)
|
|
530
|
+
- nginx reverse proxy on ports 80/443
|
|
531
|
+
|
|
532
|
+
Changes to `.tf` files are picked up on next `deploy_all()` or `setup()`.
|
|
533
|
+
|
|
534
|
+
## Contributing
|
|
535
|
+
|
|
536
|
+
Contributions welcome. Maintain the "primitive, not platform" philosophy:
|
|
537
|
+
- Keep it Docker-only
|
|
538
|
+
- No cross-provider types
|
|
539
|
+
- One EC2 per workspace
|
|
540
|
+
- Keep state local (no remote backend)
|
|
541
|
+
|
|
542
|
+
## License
|
|
543
|
+
|
|
544
|
+
MIT
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Source code language and framework detection."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SourceAnalyzer:
|
|
9
|
+
"""Analyzes source code for language, framework, and Dockerfile presence."""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def analyze(source_path: Path) -> dict:
|
|
13
|
+
"""Analyze source directory. Return lang, framework, has_dockerfile."""
|
|
14
|
+
if not source_path.exists():
|
|
15
|
+
return {"language": None, "framework": None, "has_dockerfile": False}
|
|
16
|
+
|
|
17
|
+
has_dockerfile = (source_path / "Dockerfile").exists()
|
|
18
|
+
if has_dockerfile:
|
|
19
|
+
return {"language": None, "framework": None, "has_dockerfile": True}
|
|
20
|
+
|
|
21
|
+
language, framework = None, None
|
|
22
|
+
|
|
23
|
+
# Check Node.js
|
|
24
|
+
pkg_json = source_path / "package.json"
|
|
25
|
+
if pkg_json.exists():
|
|
26
|
+
try:
|
|
27
|
+
with open(pkg_json) as f:
|
|
28
|
+
data = json.load(f)
|
|
29
|
+
deps = {**data.get("dependencies", {}), **data.get("devDependencies", {})}
|
|
30
|
+
if "next" in deps:
|
|
31
|
+
language, framework = "node", "next"
|
|
32
|
+
elif "express" in deps:
|
|
33
|
+
language, framework = "node", "express"
|
|
34
|
+
elif "react" in deps:
|
|
35
|
+
language, framework = "node", "react"
|
|
36
|
+
else:
|
|
37
|
+
language = "node"
|
|
38
|
+
except (json.JSONDecodeError, OSError):
|
|
39
|
+
language = "node"
|
|
40
|
+
|
|
41
|
+
# Check Python
|
|
42
|
+
if not language:
|
|
43
|
+
if (source_path / "requirements.txt").exists():
|
|
44
|
+
language = "python"
|
|
45
|
+
try:
|
|
46
|
+
with open(source_path / "requirements.txt") as f:
|
|
47
|
+
content = f.read()
|
|
48
|
+
if "fastapi" in content:
|
|
49
|
+
framework = "fastapi"
|
|
50
|
+
elif "flask" in content:
|
|
51
|
+
framework = "flask"
|
|
52
|
+
elif "django" in content:
|
|
53
|
+
framework = "django"
|
|
54
|
+
except OSError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
elif (source_path / "pyproject.toml").exists():
|
|
58
|
+
language = "python"
|
|
59
|
+
try:
|
|
60
|
+
with open(source_path / "pyproject.toml") as f:
|
|
61
|
+
content = f.read()
|
|
62
|
+
if "fastapi" in content:
|
|
63
|
+
framework = "fastapi"
|
|
64
|
+
elif "flask" in content:
|
|
65
|
+
framework = "flask"
|
|
66
|
+
elif "django" in content:
|
|
67
|
+
framework = "django"
|
|
68
|
+
except OSError:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
# Check Go
|
|
72
|
+
if not language and (source_path / "go.mod").exists():
|
|
73
|
+
language = "go"
|
|
74
|
+
|
|
75
|
+
# Check Rust
|
|
76
|
+
if not language and (source_path / "Cargo.toml").exists():
|
|
77
|
+
language = "rust"
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
"language": language,
|
|
81
|
+
"framework": framework,
|
|
82
|
+
"has_dockerfile": has_dockerfile,
|
|
83
|
+
}
|