agent-deploy-mcp 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.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-deploy-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for Agent Deploy — deploy and manage apps from any AI client
5
+ Project-URL: Homepage, https://agent-deploy.org
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: httpx>=0.27.0
8
+ Requires-Dist: mcp>=1.6.0
File without changes
@@ -0,0 +1,46 @@
1
+ """
2
+ Thin httpx wrapper for the Agent Deploy REST API.
3
+ Token and base URL are read from environment variables.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+
9
+ import httpx
10
+
11
+ BASE_URL = os.environ.get("AGENT_DEPLOY_URL", "https://agent-deploy.org").rstrip("/")
12
+ _TOKEN = os.environ.get("AGENT_DEPLOY_TOKEN", "")
13
+
14
+ TIMEOUT = 120.0
15
+
16
+
17
+ def _headers() -> dict:
18
+ return {"Authorization": f"Bearer {_TOKEN}"}
19
+
20
+
21
+ def _log(method: str, path: str, res: httpx.Response) -> None:
22
+ print(f"[agent-deploy-mcp] {method} {path} → {res.status_code}", file=sys.stderr, flush=True)
23
+
24
+
25
+ def get(path: str, **kwargs) -> httpx.Response:
26
+ res = httpx.get(f"{BASE_URL}{path}", headers=_headers(), timeout=TIMEOUT, **kwargs)
27
+ _log("GET", path, res)
28
+ return res
29
+
30
+
31
+ def post(path: str, **kwargs) -> httpx.Response:
32
+ res = httpx.post(f"{BASE_URL}{path}", headers=_headers(), timeout=TIMEOUT, **kwargs)
33
+ _log("POST", path, res)
34
+ return res
35
+
36
+
37
+ def patch(path: str, **kwargs) -> httpx.Response:
38
+ res = httpx.patch(f"{BASE_URL}{path}", headers=_headers(), timeout=TIMEOUT, **kwargs)
39
+ _log("PATCH", path, res)
40
+ return res
41
+
42
+
43
+ def delete(path: str, **kwargs) -> httpx.Response:
44
+ res = httpx.delete(f"{BASE_URL}{path}", headers=_headers(), timeout=TIMEOUT, **kwargs)
45
+ _log("DELETE", path, res)
46
+ return res
@@ -0,0 +1,317 @@
1
+ """
2
+ Agent Deploy MCP Server
3
+
4
+ Exposes the Agent Deploy platform as MCP tools so any compatible AI client
5
+ (Claude Code, Claude Desktop, Cursor, etc.) can deploy and manage apps via
6
+ natural language.
7
+
8
+ Environment variables:
9
+ AGENT_DEPLOY_URL Base URL of the Agent Deploy API (default: https://agent-deploy.org)
10
+ AGENT_DEPLOY_TOKEN Bearer token — copy from Settings → Auth token
11
+ """
12
+
13
+ import json
14
+ import os
15
+
16
+ from mcp.server.fastmcp import FastMCP
17
+
18
+ from agent_deploy_mcp import client
19
+
20
+ mcp = FastMCP("Agent Deploy")
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Helpers
25
+ # ---------------------------------------------------------------------------
26
+
27
+ def _ok(data) -> str:
28
+ return json.dumps(data, indent=2, default=str)
29
+
30
+
31
+ def _err(res) -> str:
32
+ try:
33
+ detail = res.json().get("detail", res.text)
34
+ except Exception:
35
+ detail = res.text
36
+ return f"Error {res.status_code}: {detail}"
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Tools
41
+ # ---------------------------------------------------------------------------
42
+
43
+ @mcp.tool()
44
+ def list_apps() -> str:
45
+ """List all apps belonging to the authenticated user, including live container statuses."""
46
+ res = client.get("/apps")
47
+ if not res.is_success:
48
+ return _err(res)
49
+ return _ok(res.json())
50
+
51
+
52
+ @mcp.tool()
53
+ def get_app(app_id: int) -> str:
54
+ """Get full details for a single app including its Docker images.
55
+
56
+ Args:
57
+ app_id: Numeric ID of the app (visible in list_apps).
58
+ """
59
+ res = client.get(f"/apps/{app_id}")
60
+ if not res.is_success:
61
+ return _err(res)
62
+ return _ok(res.json())
63
+
64
+
65
+ @mcp.tool()
66
+ def create_app(name: str, slug: str, app_type: str) -> str:
67
+ """Create a new app record. Does not deploy anything — use deploy_app afterwards.
68
+
69
+ Args:
70
+ name: Human-readable display name, e.g. "My Portfolio".
71
+ slug: URL-safe identifier, e.g. "my-portfolio". Must be unique per user.
72
+ app_type: One of: web, api, fullstack, static.
73
+ """
74
+ res = client.post("/apps", json={"name": name, "slug": slug, "type": app_type})
75
+ if not res.is_success:
76
+ return _err(res)
77
+ return _ok(res.json())
78
+
79
+
80
+ @mcp.tool()
81
+ def deploy_app(external_id: str, project_path: str) -> str:
82
+ """Deploy a project directory to an existing app.
83
+
84
+ BEFORE calling this tool you MUST ensure:
85
+
86
+ 1. A Dockerfile or docker-compose.yml exists in the project root.
87
+ The Dockerfile must handle ALL build steps internally — do not pre-build
88
+ on the host. Use multi-stage builds where compilation is needed, e.g.:
89
+
90
+ TypeScript / React / Vite / Next.js:
91
+ FROM node:20-alpine AS build
92
+ WORKDIR /app
93
+ COPY package*.json ./
94
+ RUN npm ci
95
+ COPY . .
96
+ RUN npm run build
97
+ FROM nginx:alpine
98
+ COPY --from=build /app/dist /usr/share/nginx/html
99
+ EXPOSE 80
100
+
101
+ Python / FastAPI / Flask:
102
+ FROM python:3.12-slim
103
+ WORKDIR /app
104
+ COPY requirements.txt .
105
+ RUN pip install --no-cache-dir -r requirements.txt
106
+ COPY . .
107
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
108
+
109
+ Full-stack (frontend + backend + db): use docker-compose.yml with a
110
+ separate service per tier. Every service that serves custom content
111
+ MUST use a 'build:' key pointing to a Dockerfile — never a bare
112
+ 'image:' key alone. A bare 'image: nginx:alpine' with no 'build:'
113
+ will deploy an empty nginx container (default welcome page). Example:
114
+
115
+ services:
116
+ frontend:
117
+ build:
118
+ context: ./frontend
119
+ dockerfile: Dockerfile
120
+ backend:
121
+ build: ./backend
122
+ db:
123
+ image: postgres:16
124
+ environment:
125
+ POSTGRES_PASSWORD: secret
126
+
127
+ 2. The Dockerfile output port matches the service — nginx uses 80, FastAPI
128
+ defaults to 8000. Expose the correct port with EXPOSE.
129
+
130
+ This tool returns a shell script — run it with the Bash tool. Do NOT zip
131
+ or upload the file yourself.
132
+
133
+ Args:
134
+ external_id: The app's external_id from list_apps or create_app.
135
+ project_path: Absolute path to the project directory on the host.
136
+ """
137
+ token = os.environ.get("AGENT_DEPLOY_TOKEN", "")
138
+ base_url = os.environ.get("AGENT_DEPLOY_URL", "https://agent-deploy.org").rstrip("/")
139
+ auth = f'-H "Authorization: Bearer {token}"' if token else ""
140
+ zip_path = f"/tmp/agent-deploy-{external_id}.zip"
141
+
142
+ script = f"""
143
+ # 1. Validate Docker configuration is present
144
+ if [ ! -f "{project_path}/Dockerfile" ] && [ ! -f "{project_path}/docker-compose.yml" ] && [ ! -f "{project_path}/docker-compose.yaml" ]; then
145
+ echo "ERROR: No Dockerfile or docker-compose.yml found in {project_path}."
146
+ echo "The Dockerfile must include all build steps (e.g. npm run build for JS/TS apps)."
147
+ exit 1
148
+ fi
149
+
150
+ # 2. Warn if a JS/TS project has no build step in Dockerfile
151
+ if [ -f "{project_path}/package.json" ] && [ -f "{project_path}/Dockerfile" ]; then
152
+ if ! grep -q "npm run build\\|yarn build\\|pnpm build" "{project_path}/Dockerfile"; then
153
+ echo "WARNING: package.json detected but no build step found in Dockerfile."
154
+ echo "For TypeScript/JSX apps, add 'RUN npm run build' to your Dockerfile."
155
+ fi
156
+ fi
157
+
158
+ # 3. Warn if a compose service uses a bare image without a build context
159
+ for compose_file in "{project_path}/docker-compose.yml" "{project_path}/docker-compose.yaml"; do
160
+ if [ -f "$compose_file" ]; then
161
+ if grep -q "image:\\s*nginx" "$compose_file" && ! grep -B5 "image:\\s*nginx" "$compose_file" | grep -q "build:"; then
162
+ echo "WARNING: docker-compose.yml has a service using 'image: nginx' without a 'build:' section."
163
+ echo "This will deploy an empty nginx container (default welcome page)."
164
+ echo "Add a 'build:' key pointing to a Dockerfile with a multi-stage build."
165
+ fi
166
+ fi
167
+ done
168
+
169
+ # 4. Zip the project (excluding build artefacts and hidden files)
170
+ cd "{project_path}" && zip -r "{zip_path}" . \\
171
+ --exclude "*.git*" \\
172
+ --exclude "node_modules/*" \\
173
+ --exclude "__pycache__/*" \\
174
+ --exclude "*.pyc" \\
175
+ --exclude ".env" \\
176
+ --exclude "*.DS_Store"
177
+
178
+ # 5. Upload and deploy
179
+ curl -s -X POST "{base_url}/apps/upload" \\
180
+ {auth} \\
181
+ -F "external_id={external_id}" \\
182
+ -F "file=@{zip_path}"
183
+
184
+ # 6. Clean up
185
+ rm -f "{zip_path}"
186
+ """.strip()
187
+
188
+ return (
189
+ f"Run the following script using the Bash tool:\n\n{script}\n\n"
190
+ "Then call get_app_status to monitor the deployment."
191
+ )
192
+
193
+
194
+ @mcp.tool()
195
+ def get_app_status(app_id: int) -> str:
196
+ """Get live container statuses for every service in an app.
197
+
198
+ Returns a mapping of image_type → {status, started_at}.
199
+ Status values: running, exited, restarting, paused, dead, not_started.
200
+
201
+ Args:
202
+ app_id: Numeric ID of the app.
203
+ """
204
+ res = client.get(f"/apps/{app_id}/status")
205
+ if not res.is_success:
206
+ return _err(res)
207
+ return _ok(res.json())
208
+
209
+
210
+ @mcp.tool()
211
+ def get_logs(app_id: int, image_type: str, limit: int = 30) -> str:
212
+ """Fetch the most recent log lines for a container.
213
+
214
+ Args:
215
+ app_id: Numeric ID of the app.
216
+ image_type: Service name, e.g. frontend, backend, db, worker.
217
+ limit: Number of log lines to return (default 30, max 200).
218
+ """
219
+ res = client.get(
220
+ f"/apps/{app_id}/images/{image_type}/logs",
221
+ params={"limit": limit},
222
+ )
223
+ if not res.is_success:
224
+ return _err(res)
225
+ entries = res.json()
226
+ if not entries:
227
+ return "No logs found for this container."
228
+ lines = [f"[{e['timestamp']}] {e['log_str']}" for e in entries]
229
+ return "\n".join(lines)
230
+
231
+
232
+ @mcp.tool()
233
+ def analyze_logs(app_id: int, image_type: str) -> str:
234
+ """Get a plain-English AI summary of the last 20 log lines for a container.
235
+
236
+ Args:
237
+ app_id: Numeric ID of the app.
238
+ image_type: Service name, e.g. frontend, backend, db, worker.
239
+ """
240
+ res = client.get(f"/apps/{app_id}/images/{image_type}/logs/analyze")
241
+ if not res.is_success:
242
+ return _err(res)
243
+ analysis = res.json().get("analysis")
244
+ if not analysis:
245
+ return "Analysis unavailable."
246
+ return analysis
247
+
248
+
249
+ @mcp.tool()
250
+ def restart_app(app_id: int) -> str:
251
+ """Restart all containers for an app.
252
+
253
+ Args:
254
+ app_id: Numeric ID of the app.
255
+ """
256
+ res = client.post(f"/apps/{app_id}/restart")
257
+ if not res.is_success:
258
+ return _err(res)
259
+ return _ok(res.json())
260
+
261
+
262
+ @mcp.tool()
263
+ def set_app_visibility(app_id: int, public: bool) -> str:
264
+ """Make an app public or private.
265
+
266
+ Args:
267
+ app_id: Numeric ID of the app.
268
+ public: True to make public, False to make private.
269
+ """
270
+ res = client.patch(f"/apps/{app_id}", json={"is_public": public})
271
+ if not res.is_success:
272
+ return _err(res)
273
+ return f"App {app_id} is now {'public' if public else 'private'}."
274
+
275
+
276
+ @mcp.tool()
277
+ def update_app(app_id: int, name: str | None = None, slug: str | None = None, app_type: str | None = None) -> str:
278
+ """Update an app's name, slug, or type. Only provided fields are changed.
279
+
280
+ Args:
281
+ app_id: Numeric ID of the app.
282
+ name: New display name (optional).
283
+ slug: New URL slug (optional). Must be unique per user.
284
+ app_type: New type: web, api, fullstack, static (optional).
285
+ """
286
+ payload = {k: v for k, v in {"name": name, "slug": slug, "type": app_type}.items() if v is not None}
287
+ if not payload:
288
+ return "No fields provided — nothing to update."
289
+ res = client.patch(f"/apps/{app_id}", json=payload)
290
+ if not res.is_success:
291
+ return _err(res)
292
+ return _ok(res.json())
293
+
294
+
295
+ @mcp.tool()
296
+ def delete_app(app_id: int) -> str:
297
+ """Soft-delete an app. Containers are stopped and the app is removed from the dashboard.
298
+
299
+ Args:
300
+ app_id: Numeric ID of the app to delete.
301
+ """
302
+ res = client.delete(f"/apps/{app_id}")
303
+ if not res.is_success:
304
+ return _err(res)
305
+ return f"App {app_id} deleted successfully."
306
+
307
+
308
+ # ---------------------------------------------------------------------------
309
+ # Entry point
310
+ # ---------------------------------------------------------------------------
311
+
312
+ def main() -> None:
313
+ mcp.run(transport="stdio")
314
+
315
+
316
+ if __name__ == "__main__":
317
+ main()
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agent-deploy-mcp"
7
+ version = "0.1.0"
8
+ description = "MCP server for Agent Deploy — deploy and manage apps from any AI client"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "mcp>=1.6.0",
12
+ "httpx>=0.27.0",
13
+ ]
14
+
15
+ [project.scripts]
16
+ agent-deploy-mcp = "agent_deploy_mcp.server:main"
17
+
18
+ [project.urls]
19
+ Homepage = "https://agent-deploy.org"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["agent_deploy_mcp"]