feat: v3.7.0 — Coolify MCP plugin (30 tools)
New plugin: Coolify deployment management (applications, deployments, servers). 10 plugins, 596 tools, 686 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
**MCP Hub** — a Python MCP (Model Context Protocol) server that manages multiple self-hosted services through a unified plugin architecture. Supports 9 plugin types (WordPress, WooCommerce, WordPress Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with 567 tools total. The tool count stays constant regardless of how many sites are configured.
|
**MCP Hub** — a Python MCP (Model Context Protocol) server that manages multiple self-hosted services through a unified plugin architecture. Supports 10 plugin types (WordPress, WooCommerce, WordPress Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus, Coolify) with 597 tools total. The tool count stays constant regardless of how many sites are configured.
|
||||||
|
|
||||||
## Quick Setup
|
## Quick Setup
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ plugins/{name}/
|
|||||||
└── schemas/ # Pydantic models for validation
|
└── schemas/ # Pydantic models for validation
|
||||||
```
|
```
|
||||||
|
|
||||||
**Registered plugins**: wordpress, woocommerce, wordpress_advanced, gitea, n8n, supabase, openpanel, appwrite, directus
|
**Registered plugins**: wordpress, woocommerce, wordpress_advanced, gitea, n8n, supabase, openpanel, appwrite, directus, coolify
|
||||||
|
|
||||||
**Plugin visibility** (Track F.1): Public users only see plugins listed in `ENABLED_PLUGINS` env var (default: `wordpress,woocommerce,supabase`). Admin sees all. Controlled by `core/plugin_visibility.py`.
|
**Plugin visibility** (Track F.1): Public users only see plugins listed in `ENABLED_PLUGINS` env var (default: `wordpress,woocommerce,supabase`). Admin sees all. Controlled by `core/plugin_visibility.py`.
|
||||||
|
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -13,8 +13,8 @@ Connect your sites, stores, repos, and databases — manage them all through Cla
|
|||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
[](https://pypi.org/project/mcphub-server/)
|
[](https://pypi.org/project/mcphub-server/)
|
||||||
[](https://hub.docker.com/r/airano/mcphub)
|
[](https://hub.docker.com/r/airano/mcphub)
|
||||||
[]()
|
[]()
|
||||||
[]()
|
[]()
|
||||||
[](https://github.com/airano-ir/mcphub/actions/workflows/ci.yml)
|
[](https://github.com/airano-ir/mcphub/actions/workflows/ci.yml)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -25,7 +25,7 @@ Connect your sites, stores, repos, and databases — manage them all through Cla
|
|||||||
|
|
||||||
WordPress powers 43% of the web. WooCommerce runs 36% of online stores. Yet **no MCP server existed** for managing them through AI — until now.
|
WordPress powers 43% of the web. WooCommerce runs 36% of online stores. Yet **no MCP server existed** for managing them through AI — until now.
|
||||||
|
|
||||||
MCP Hub is the first MCP server that lets you manage WordPress, WooCommerce, and 7 other self-hosted services through any AI assistant. Instead of clicking through dashboards, just tell your AI what to do:
|
MCP Hub is the first MCP server that lets you manage WordPress, WooCommerce, and 8 other self-hosted services through any AI assistant. Instead of clicking through dashboards, just tell your AI what to do:
|
||||||
|
|
||||||
> *"Update the SEO meta description for all WooCommerce products that don't have one"*
|
> *"Update the SEO meta description for all WooCommerce products that don't have one"*
|
||||||
>
|
>
|
||||||
@@ -62,8 +62,9 @@ MCP Hub is the first MCP server that lets you manage WordPress, WooCommerce, and
|
|||||||
| **OpenPanel** | 42 | Events, export, insights, profiles, projects, system |
|
| **OpenPanel** | 42 | Events, export, insights, profiles, projects, system |
|
||||||
| **Appwrite** | 100 | Databases, auth, storage, functions, teams, messaging |
|
| **Appwrite** | 100 | Databases, auth, storage, functions, teams, messaging |
|
||||||
| **Directus** | 100 | Collections, items, users, files, flows, permissions |
|
| **Directus** | 100 | Collections, items, users, files, flows, permissions |
|
||||||
|
| **Coolify** | 30 | Applications, deployments, servers, env vars, logs |
|
||||||
| **System** | 24 | Health monitoring, API keys, OAuth management, audit |
|
| **System** | 24 | Health monitoring, API keys, OAuth management, audit |
|
||||||
| **Total** | **565** | Constant count — scales to unlimited sites |
|
| **Total** | **596** | Constant count — scales to unlimited sites |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -164,6 +165,7 @@ MASTER_API_KEY=your-secure-key-here
|
|||||||
| OpenPanel | URL, Client ID, Client Secret | OpenPanel Dashboard → Project Settings |
|
| OpenPanel | URL, Client ID, Client Secret | OpenPanel Dashboard → Project Settings |
|
||||||
| Appwrite | URL, API Key, Project ID | Appwrite Console → Settings → API Keys |
|
| Appwrite | URL, API Key, Project ID | Appwrite Console → Settings → API Keys |
|
||||||
| Directus | URL, Static Token | Directus Admin → Settings |
|
| Directus | URL, Static Token | Directus Admin → Settings |
|
||||||
|
| Coolify | URL, API Token | Coolify → Keys & Tokens → API tokens |
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -276,11 +278,12 @@ MCP Hub supports **Open Dynamic Client Registration** (RFC 7591). ChatGPT can au
|
|||||||
/openpanel/mcp → OpenPanel tools (42 tools)
|
/openpanel/mcp → OpenPanel tools (42 tools)
|
||||||
/appwrite/mcp → Appwrite tools (100 tools)
|
/appwrite/mcp → Appwrite tools (100 tools)
|
||||||
/directus/mcp → Directus tools (100 tools)
|
/directus/mcp → Directus tools (100 tools)
|
||||||
|
/coolify/mcp → Coolify tools (30 tools)
|
||||||
/project/{alias}/mcp → Per-project endpoint (auto-injects site)
|
/project/{alias}/mcp → Per-project endpoint (auto-injects site)
|
||||||
/u/{user_id}/{alias}/mcp → Per-user endpoint (hosted/OAuth users)
|
/u/{user_id}/{alias}/mcp → Per-user endpoint (hosted/OAuth users)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Recommendation**: Use plugin-specific endpoints instead of `/mcp` (565 tools) to minimize token usage.
|
**Recommendation**: Use plugin-specific endpoints instead of `/mcp` (596 tools) to minimize token usage.
|
||||||
|
|
||||||
| Endpoint | Use Case | Tools |
|
| Endpoint | Use Case | Tools |
|
||||||
|----------|----------|------:|
|
|----------|----------|------:|
|
||||||
|
|||||||
@@ -207,6 +207,15 @@ PLUGIN_CREDENTIAL_FIELDS: dict[str, list[dict[str, Any]]] = {
|
|||||||
"hint": "Directus → Settings → User → Static Token",
|
"hint": "Directus → Settings → User → Static Token",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"coolify": [
|
||||||
|
{
|
||||||
|
"name": "token",
|
||||||
|
"label": "API Token",
|
||||||
|
"type": "password",
|
||||||
|
"required": True,
|
||||||
|
"hint": "Coolify → Keys & Tokens → API tokens → Create",
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Plugin display names for UI
|
# Plugin display names for UI
|
||||||
@@ -220,6 +229,7 @@ PLUGIN_DISPLAY_NAMES: dict[str, str] = {
|
|||||||
"openpanel": "OpenPanel",
|
"openpanel": "OpenPanel",
|
||||||
"appwrite": "Appwrite",
|
"appwrite": "Appwrite",
|
||||||
"directus": "Directus",
|
"directus": "Directus",
|
||||||
|
"coolify": "Coolify",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Health check endpoints per plugin type
|
# Health check endpoints per plugin type
|
||||||
@@ -233,6 +243,7 @@ _HEALTH_ENDPOINTS: dict[str, dict[str, Any]] = {
|
|||||||
"openpanel": {"path": "/healthcheck", "method": "GET"},
|
"openpanel": {"path": "/healthcheck", "method": "GET"},
|
||||||
"appwrite": {"path": "/v1/health", "method": "GET"},
|
"appwrite": {"path": "/v1/health", "method": "GET"},
|
||||||
"directus": {"path": "/server/health", "method": "GET"},
|
"directus": {"path": "/server/health", "method": "GET"},
|
||||||
|
"coolify": {"path": "/api/v1/version", "method": "GET"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -354,7 +365,7 @@ async def validate_site_connection(
|
|||||||
elif plugin_type == "appwrite":
|
elif plugin_type == "appwrite":
|
||||||
headers["X-Appwrite-Project"] = credentials.get("project_id", "")
|
headers["X-Appwrite-Project"] = credentials.get("project_id", "")
|
||||||
headers["X-Appwrite-Key"] = credentials.get("api_key", "")
|
headers["X-Appwrite-Key"] = credentials.get("api_key", "")
|
||||||
elif plugin_type == "directus":
|
elif plugin_type in ("directus", "coolify"):
|
||||||
headers["Authorization"] = f"Bearer {credentials.get('token', '')}"
|
headers["Authorization"] = f"Bearer {credentials.get('token', '')}"
|
||||||
elif plugin_type == "openpanel":
|
elif plugin_type == "openpanel":
|
||||||
headers["openpanel-client-id"] = credentials.get("client_id", "")
|
headers["openpanel-client-id"] = credentials.get("client_id", "")
|
||||||
|
|||||||
95
docs/prompts/next-session-coolify-f17.md
Normal file
95
docs/prompts/next-session-coolify-f17.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# Next Session — F.17 Coolify MCP Plugin (MVP)
|
||||||
|
|
||||||
|
> Session prompt. Copy and paste into a new Claude Code conversation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt:
|
||||||
|
|
||||||
|
```
|
||||||
|
از مهارت project-ops برای آشنایی با محیط استفاده کن. حافظه و فایلهای مرجع را بررسی کن.
|
||||||
|
|
||||||
|
## فایلهای مرجع
|
||||||
|
- docs/plans/2026-04-02-coolify-mcp-plugin-design.md (mcphub-internal) ← طرح کامل پلاگین
|
||||||
|
- docs/plans/2026-03-25-v4-development-cycle.md (mcphub-internal, branch Phase-1) ← فاز F.17
|
||||||
|
- CLAUDE.md (mcphub-internal)
|
||||||
|
- plugins/gitea/ (mcphub-internal) ← الگوی مرجع (آخرین پلاگین ساختهشده)
|
||||||
|
- plugins/base.py (mcphub-internal) ← BasePlugin interface
|
||||||
|
|
||||||
|
## وضعیت فعلی
|
||||||
|
- MCPHub: v3.6.0 — 566 ابزار، 5 پلاگین عمومی
|
||||||
|
- FastMCP: >=3.0.0,<4.0.0
|
||||||
|
- CI سبز
|
||||||
|
- طرح Coolify: نوشته شده — ~68 ابزار در 6 handler
|
||||||
|
|
||||||
|
## ریپازیتوری
|
||||||
|
- MCPHub (internal): `/config/workspace/mcphub-internal` (branch Phase-1)
|
||||||
|
|
||||||
|
## هدف session: F.17 Phase 1 — Coolify MVP
|
||||||
|
|
||||||
|
### پیشنیاز اول: Coolify API Token
|
||||||
|
- [ ] در داشبورد Coolify، بخش Keys & Tokens، یک API token بساز
|
||||||
|
- [ ] تست اتصال: `curl -s https://COOLIFY_URL/api/v1/version -H "Authorization: Bearer TOKEN"`
|
||||||
|
- [ ] اگر URL و TOKEN مشخص نبود، از کاربر بپرس
|
||||||
|
|
||||||
|
### مرحله ۱: ساختار اولیه پلاگین
|
||||||
|
- [ ] `plugins/coolify/__init__.py` (خالی)
|
||||||
|
- [ ] `plugins/coolify/client.py` — CoolifyClient با Bearer Token auth
|
||||||
|
- الگو از `plugins/gitea/client.py` بگیر
|
||||||
|
- متدها: `request()`, `get()`, `post()`, `patch()`, `delete()`
|
||||||
|
- Error handling + retry مشابه Gitea client
|
||||||
|
- [ ] `plugins/coolify/plugin.py` — CoolifyPlugin(BasePlugin)
|
||||||
|
- الگو از `plugins/gitea/plugin.py` بگیر
|
||||||
|
- `get_tool_specifications()` باید ابزارهای handler ها رو جمع کنه
|
||||||
|
- [ ] `plugins/coolify/handlers/__init__.py`
|
||||||
|
- [ ] `plugins/coolify/schemas/__init__.py`
|
||||||
|
- [ ] `plugins/coolify/schemas/common.py` — مدلهای مشترک (UUID, pagination)
|
||||||
|
|
||||||
|
### مرحله ۲: Handler — Applications (18 ابزار)
|
||||||
|
- [ ] `plugins/coolify/handlers/applications.py`
|
||||||
|
- [ ] ابزارها (از طرح):
|
||||||
|
- list_applications, get_application
|
||||||
|
- create_application_public, create_application_dockerfile, create_application_docker_image, create_application_compose
|
||||||
|
- update_application, delete_application
|
||||||
|
- start_application, stop_application, restart_application
|
||||||
|
- get_application_logs
|
||||||
|
- list_application_envs, create_application_env, update_application_env, update_application_envs_bulk, delete_application_env
|
||||||
|
- [ ] schemas/application.py — Pydantic models
|
||||||
|
|
||||||
|
### مرحله ۳: Handler — Deployments (5 ابزار)
|
||||||
|
- [ ] `plugins/coolify/handlers/deployments.py`
|
||||||
|
- [ ] ابزارها: list_deployments, get_deployment, cancel_deployment, deploy, list_app_deployments
|
||||||
|
|
||||||
|
### مرحله ۴: Handler — Servers (8 ابزار)
|
||||||
|
- [ ] `plugins/coolify/handlers/servers.py`
|
||||||
|
- [ ] ابزارها: list_servers, get_server, create_server, update_server, delete_server, get_server_resources, get_server_domains, validate_server
|
||||||
|
- [ ] schemas/server.py
|
||||||
|
|
||||||
|
### مرحله ۵: ثبت پلاگین و تنظیمات
|
||||||
|
- [ ] در `plugins/__init__.py` اضافه کن: `from plugins.coolify.plugin import CoolifyPlugin` + `registry.register("coolify", CoolifyPlugin)`
|
||||||
|
- [ ] در `env.example` اضافه کن: `COOLIFY_URL`, `COOLIFY_TOKEN`
|
||||||
|
- [ ] پلاگین فعلا admin-only باشد (به ENABLED_PLUGINS اضافه نشود)
|
||||||
|
|
||||||
|
### مرحله ۶: تست
|
||||||
|
- [ ] `tests/test_coolify.py` — Unit tests با mocked HTTP
|
||||||
|
- الگو از `tests/test_gitea*.py` بگیر
|
||||||
|
- حداقل: test_list_applications, test_get_application, test_deploy, test_list_servers
|
||||||
|
- [ ] `pytest tests/test_coolify.py -v`
|
||||||
|
- [ ] اگر API Token موجود بود: integration test با Coolify واقعی
|
||||||
|
|
||||||
|
### مرحله ۷: تست نهایی و commit
|
||||||
|
- [ ] `uvx --python 3.12 black .`
|
||||||
|
- [ ] `uvx ruff check --fix .`
|
||||||
|
- [ ] `pytest` (همه تستها سبز)
|
||||||
|
- [ ] Commit: `feat(F.17): add Coolify MCP plugin — Phase 1 MVP (~31 tools)`
|
||||||
|
- [ ] Push to Phase-1
|
||||||
|
|
||||||
|
## قوانین
|
||||||
|
- اول پلن بده، بدون تایید شروع نکن
|
||||||
|
- از Gitea plugin به عنوان الگوی اصلی استفاده کن (آخرین و تمیزترین پلاگین)
|
||||||
|
- هر مرحله commit شود
|
||||||
|
- tool specifications باید دقیقا مطابق فرمت BasePlugin باشند (name, method_name, description, schema, scope)
|
||||||
|
- scope ها: read, write, admin — مطابق جدول در طرح
|
||||||
|
- حافظه آپدیت شود بعد از اتمام
|
||||||
|
- ایمیل git داخلی: mcphub.dev@gmail.com
|
||||||
|
```
|
||||||
77
docs/prompts/next-session-coolify-test.md
Normal file
77
docs/prompts/next-session-coolify-test.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Next Session — Coolify MCP Plugin Testing & Next Steps
|
||||||
|
|
||||||
|
> Session prompt. Copy and paste into a new Claude Code conversation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt:
|
||||||
|
|
||||||
|
```
|
||||||
|
از مهارت project-ops برای آشنایی با محیط استفاده کن. حافظه و فایلهای مرجع را بررسی کن.
|
||||||
|
|
||||||
|
## هدف: تست پلاگین Coolify MCP و مراحل بعدی
|
||||||
|
|
||||||
|
### پیشنیاز
|
||||||
|
- MCPHub redeploy شده و سایت Coolify اضافه شده
|
||||||
|
- MCP endpoint `mcphub-coolify` در `.claude.json` تنظیم شده
|
||||||
|
- پلاگین 30 ابزار دارد (17 application + 5 deployment + 8 server)
|
||||||
|
|
||||||
|
### مرحله ۱: تأیید اتصال MCP
|
||||||
|
- [ ] لیست ابزارهای coolify را از MCP بگیر (باید 30 ابزار باشد)
|
||||||
|
- [ ] اگر ابزار coolify در لیست نبود، `.claude.json` و `settings.json` را بررسی کن
|
||||||
|
|
||||||
|
### مرحله ۲: تست ابزارهای read
|
||||||
|
- [ ] `coolify_list_servers` — لیست سرورها
|
||||||
|
- [ ] `coolify_list_applications` — لیست اپلیکیشنها
|
||||||
|
- [ ] `coolify_list_deployments` — لیست دیپلویمنتهای در حال اجرا
|
||||||
|
- [ ] `coolify_get_server_resources(uuid=SERVER_UUID)` — منابع سرور
|
||||||
|
- [ ] `coolify_get_server_domains(uuid=SERVER_UUID)` — دامنههای سرور
|
||||||
|
- [ ] یک اپلیکیشن انتخاب کن و:
|
||||||
|
- [ ] `coolify_get_application(uuid=APP_UUID)` — جزئیات
|
||||||
|
- [ ] `coolify_get_application_logs(uuid=APP_UUID, lines=50)` — لاگها
|
||||||
|
- [ ] `coolify_list_application_envs(uuid=APP_UUID)` — متغیرهای محیطی
|
||||||
|
- [ ] `coolify_list_app_deployments(uuid=APP_UUID)` — تاریخچه دیپلوی
|
||||||
|
|
||||||
|
### مرحله ۳: تست ابزارهای write (با احتیاط)
|
||||||
|
- [ ] از کاربر بپرس آیا مجاز است یک env var تست ایجاد/حذف کند
|
||||||
|
- [ ] اگر بله:
|
||||||
|
- [ ] `coolify_create_application_env(uuid=APP_UUID, key="TEST_VAR", value="test123")`
|
||||||
|
- [ ] `coolify_delete_application_env(uuid=APP_UUID, env_uuid=...)`
|
||||||
|
|
||||||
|
### مرحله ۴: گزارش نتایج
|
||||||
|
- [ ] خلاصه نتایج تست (چند ابزار کار کرد، مشکلات)
|
||||||
|
- [ ] مقایسه با ابزارهای Gitea و Supabase MCP (کیفیت پاسخها)
|
||||||
|
|
||||||
|
### مرحله ۵: اگر تست موفق بود — Sync به نسخه عمومی
|
||||||
|
- [ ] `python3.11 scripts/community-build/sync.py --output ../mcphub/`
|
||||||
|
- [ ] `cd /config/workspace/mcphub && uvx --python 3.12 black . && uvx ruff check --fix .`
|
||||||
|
- [ ] تستها: `python3.11 -m pytest tests/ -q`
|
||||||
|
- [ ] Commit و push نسخه عمومی
|
||||||
|
|
||||||
|
### مرحله ۶: آپدیتها
|
||||||
|
- [ ] mcp-skills/skills/coolify/SKILL.md — از planned به active تغییر کرده (بررسی شود)
|
||||||
|
- [ ] حافظه آپدیت شود
|
||||||
|
|
||||||
|
## پیشنهاد مراحل بعدی (بعد از تست)
|
||||||
|
|
||||||
|
### فوری
|
||||||
|
1. **F.17 Phase 2**: اضافه کردن databases (16 ابزار) + services (13 ابزار) → ~59 ابزار کل
|
||||||
|
2. **F.17 Phase 3**: projects (8 ابزار) → ~67 ابزار کل — تکمیل طرح اصلی
|
||||||
|
|
||||||
|
### میانمدت
|
||||||
|
3. **Coolify Workflow Skill**: مهارت اختصاصی برای عملیات متداول (deploy all, backup all, health check all)
|
||||||
|
4. **Integration Test**: تست خودکار با Coolify واقعی (pytest mark integration)
|
||||||
|
5. **F.14a**: ثبت MCPHub در Smithery + Official MCP Registry
|
||||||
|
|
||||||
|
### بلندمدت
|
||||||
|
6. **Blog Post**: نوشتن مقاله درباره "Self-hosted MCP Hub with Coolify Integration"
|
||||||
|
7. **F.5a**: Base64 media upload برای WordPress
|
||||||
|
8. **F.6**: Claude Code skills بومی
|
||||||
|
|
||||||
|
## قوانین
|
||||||
|
- اول ابزارهای read تست شوند، بعد write
|
||||||
|
- قبل از هر عملیات write از کاربر تأیید بگیر
|
||||||
|
- نتایج تست دقیق گزارش شود (UUID ها، خطاها، زمان پاسخ)
|
||||||
|
- حافظه آپدیت شود بعد از اتمام
|
||||||
|
- ایمیل git داخلی: mcphub.dev@gmail.com
|
||||||
|
```
|
||||||
93
docs/prompts/next-session.md
Normal file
93
docs/prompts/next-session.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Next Session — Registry + MCP Skills + Infrastructure
|
||||||
|
|
||||||
|
> Session prompt. Copy and paste into a new Claude Code conversation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt:
|
||||||
|
|
||||||
|
```
|
||||||
|
از مهارت project-ops برای آشنایی با محیط استفاده کن. حافظه و فایلهای مرجع را بررسی کن.
|
||||||
|
|
||||||
|
## فایلهای مرجع
|
||||||
|
- docs/plans/2026-03-25-v4-development-cycle.md (mcphub-internal, branch Phase-1)
|
||||||
|
- CLAUDE.md (mcphub-internal)
|
||||||
|
- CHANGELOG.md (mcphub-internal)
|
||||||
|
|
||||||
|
## وضعیت فعلی
|
||||||
|
- MCPHub: v3.6.0 — 567 ابزار، 5 پلاگین عمومی (WordPress, WooCommerce, Supabase, OpenPanel, Gitea)
|
||||||
|
- FastMCP: >=3.0.0,<4.0.0
|
||||||
|
- CI سبز
|
||||||
|
|
||||||
|
## MCP Endpoints فعال
|
||||||
|
- mcphub-supabase: 70 ابزار Supabase (DB, auth, storage)
|
||||||
|
- mcphub-gitea: 58 ابزار Gitea (repos, issues, PRs, webhooks)
|
||||||
|
|
||||||
|
## ریپازیتوریها
|
||||||
|
### GitHub (airano-ir)
|
||||||
|
- mcphub (public) → /config/workspace/mcphub
|
||||||
|
- mcphub (private, Phase-1) → /config/workspace/mcphub-internal
|
||||||
|
- mcp-skills (private, خالی) → /config/workspace/mcp-skills
|
||||||
|
- skillhub-internal → /config/workspace/skillhub-internal
|
||||||
|
- skillhub (public) → /config/workspace/skillhub
|
||||||
|
|
||||||
|
### Gitea (atlatl @ gitea.example.com)
|
||||||
|
- polymarket (private) → /config/workspace/polymarket
|
||||||
|
- polymarket-skill (private) → /config/workspace/polymarket-skill
|
||||||
|
- project-ops (private) → مهارت در /config/.claude/skills/project-ops/
|
||||||
|
|
||||||
|
## اهداف این session (به ترتیب اولویت)
|
||||||
|
|
||||||
|
### 1. Registry Submissions (F.14a)
|
||||||
|
وضعیت فعلی:
|
||||||
|
- Glama: ثبت شده (Score: A)، glama.json اضافه شده
|
||||||
|
- awesome-mcp-servers: PR #2147 باز — badge SVG + tool count اصلاح شده، منتظر merge
|
||||||
|
- Smithery.ai: ثبت نشده → از smithery.ai/new ثبت کن (نیاز به public HTTPS URL: mcp.example.com)
|
||||||
|
- Official MCP Registry: ثبت نشده → بررسی `mcp-publisher` CLI
|
||||||
|
کارها:
|
||||||
|
- [ ] وضعیت PR #2147 چک شود — اگر feedback جدید دارد رفع شود
|
||||||
|
- [ ] ثبت در Smithery.ai
|
||||||
|
- [ ] بررسی Official MCP Registry submission process
|
||||||
|
|
||||||
|
### 2. MCP Skills — اولین مهارتها (github:airano-ir/mcp-skills)
|
||||||
|
ریپازیتوری خالی ساخته شده. ساختار:
|
||||||
|
```
|
||||||
|
mcp-skills/skills/
|
||||||
|
├── wordpress/ ← اولین مهارت
|
||||||
|
├── supabase/
|
||||||
|
├── gitea/
|
||||||
|
├── woocommerce/
|
||||||
|
├── openpanel/
|
||||||
|
└── coolify/ ← آینده
|
||||||
|
```
|
||||||
|
کارها:
|
||||||
|
- [ ] از SkillHub بهترین مهارتهای مرتبط را جستجو کن: `npx skillhub search "wordpress mcp" --sort aiScore`
|
||||||
|
- [ ] اگر مهارت با کیفیت بالا (aiScore > 70) پیدا نشد، با skill-creator مهارت جدید بساز
|
||||||
|
- [ ] اولین مهارت: WordPress content workflow (استفاده از MCP endpoint مستقیم)
|
||||||
|
- [ ] هر مهارت باید SKILL.md + اسکریپتهای عملی داشته باشد
|
||||||
|
- [ ] بعد از تست، commit و push به github:airano-ir/mcp-skills
|
||||||
|
|
||||||
|
### 3. Project-Ops Sync با Gitea
|
||||||
|
- [ ] محتوای `/config/.claude/skills/project-ops/SKILL.md` را به `gitea:atlatl/project-ops` push کن
|
||||||
|
- [ ] مطمئن شو backup در Gitea up-to-date است
|
||||||
|
|
||||||
|
### 4. Blog Workspace Setup
|
||||||
|
- [ ] WordPress MCP endpoint اضافه کن از داشبورد mcp.example.com (blog.example.com)
|
||||||
|
- [ ] یک پست تست با MCP WordPress tools بنویس
|
||||||
|
- [ ] اگر API مشکلی داشت، در mcphub-internal fix کن
|
||||||
|
|
||||||
|
### 5. بررسی Coolify MCP (F.17)
|
||||||
|
- [ ] API documentation کولیفای را بررسی کن
|
||||||
|
- [ ] بررسی آیا Coolify API در دسترس است از این محیط
|
||||||
|
- [ ] یک طرح اولیه از tools مورد نیاز بنویس
|
||||||
|
- [ ] نتیجه را در docs/plans/ ذخیره کن
|
||||||
|
|
||||||
|
## قوانین
|
||||||
|
- اول پلن بده، بدون تایید شروع نکن
|
||||||
|
- هر مرحله commit شود
|
||||||
|
- حافظه و project-ops در صورت نیاز آپدیت شود
|
||||||
|
- از SkillHub برای پیدا کردن بهترین مهارتها استفاده کن (aiScore بالاترین)
|
||||||
|
- اگر مهارتی بررسی نشده، با auto-review بررسی کن
|
||||||
|
- ایمیل git عمومی: hi.airano@gmail.com
|
||||||
|
- ایمیل git داخلی: mcphub.dev@gmail.com
|
||||||
|
```
|
||||||
@@ -89,6 +89,14 @@ DASHBOARD_SESSION_SECRET=
|
|||||||
# USER_RATE_LIMIT_PER_MIN=30
|
# USER_RATE_LIMIT_PER_MIN=30
|
||||||
# USER_RATE_LIMIT_PER_HR=500
|
# USER_RATE_LIMIT_PER_HR=500
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# COOLIFY (admin-only — F.17)
|
||||||
|
# ============================================
|
||||||
|
# Coolify instance for AI-driven deployment management.
|
||||||
|
# Create an API token in Coolify: Keys & Tokens → API tokens
|
||||||
|
# COOLIFY_URL=https://coolify.example.com
|
||||||
|
# COOLIFY_TOKEN=your-api-token-here
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# ADVANCED (optional — defaults are fine)
|
# ADVANCED (optional — defaults are fine)
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ v2.3.0 (Phase G): Supabase Self-Hosted Plugin added
|
|||||||
|
|
||||||
from plugins.appwrite.plugin import AppwritePlugin
|
from plugins.appwrite.plugin import AppwritePlugin
|
||||||
from plugins.base import BasePlugin, PluginRegistry
|
from plugins.base import BasePlugin, PluginRegistry
|
||||||
|
from plugins.coolify.plugin import CoolifyPlugin
|
||||||
from plugins.directus.plugin import DirectusPlugin
|
from plugins.directus.plugin import DirectusPlugin
|
||||||
from plugins.gitea.plugin import GiteaPlugin
|
from plugins.gitea.plugin import GiteaPlugin
|
||||||
from plugins.n8n.plugin import N8nPlugin
|
from plugins.n8n.plugin import N8nPlugin
|
||||||
@@ -33,6 +34,7 @@ registry.register("supabase", SupabasePlugin)
|
|||||||
registry.register("openpanel", OpenPanelPlugin)
|
registry.register("openpanel", OpenPanelPlugin)
|
||||||
registry.register("appwrite", AppwritePlugin)
|
registry.register("appwrite", AppwritePlugin)
|
||||||
registry.register("directus", DirectusPlugin)
|
registry.register("directus", DirectusPlugin)
|
||||||
|
registry.register("coolify", CoolifyPlugin)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BasePlugin",
|
"BasePlugin",
|
||||||
@@ -47,4 +49,5 @@ __all__ = [
|
|||||||
"OpenPanelPlugin",
|
"OpenPanelPlugin",
|
||||||
"AppwritePlugin",
|
"AppwritePlugin",
|
||||||
"DirectusPlugin",
|
"DirectusPlugin",
|
||||||
|
"CoolifyPlugin",
|
||||||
]
|
]
|
||||||
|
|||||||
5
plugins/coolify/__init__.py
Normal file
5
plugins/coolify/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""
|
||||||
|
Coolify Plugin — AI-driven deployment management for Coolify instances.
|
||||||
|
|
||||||
|
F.17: Phase 1 MVP — Applications, Deployments, Servers (~30 tools)
|
||||||
|
"""
|
||||||
269
plugins/coolify/client.py
Normal file
269
plugins/coolify/client.py
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
"""
|
||||||
|
Coolify REST API Client
|
||||||
|
|
||||||
|
Handles all HTTP communication with Coolify REST API.
|
||||||
|
Separates API communication from business logic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
|
||||||
|
class CoolifyClient:
|
||||||
|
"""
|
||||||
|
Coolify REST API client for HTTP communication.
|
||||||
|
|
||||||
|
Handles Bearer token authentication, request formatting,
|
||||||
|
and error handling for all Coolify API v1 endpoints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, site_url: str, token: str):
|
||||||
|
"""
|
||||||
|
Initialize Coolify API client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
site_url: Coolify instance URL (e.g., https://coolify.example.com)
|
||||||
|
token: API token for Bearer authentication
|
||||||
|
"""
|
||||||
|
self.site_url = site_url.rstrip("/")
|
||||||
|
self.api_base = f"{self.site_url}/api/v1"
|
||||||
|
self.token = token
|
||||||
|
self.logger = logging.getLogger(f"CoolifyClient.{site_url}")
|
||||||
|
|
||||||
|
def _get_headers(self) -> dict[str, str]:
|
||||||
|
"""Get request headers with Bearer authentication."""
|
||||||
|
return {
|
||||||
|
"Authorization": f"Bearer {self.token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def request(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
endpoint: str,
|
||||||
|
params: dict | None = None,
|
||||||
|
json_data: dict | None = None,
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Make authenticated request to Coolify REST API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method: HTTP method (GET, POST, PATCH, DELETE)
|
||||||
|
endpoint: API endpoint (without base URL)
|
||||||
|
params: Query parameters
|
||||||
|
json_data: JSON body data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
API response (dict, list, or None)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: On API errors with status code and message
|
||||||
|
"""
|
||||||
|
url = f"{self.api_base}/{endpoint.lstrip('/')}"
|
||||||
|
|
||||||
|
if params:
|
||||||
|
params = {k: v for k, v in params.items() if v is not None}
|
||||||
|
|
||||||
|
if json_data:
|
||||||
|
json_data = {k: v for k, v in json_data.items() if v is not None}
|
||||||
|
|
||||||
|
self.logger.debug(f"{method} {url}")
|
||||||
|
|
||||||
|
async with (
|
||||||
|
aiohttp.ClientSession() as session,
|
||||||
|
session.request(
|
||||||
|
method=method,
|
||||||
|
url=url,
|
||||||
|
params=params,
|
||||||
|
json=json_data,
|
||||||
|
headers=self._get_headers(),
|
||||||
|
) as response,
|
||||||
|
):
|
||||||
|
self.logger.debug(f"Response status: {response.status}")
|
||||||
|
|
||||||
|
if response.status == 204:
|
||||||
|
return {"success": True, "message": "Operation completed successfully"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response_data = await response.json()
|
||||||
|
except Exception:
|
||||||
|
response_text = await response.text()
|
||||||
|
if response.status >= 400:
|
||||||
|
raise Exception(
|
||||||
|
f"Coolify API error (status {response.status}): {response_text}"
|
||||||
|
)
|
||||||
|
return {"success": True, "message": response_text}
|
||||||
|
|
||||||
|
if response.status >= 400:
|
||||||
|
error_msg = response_data.get("message", "Unknown error")
|
||||||
|
raise Exception(f"Coolify API error (status {response.status}): {error_msg}")
|
||||||
|
|
||||||
|
return response_data
|
||||||
|
|
||||||
|
# --- Applications ---
|
||||||
|
|
||||||
|
async def list_applications(self, tag: str | None = None) -> list[dict]:
|
||||||
|
"""List all applications."""
|
||||||
|
params = {"tag": tag} if tag else {}
|
||||||
|
return await self.request("GET", "applications", params=params)
|
||||||
|
|
||||||
|
async def get_application(self, uuid: str) -> dict:
|
||||||
|
"""Get application by UUID."""
|
||||||
|
return await self.request("GET", f"applications/{uuid}")
|
||||||
|
|
||||||
|
async def create_application_public(self, data: dict) -> dict:
|
||||||
|
"""Create application from public repository."""
|
||||||
|
return await self.request("POST", "applications/public", json_data=data)
|
||||||
|
|
||||||
|
async def create_application_dockerfile(self, data: dict) -> dict:
|
||||||
|
"""Create application from Dockerfile."""
|
||||||
|
return await self.request("POST", "applications/dockerfile", json_data=data)
|
||||||
|
|
||||||
|
async def create_application_docker_image(self, data: dict) -> dict:
|
||||||
|
"""Create application from Docker image."""
|
||||||
|
return await self.request("POST", "applications/dockerimage", json_data=data)
|
||||||
|
|
||||||
|
async def create_application_docker_compose(self, data: dict) -> dict:
|
||||||
|
"""Create application from Docker Compose."""
|
||||||
|
return await self.request("POST", "applications/dockercompose", json_data=data)
|
||||||
|
|
||||||
|
async def update_application(self, uuid: str, data: dict) -> dict:
|
||||||
|
"""Update application by UUID."""
|
||||||
|
return await self.request("PATCH", f"applications/{uuid}", json_data=data)
|
||||||
|
|
||||||
|
async def delete_application(
|
||||||
|
self,
|
||||||
|
uuid: str,
|
||||||
|
delete_configurations: bool = True,
|
||||||
|
delete_volumes: bool = True,
|
||||||
|
docker_cleanup: bool = True,
|
||||||
|
delete_connected_networks: bool = True,
|
||||||
|
) -> dict:
|
||||||
|
"""Delete application by UUID."""
|
||||||
|
params = {
|
||||||
|
"delete_configurations": str(delete_configurations).lower(),
|
||||||
|
"delete_volumes": str(delete_volumes).lower(),
|
||||||
|
"docker_cleanup": str(docker_cleanup).lower(),
|
||||||
|
"delete_connected_networks": str(delete_connected_networks).lower(),
|
||||||
|
}
|
||||||
|
return await self.request("DELETE", f"applications/{uuid}", params=params)
|
||||||
|
|
||||||
|
async def start_application(
|
||||||
|
self, uuid: str, force: bool = False, instant_deploy: bool = False
|
||||||
|
) -> dict:
|
||||||
|
"""Deploy/start application."""
|
||||||
|
params = {}
|
||||||
|
if force:
|
||||||
|
params["force"] = "true"
|
||||||
|
if instant_deploy:
|
||||||
|
params["instant_deploy"] = "true"
|
||||||
|
return await self.request("GET", f"applications/{uuid}/start", params=params)
|
||||||
|
|
||||||
|
async def stop_application(self, uuid: str, docker_cleanup: bool = True) -> dict:
|
||||||
|
"""Stop application."""
|
||||||
|
params = {"docker_cleanup": str(docker_cleanup).lower()}
|
||||||
|
return await self.request("GET", f"applications/{uuid}/stop", params=params)
|
||||||
|
|
||||||
|
async def restart_application(self, uuid: str) -> dict:
|
||||||
|
"""Restart application."""
|
||||||
|
return await self.request("GET", f"applications/{uuid}/restart")
|
||||||
|
|
||||||
|
async def get_application_logs(self, uuid: str, lines: int = 100) -> dict:
|
||||||
|
"""Get application logs."""
|
||||||
|
return await self.request("GET", f"applications/{uuid}/logs", params={"lines": lines})
|
||||||
|
|
||||||
|
# --- Application Environment Variables ---
|
||||||
|
|
||||||
|
async def list_application_envs(self, uuid: str) -> list[dict]:
|
||||||
|
"""List application environment variables."""
|
||||||
|
return await self.request("GET", f"applications/{uuid}/envs")
|
||||||
|
|
||||||
|
async def create_application_env(self, uuid: str, data: dict) -> dict:
|
||||||
|
"""Create application environment variable."""
|
||||||
|
return await self.request("POST", f"applications/{uuid}/envs", json_data=data)
|
||||||
|
|
||||||
|
async def update_application_env(self, uuid: str, data: dict) -> dict:
|
||||||
|
"""Update application environment variable."""
|
||||||
|
return await self.request("PATCH", f"applications/{uuid}/envs", json_data=data)
|
||||||
|
|
||||||
|
async def update_application_envs_bulk(self, uuid: str, data: list[dict]) -> dict:
|
||||||
|
"""Bulk update application environment variables."""
|
||||||
|
return await self.request(
|
||||||
|
"PATCH", f"applications/{uuid}/envs/bulk", json_data={"data": data}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def delete_application_env(self, uuid: str, env_uuid: str) -> dict:
|
||||||
|
"""Delete application environment variable."""
|
||||||
|
return await self.request("DELETE", f"applications/{uuid}/envs/{env_uuid}")
|
||||||
|
|
||||||
|
# --- Deployments ---
|
||||||
|
|
||||||
|
async def list_deployments(self) -> list[dict]:
|
||||||
|
"""List running deployments."""
|
||||||
|
return await self.request("GET", "deployments")
|
||||||
|
|
||||||
|
async def get_deployment(self, uuid: str) -> dict:
|
||||||
|
"""Get deployment by UUID."""
|
||||||
|
return await self.request("GET", f"deployments/{uuid}")
|
||||||
|
|
||||||
|
async def cancel_deployment(self, uuid: str) -> dict:
|
||||||
|
"""Cancel a deployment."""
|
||||||
|
return await self.request("POST", f"deployments/{uuid}/cancel")
|
||||||
|
|
||||||
|
async def deploy(
|
||||||
|
self,
|
||||||
|
tag: str | None = None,
|
||||||
|
uuid: str | None = None,
|
||||||
|
force: bool = False,
|
||||||
|
) -> dict:
|
||||||
|
"""Deploy by tag or UUID."""
|
||||||
|
params = {}
|
||||||
|
if tag:
|
||||||
|
params["tag"] = tag
|
||||||
|
if uuid:
|
||||||
|
params["uuid"] = uuid
|
||||||
|
if force:
|
||||||
|
params["force"] = "true"
|
||||||
|
return await self.request("GET", "deploy", params=params)
|
||||||
|
|
||||||
|
async def list_app_deployments(self, uuid: str, skip: int = 0, take: int = 10) -> list[dict]:
|
||||||
|
"""List deployments for a specific application."""
|
||||||
|
params = {"skip": skip, "take": take}
|
||||||
|
return await self.request("GET", f"deployments/applications/{uuid}", params=params)
|
||||||
|
|
||||||
|
# --- Servers ---
|
||||||
|
|
||||||
|
async def list_servers(self) -> list[dict]:
|
||||||
|
"""List all servers."""
|
||||||
|
return await self.request("GET", "servers")
|
||||||
|
|
||||||
|
async def get_server(self, uuid: str) -> dict:
|
||||||
|
"""Get server by UUID."""
|
||||||
|
return await self.request("GET", f"servers/{uuid}")
|
||||||
|
|
||||||
|
async def create_server(self, data: dict) -> dict:
|
||||||
|
"""Create a new server."""
|
||||||
|
return await self.request("POST", "servers", json_data=data)
|
||||||
|
|
||||||
|
async def update_server(self, uuid: str, data: dict) -> dict:
|
||||||
|
"""Update server by UUID."""
|
||||||
|
return await self.request("PATCH", f"servers/{uuid}", json_data=data)
|
||||||
|
|
||||||
|
async def delete_server(self, uuid: str) -> dict:
|
||||||
|
"""Delete server by UUID."""
|
||||||
|
return await self.request("DELETE", f"servers/{uuid}")
|
||||||
|
|
||||||
|
async def get_server_resources(self, uuid: str) -> list[dict]:
|
||||||
|
"""Get resources on a server."""
|
||||||
|
return await self.request("GET", f"servers/{uuid}/resources")
|
||||||
|
|
||||||
|
async def get_server_domains(self, uuid: str) -> list[dict]:
|
||||||
|
"""Get domains configured on a server."""
|
||||||
|
return await self.request("GET", f"servers/{uuid}/domains")
|
||||||
|
|
||||||
|
async def validate_server(self, uuid: str) -> dict:
|
||||||
|
"""Validate server connectivity and configuration."""
|
||||||
|
return await self.request("GET", f"servers/{uuid}/validate")
|
||||||
13
plugins/coolify/handlers/__init__.py
Normal file
13
plugins/coolify/handlers/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
"""
|
||||||
|
Coolify Plugin Handlers
|
||||||
|
|
||||||
|
All tool handlers for Coolify operations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from . import applications, deployments, servers
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"applications",
|
||||||
|
"deployments",
|
||||||
|
"servers",
|
||||||
|
]
|
||||||
860
plugins/coolify/handlers/applications.py
Normal file
860
plugins/coolify/handlers/applications.py
Normal file
@@ -0,0 +1,860 @@
|
|||||||
|
"""Application Handler — manages Coolify applications, lifecycle, and env vars."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from plugins.coolify.client import CoolifyClient
|
||||||
|
|
||||||
|
|
||||||
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||||
|
"""Return tool specifications for ToolGenerator."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "list_applications",
|
||||||
|
"method_name": "list_applications",
|
||||||
|
"description": "List all Coolify applications. Optionally filter by tag name.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tag": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Filter by tag name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_application",
|
||||||
|
"method_name": "get_application",
|
||||||
|
"description": "Get details of a specific Coolify application by UUID.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Application UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_application_public",
|
||||||
|
"method_name": "create_application_public",
|
||||||
|
"description": (
|
||||||
|
"Create a Coolify application from a public Git repository. "
|
||||||
|
"Requires project_uuid, server_uuid, environment, git_repository, "
|
||||||
|
"git_branch, build_pack, and ports_exposes."
|
||||||
|
),
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Project UUID",
|
||||||
|
},
|
||||||
|
"server_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Server UUID",
|
||||||
|
},
|
||||||
|
"environment_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment name (e.g., 'production')",
|
||||||
|
},
|
||||||
|
"git_repository": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Git repository URL",
|
||||||
|
},
|
||||||
|
"git_branch": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Git branch name",
|
||||||
|
},
|
||||||
|
"build_pack": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Build pack type",
|
||||||
|
"enum": ["nixpacks", "static", "dockerfile", "dockercompose"],
|
||||||
|
},
|
||||||
|
"ports_exposes": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Exposed ports (e.g., '3000')",
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Application name",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Application description",
|
||||||
|
},
|
||||||
|
"domains": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Comma-separated domain URLs",
|
||||||
|
},
|
||||||
|
"instant_deploy": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Deploy immediately after creation",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"project_uuid",
|
||||||
|
"server_uuid",
|
||||||
|
"environment_name",
|
||||||
|
"git_repository",
|
||||||
|
"git_branch",
|
||||||
|
"build_pack",
|
||||||
|
"ports_exposes",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_application_dockerfile",
|
||||||
|
"method_name": "create_application_dockerfile",
|
||||||
|
"description": (
|
||||||
|
"Create a Coolify application from a Dockerfile (without git). "
|
||||||
|
"Requires project_uuid, server_uuid, environment, and dockerfile content."
|
||||||
|
),
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Project UUID",
|
||||||
|
},
|
||||||
|
"server_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Server UUID",
|
||||||
|
},
|
||||||
|
"environment_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment name",
|
||||||
|
},
|
||||||
|
"dockerfile": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Dockerfile content",
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Application name",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Application description",
|
||||||
|
},
|
||||||
|
"domains": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Comma-separated domain URLs",
|
||||||
|
},
|
||||||
|
"ports_exposes": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Exposed ports",
|
||||||
|
},
|
||||||
|
"instant_deploy": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Deploy immediately",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"project_uuid",
|
||||||
|
"server_uuid",
|
||||||
|
"environment_name",
|
||||||
|
"dockerfile",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_application_docker_image",
|
||||||
|
"method_name": "create_application_docker_image",
|
||||||
|
"description": (
|
||||||
|
"Create a Coolify application from a Docker image. "
|
||||||
|
"Requires project_uuid, server_uuid, environment, "
|
||||||
|
"docker_registry_image_name, and ports_exposes."
|
||||||
|
),
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Project UUID",
|
||||||
|
},
|
||||||
|
"server_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Server UUID",
|
||||||
|
},
|
||||||
|
"environment_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment name",
|
||||||
|
},
|
||||||
|
"docker_registry_image_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Docker image name (e.g., 'nginx')",
|
||||||
|
},
|
||||||
|
"ports_exposes": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Exposed ports",
|
||||||
|
},
|
||||||
|
"docker_registry_image_tag": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Docker image tag (default: latest)",
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Application name",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Application description",
|
||||||
|
},
|
||||||
|
"domains": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Comma-separated domain URLs",
|
||||||
|
},
|
||||||
|
"instant_deploy": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Deploy immediately",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"project_uuid",
|
||||||
|
"server_uuid",
|
||||||
|
"environment_name",
|
||||||
|
"docker_registry_image_name",
|
||||||
|
"ports_exposes",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_application_compose",
|
||||||
|
"method_name": "create_application_compose",
|
||||||
|
"description": (
|
||||||
|
"Create a Coolify application from Docker Compose. "
|
||||||
|
"Note: Deprecated by Coolify — prefer using services instead."
|
||||||
|
),
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Project UUID",
|
||||||
|
},
|
||||||
|
"server_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Server UUID",
|
||||||
|
},
|
||||||
|
"environment_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment name",
|
||||||
|
},
|
||||||
|
"docker_compose_raw": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Raw Docker Compose YAML content",
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Application name",
|
||||||
|
},
|
||||||
|
"instant_deploy": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Deploy immediately",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"project_uuid",
|
||||||
|
"server_uuid",
|
||||||
|
"environment_name",
|
||||||
|
"docker_compose_raw",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update_application",
|
||||||
|
"method_name": "update_application",
|
||||||
|
"description": (
|
||||||
|
"Update a Coolify application settings. Supports name, description, "
|
||||||
|
"domains, build settings, health checks, resource limits, and more."
|
||||||
|
),
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Application UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Application name",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Application description",
|
||||||
|
},
|
||||||
|
"domains": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Comma-separated domain URLs",
|
||||||
|
},
|
||||||
|
"git_repository": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Git repository URL",
|
||||||
|
},
|
||||||
|
"git_branch": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Git branch",
|
||||||
|
},
|
||||||
|
"build_pack": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"nixpacks",
|
||||||
|
"static",
|
||||||
|
"dockerfile",
|
||||||
|
"dockercompose",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{"type": "null"},
|
||||||
|
],
|
||||||
|
"description": "Build pack type",
|
||||||
|
},
|
||||||
|
"install_command": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Install command",
|
||||||
|
},
|
||||||
|
"build_command": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Build command",
|
||||||
|
},
|
||||||
|
"start_command": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Start command",
|
||||||
|
},
|
||||||
|
"ports_exposes": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Exposed ports",
|
||||||
|
},
|
||||||
|
"is_auto_deploy_enabled": {
|
||||||
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||||
|
"description": "Enable auto-deploy on push",
|
||||||
|
},
|
||||||
|
"instant_deploy": {
|
||||||
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||||
|
"description": "Deploy instantly",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delete_application",
|
||||||
|
"method_name": "delete_application",
|
||||||
|
"description": (
|
||||||
|
"Delete a Coolify application permanently. "
|
||||||
|
"Optionally clean up configs, volumes, and networks."
|
||||||
|
),
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Application UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"delete_configurations": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Delete configurations",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
"delete_volumes": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Delete volumes",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
"docker_cleanup": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Run Docker cleanup",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
"delete_connected_networks": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Delete connected networks",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "admin",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "start_application",
|
||||||
|
"method_name": "start_application",
|
||||||
|
"description": "Deploy/start a Coolify application. Triggers a new deployment.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Application UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"force": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Force rebuild without cache",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
"instant_deploy": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Skip deployment queue",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stop_application",
|
||||||
|
"method_name": "stop_application",
|
||||||
|
"description": "Stop a running Coolify application.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Application UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"docker_cleanup": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Prune networks and volumes",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "restart_application",
|
||||||
|
"method_name": "restart_application",
|
||||||
|
"description": "Restart a Coolify application.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Application UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_application_logs",
|
||||||
|
"method_name": "get_application_logs",
|
||||||
|
"description": "Get logs for a Coolify application.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Application UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"lines": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of log lines to retrieve",
|
||||||
|
"default": 100,
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 10000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "list_application_envs",
|
||||||
|
"method_name": "list_application_envs",
|
||||||
|
"description": "List environment variables for a Coolify application.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Application UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_application_env",
|
||||||
|
"method_name": "create_application_env",
|
||||||
|
"description": "Create an environment variable for a Coolify application.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Application UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment variable key",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment variable value",
|
||||||
|
},
|
||||||
|
"is_preview": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Preview deployment only",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
"is_literal": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Treat as literal (no variable interpolation)",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
"is_multiline": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Allow multiline value",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
"is_shown_once": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Show value only once",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid", "key", "value"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update_application_env",
|
||||||
|
"method_name": "update_application_env",
|
||||||
|
"description": "Update an environment variable for a Coolify application.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Application UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment variable key",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "New value",
|
||||||
|
},
|
||||||
|
"is_preview": {
|
||||||
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||||
|
"description": "Preview deployment only",
|
||||||
|
},
|
||||||
|
"is_literal": {
|
||||||
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||||
|
"description": "Treat as literal",
|
||||||
|
},
|
||||||
|
"is_multiline": {
|
||||||
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||||
|
"description": "Allow multiline",
|
||||||
|
},
|
||||||
|
"is_shown_once": {
|
||||||
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||||
|
"description": "Show only once",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid", "key", "value"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update_application_envs_bulk",
|
||||||
|
"method_name": "update_application_envs_bulk",
|
||||||
|
"description": "Bulk update environment variables for a Coolify application.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Application UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of env var objects with key, value, and flags",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"key": {"type": "string"},
|
||||||
|
"value": {"type": "string"},
|
||||||
|
"is_preview": {"type": "boolean"},
|
||||||
|
"is_literal": {"type": "boolean"},
|
||||||
|
"is_multiline": {"type": "boolean"},
|
||||||
|
"is_shown_once": {"type": "boolean"},
|
||||||
|
},
|
||||||
|
"required": ["key", "value"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid", "data"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delete_application_env",
|
||||||
|
"method_name": "delete_application_env",
|
||||||
|
"description": "Delete an environment variable from a Coolify application.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Application UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"env_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment variable UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid", "env_uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Handler Functions ---
|
||||||
|
|
||||||
|
|
||||||
|
async def list_applications(client: CoolifyClient, tag: str | None = None) -> str:
|
||||||
|
"""List all applications."""
|
||||||
|
apps = await client.list_applications(tag=tag)
|
||||||
|
result = {"success": True, "count": len(apps), "applications": apps}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_application(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Get application details."""
|
||||||
|
app = await client.get_application(uuid)
|
||||||
|
result = {"success": True, "application": app}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_application_public(client: CoolifyClient, **kwargs) -> str:
|
||||||
|
"""Create application from public repository."""
|
||||||
|
app = await client.create_application_public(kwargs)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": "Application created successfully",
|
||||||
|
"application": app,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_application_dockerfile(client: CoolifyClient, **kwargs) -> str:
|
||||||
|
"""Create application from Dockerfile."""
|
||||||
|
app = await client.create_application_dockerfile(kwargs)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": "Application created from Dockerfile",
|
||||||
|
"application": app,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_application_docker_image(client: CoolifyClient, **kwargs) -> str:
|
||||||
|
"""Create application from Docker image."""
|
||||||
|
app = await client.create_application_docker_image(kwargs)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": "Application created from Docker image",
|
||||||
|
"application": app,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_application_compose(client: CoolifyClient, **kwargs) -> str:
|
||||||
|
"""Create application from Docker Compose."""
|
||||||
|
app = await client.create_application_docker_compose(kwargs)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": "Application created from Docker Compose",
|
||||||
|
"application": app,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_application(client: CoolifyClient, uuid: str, **kwargs) -> str:
|
||||||
|
"""Update application settings."""
|
||||||
|
data = {k: v for k, v in kwargs.items() if v is not None and k != "uuid"}
|
||||||
|
app = await client.update_application(uuid, data)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Application '{uuid}' updated successfully",
|
||||||
|
"application": app,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_application(
|
||||||
|
client: CoolifyClient,
|
||||||
|
uuid: str,
|
||||||
|
delete_configurations: bool = True,
|
||||||
|
delete_volumes: bool = True,
|
||||||
|
docker_cleanup: bool = True,
|
||||||
|
delete_connected_networks: bool = True,
|
||||||
|
) -> str:
|
||||||
|
"""Delete an application."""
|
||||||
|
result_data = await client.delete_application(
|
||||||
|
uuid,
|
||||||
|
delete_configurations=delete_configurations,
|
||||||
|
delete_volumes=delete_volumes,
|
||||||
|
docker_cleanup=docker_cleanup,
|
||||||
|
delete_connected_networks=delete_connected_networks,
|
||||||
|
)
|
||||||
|
result = {"success": True, "message": f"Application '{uuid}' deleted", "data": result_data}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def start_application(
|
||||||
|
client: CoolifyClient,
|
||||||
|
uuid: str,
|
||||||
|
force: bool = False,
|
||||||
|
instant_deploy: bool = False,
|
||||||
|
) -> str:
|
||||||
|
"""Deploy/start application."""
|
||||||
|
result_data = await client.start_application(uuid, force=force, instant_deploy=instant_deploy)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Application '{uuid}' deployment queued",
|
||||||
|
"data": result_data,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def stop_application(client: CoolifyClient, uuid: str, docker_cleanup: bool = True) -> str:
|
||||||
|
"""Stop application."""
|
||||||
|
result_data = await client.stop_application(uuid, docker_cleanup=docker_cleanup)
|
||||||
|
result = {"success": True, "message": f"Application '{uuid}' stopping", "data": result_data}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def restart_application(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Restart application."""
|
||||||
|
result_data = await client.restart_application(uuid)
|
||||||
|
result = {"success": True, "message": f"Application '{uuid}' restarting", "data": result_data}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_application_logs(client: CoolifyClient, uuid: str, lines: int = 100) -> str:
|
||||||
|
"""Get application logs."""
|
||||||
|
logs = await client.get_application_logs(uuid, lines=lines)
|
||||||
|
result = {"success": True, "logs": logs}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def list_application_envs(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""List application environment variables."""
|
||||||
|
envs = await client.list_application_envs(uuid)
|
||||||
|
result = {"success": True, "count": len(envs), "envs": envs}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_application_env(
|
||||||
|
client: CoolifyClient,
|
||||||
|
uuid: str,
|
||||||
|
key: str,
|
||||||
|
value: str,
|
||||||
|
is_preview: bool = False,
|
||||||
|
is_literal: bool = False,
|
||||||
|
is_multiline: bool = False,
|
||||||
|
is_shown_once: bool = False,
|
||||||
|
) -> str:
|
||||||
|
"""Create application environment variable."""
|
||||||
|
data = {
|
||||||
|
"key": key,
|
||||||
|
"value": value,
|
||||||
|
"is_preview": is_preview,
|
||||||
|
"is_literal": is_literal,
|
||||||
|
"is_multiline": is_multiline,
|
||||||
|
"is_shown_once": is_shown_once,
|
||||||
|
}
|
||||||
|
result_data = await client.create_application_env(uuid, data)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Env var '{key}' created",
|
||||||
|
"data": result_data,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_application_env(
|
||||||
|
client: CoolifyClient,
|
||||||
|
uuid: str,
|
||||||
|
key: str,
|
||||||
|
value: str,
|
||||||
|
is_preview: bool | None = None,
|
||||||
|
is_literal: bool | None = None,
|
||||||
|
is_multiline: bool | None = None,
|
||||||
|
is_shown_once: bool | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Update application environment variable."""
|
||||||
|
data = {"key": key, "value": value}
|
||||||
|
if is_preview is not None:
|
||||||
|
data["is_preview"] = is_preview
|
||||||
|
if is_literal is not None:
|
||||||
|
data["is_literal"] = is_literal
|
||||||
|
if is_multiline is not None:
|
||||||
|
data["is_multiline"] = is_multiline
|
||||||
|
if is_shown_once is not None:
|
||||||
|
data["is_shown_once"] = is_shown_once
|
||||||
|
result_data = await client.update_application_env(uuid, data)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Env var '{key}' updated",
|
||||||
|
"data": result_data,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_application_envs_bulk(client: CoolifyClient, uuid: str, data: list[dict]) -> str:
|
||||||
|
"""Bulk update application environment variables."""
|
||||||
|
result_data = await client.update_application_envs_bulk(uuid, data)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Bulk update {len(data)} env vars",
|
||||||
|
"data": result_data,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_application_env(client: CoolifyClient, uuid: str, env_uuid: str) -> str:
|
||||||
|
"""Delete application environment variable."""
|
||||||
|
await client.delete_application_env(uuid, env_uuid)
|
||||||
|
result = {"success": True, "message": f"Env var '{env_uuid}' deleted"}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
162
plugins/coolify/handlers/deployments.py
Normal file
162
plugins/coolify/handlers/deployments.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
"""Deployment Handler — manages Coolify deployments."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from plugins.coolify.client import CoolifyClient
|
||||||
|
|
||||||
|
|
||||||
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||||
|
"""Return tool specifications for ToolGenerator."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "list_deployments",
|
||||||
|
"method_name": "list_deployments",
|
||||||
|
"description": "List all running deployments on the Coolify instance.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_deployment",
|
||||||
|
"method_name": "get_deployment",
|
||||||
|
"description": "Get details of a specific deployment by UUID.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Deployment UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cancel_deployment",
|
||||||
|
"method_name": "cancel_deployment",
|
||||||
|
"description": "Cancel a running deployment.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Deployment UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deploy",
|
||||||
|
"method_name": "deploy",
|
||||||
|
"description": (
|
||||||
|
"Trigger deployment by tag name or resource UUID. "
|
||||||
|
"Can deploy multiple resources at once using comma-separated values."
|
||||||
|
),
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tag": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Tag name(s), comma-separated",
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Resource UUID(s), comma-separated",
|
||||||
|
},
|
||||||
|
"force": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Force rebuild without cache",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "list_app_deployments",
|
||||||
|
"method_name": "list_app_deployments",
|
||||||
|
"description": "List deployment history for a specific application.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Application UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"skip": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of deployments to skip",
|
||||||
|
"default": 0,
|
||||||
|
"minimum": 0,
|
||||||
|
},
|
||||||
|
"take": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of deployments to return",
|
||||||
|
"default": 10,
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Handler Functions ---
|
||||||
|
|
||||||
|
|
||||||
|
async def list_deployments(client: CoolifyClient) -> str:
|
||||||
|
"""List running deployments."""
|
||||||
|
deployments = await client.list_deployments()
|
||||||
|
result = {"success": True, "count": len(deployments), "deployments": deployments}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_deployment(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Get deployment details."""
|
||||||
|
deployment = await client.get_deployment(uuid)
|
||||||
|
result = {"success": True, "deployment": deployment}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def cancel_deployment(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Cancel a deployment."""
|
||||||
|
result_data = await client.cancel_deployment(uuid)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Deployment '{uuid}' cancelled",
|
||||||
|
"data": result_data,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def deploy(
|
||||||
|
client: CoolifyClient,
|
||||||
|
tag: str | None = None,
|
||||||
|
uuid: str | None = None,
|
||||||
|
force: bool = False,
|
||||||
|
) -> str:
|
||||||
|
"""Deploy by tag or UUID."""
|
||||||
|
result_data = await client.deploy(tag=tag, uuid=uuid, force=force)
|
||||||
|
result = {"success": True, "message": "Deployment triggered", "data": result_data}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def list_app_deployments(
|
||||||
|
client: CoolifyClient, uuid: str, skip: int = 0, take: int = 10
|
||||||
|
) -> str:
|
||||||
|
"""List deployment history for an application."""
|
||||||
|
deployments = await client.list_app_deployments(uuid, skip=skip, take=take)
|
||||||
|
result = {"success": True, "count": len(deployments), "deployments": deployments}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
286
plugins/coolify/handlers/servers.py
Normal file
286
plugins/coolify/handlers/servers.py
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
"""Server Handler — manages Coolify servers."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from plugins.coolify.client import CoolifyClient
|
||||||
|
|
||||||
|
|
||||||
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||||
|
"""Return tool specifications for ToolGenerator."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "list_servers",
|
||||||
|
"method_name": "list_servers",
|
||||||
|
"description": "List all servers registered in the Coolify instance.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_server",
|
||||||
|
"method_name": "get_server",
|
||||||
|
"description": "Get details of a specific server by UUID, including settings.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Server UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_server",
|
||||||
|
"method_name": "create_server",
|
||||||
|
"description": (
|
||||||
|
"Register a new server in Coolify. "
|
||||||
|
"Requires name, IP, port, user, and a private key UUID for SSH access."
|
||||||
|
),
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Server name",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Server IP address",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "SSH port",
|
||||||
|
"default": 22,
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "SSH user",
|
||||||
|
"default": "root",
|
||||||
|
},
|
||||||
|
"private_key_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Private key UUID for SSH authentication",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Server description",
|
||||||
|
},
|
||||||
|
"is_build_server": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Use as build server",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
"instant_validate": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Validate server immediately after creation",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
"proxy_type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Proxy type for the server",
|
||||||
|
"enum": ["traefik", "caddy", "none"],
|
||||||
|
"default": "traefik",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["name", "ip", "private_key_uuid"],
|
||||||
|
},
|
||||||
|
"scope": "admin",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update_server",
|
||||||
|
"method_name": "update_server",
|
||||||
|
"description": "Update server configuration.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Server UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Server name",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Server description",
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "Server IP address",
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||||
|
"description": "SSH port",
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "SSH user",
|
||||||
|
},
|
||||||
|
"proxy_type": {
|
||||||
|
"anyOf": [
|
||||||
|
{"type": "string", "enum": ["traefik", "caddy", "none"]},
|
||||||
|
{"type": "null"},
|
||||||
|
],
|
||||||
|
"description": "Proxy type",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delete_server",
|
||||||
|
"method_name": "delete_server",
|
||||||
|
"description": "Delete a server from Coolify. This cannot be undone!",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Server UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "admin",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_server_resources",
|
||||||
|
"method_name": "get_server_resources",
|
||||||
|
"description": (
|
||||||
|
"Get all resources (applications, databases, services) "
|
||||||
|
"deployed on a specific server."
|
||||||
|
),
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Server UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_server_domains",
|
||||||
|
"method_name": "get_server_domains",
|
||||||
|
"description": "Get all domains configured on a specific server.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Server UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "validate_server",
|
||||||
|
"method_name": "validate_server",
|
||||||
|
"description": "Validate server connectivity and configuration.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Server UUID",
|
||||||
|
"minLength": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["uuid"],
|
||||||
|
},
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Handler Functions ---
|
||||||
|
|
||||||
|
|
||||||
|
async def list_servers(client: CoolifyClient) -> str:
|
||||||
|
"""List all servers."""
|
||||||
|
servers = await client.list_servers()
|
||||||
|
result = {"success": True, "count": len(servers), "servers": servers}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_server(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Get server details."""
|
||||||
|
server = await client.get_server(uuid)
|
||||||
|
result = {"success": True, "server": server}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_server(client: CoolifyClient, **kwargs) -> str:
|
||||||
|
"""Create a new server."""
|
||||||
|
server = await client.create_server(kwargs)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": "Server created successfully",
|
||||||
|
"server": server,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_server(client: CoolifyClient, uuid: str, **kwargs) -> str:
|
||||||
|
"""Update server configuration."""
|
||||||
|
data = {k: v for k, v in kwargs.items() if v is not None and k != "uuid"}
|
||||||
|
server = await client.update_server(uuid, data)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Server '{uuid}' updated",
|
||||||
|
"server": server,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_server(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Delete a server."""
|
||||||
|
await client.delete_server(uuid)
|
||||||
|
result = {"success": True, "message": f"Server '{uuid}' deleted"}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_server_resources(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Get server resources."""
|
||||||
|
resources = await client.get_server_resources(uuid)
|
||||||
|
result = {"success": True, "count": len(resources), "resources": resources}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_server_domains(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Get server domains."""
|
||||||
|
domains = await client.get_server_domains(uuid)
|
||||||
|
result = {"success": True, "domains": domains}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_server(client: CoolifyClient, uuid: str) -> str:
|
||||||
|
"""Validate server."""
|
||||||
|
result_data = await client.validate_server(uuid)
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"message": "Server validation started",
|
||||||
|
"data": result_data,
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
106
plugins/coolify/plugin.py
Normal file
106
plugins/coolify/plugin.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"""
|
||||||
|
Coolify Plugin — Clean Architecture
|
||||||
|
|
||||||
|
AI-driven deployment management for Coolify instances.
|
||||||
|
Modular handlers for applications, deployments, and servers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from plugins.base import BasePlugin
|
||||||
|
from plugins.coolify import handlers
|
||||||
|
from plugins.coolify.client import CoolifyClient
|
||||||
|
|
||||||
|
|
||||||
|
class CoolifyPlugin(BasePlugin):
|
||||||
|
"""
|
||||||
|
Coolify project plugin — Clean architecture.
|
||||||
|
|
||||||
|
Provides Coolify deployment management capabilities including:
|
||||||
|
- Application management (CRUD, lifecycle, env vars, logs)
|
||||||
|
- Deployment control (list, cancel, deploy by tag/UUID)
|
||||||
|
- Server management (CRUD, resources, domains, validation)
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_plugin_name() -> str:
|
||||||
|
"""Return plugin type identifier."""
|
||||||
|
return "coolify"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_required_config_keys() -> list[str]:
|
||||||
|
"""Return required configuration keys."""
|
||||||
|
return ["url", "token"]
|
||||||
|
|
||||||
|
def __init__(self, config: dict[str, Any], project_id: str | None = None):
|
||||||
|
"""
|
||||||
|
Initialize Coolify plugin with handlers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Configuration dictionary containing:
|
||||||
|
- url: Coolify instance URL
|
||||||
|
- token: API token for Bearer authentication
|
||||||
|
project_id: Optional project ID (auto-generated if not provided)
|
||||||
|
"""
|
||||||
|
super().__init__(config, project_id=project_id)
|
||||||
|
|
||||||
|
self.client = CoolifyClient(
|
||||||
|
site_url=config["url"],
|
||||||
|
token=config["token"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Return all tool specifications for ToolGenerator.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of tool specification dictionaries
|
||||||
|
"""
|
||||||
|
specs = []
|
||||||
|
|
||||||
|
specs.extend(handlers.applications.get_tool_specifications())
|
||||||
|
specs.extend(handlers.deployments.get_tool_specifications())
|
||||||
|
specs.extend(handlers.servers.get_tool_specifications())
|
||||||
|
|
||||||
|
return specs
|
||||||
|
|
||||||
|
def __getattr__(self, name: str):
|
||||||
|
"""
|
||||||
|
Dynamically delegate method calls to appropriate handlers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Method name being called
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Handler function from the appropriate handler module
|
||||||
|
"""
|
||||||
|
handler_modules = [
|
||||||
|
handlers.applications,
|
||||||
|
handlers.deployments,
|
||||||
|
handlers.servers,
|
||||||
|
]
|
||||||
|
|
||||||
|
for module in handler_modules:
|
||||||
|
if hasattr(module, name):
|
||||||
|
func = getattr(module, name)
|
||||||
|
|
||||||
|
async def wrapper(_func=func, **kwargs):
|
||||||
|
return await _func(self.client, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
|
||||||
|
|
||||||
|
async def health_check(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Check if Coolify instance is accessible.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict containing health check result
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await self.client.request("GET", "version")
|
||||||
|
return {"healthy": True, "message": "Coolify instance is accessible"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"healthy": False, "message": f"Coolify health check failed: {str(e)}"}
|
||||||
1
plugins/coolify/schemas/__init__.py
Normal file
1
plugins/coolify/schemas/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Coolify Plugin Schemas"""
|
||||||
55
plugins/coolify/schemas/common.py
Normal file
55
plugins/coolify/schemas/common.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"""
|
||||||
|
Common Pydantic Schemas for Coolify Plugin
|
||||||
|
|
||||||
|
Shared validation schemas used across Coolify handlers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class Site(BaseModel):
|
||||||
|
"""Coolify site configuration."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
site_id: str = Field(..., description="Site identifier")
|
||||||
|
url: str = Field(..., description="Coolify instance URL")
|
||||||
|
token: str = Field(..., description="API token for Bearer authentication")
|
||||||
|
alias: str | None = Field(None, description="Site alias")
|
||||||
|
|
||||||
|
|
||||||
|
class PaginationParams(BaseModel):
|
||||||
|
"""Pagination parameters for list endpoints."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
skip: int = Field(default=0, ge=0, description="Number of items to skip")
|
||||||
|
take: int = Field(default=10, ge=1, le=100, description="Number of items to take")
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorResponse(BaseModel):
|
||||||
|
"""Standard error response."""
|
||||||
|
|
||||||
|
error: bool = Field(default=True)
|
||||||
|
message: str = Field(..., description="Error message")
|
||||||
|
details: dict[str, Any] | None = Field(None, description="Additional error details")
|
||||||
|
|
||||||
|
|
||||||
|
class SuccessResponse(BaseModel):
|
||||||
|
"""Standard success response."""
|
||||||
|
|
||||||
|
success: bool = Field(default=True)
|
||||||
|
message: str = Field(..., description="Success message")
|
||||||
|
data: dict[str, Any] | None = Field(None, description="Response data")
|
||||||
|
|
||||||
|
|
||||||
|
class CoolifyTimestamps(BaseModel):
|
||||||
|
"""Common timestamp fields."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
created_at: datetime | None = None
|
||||||
|
updated_at: datetime | None = None
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "mcphub-server"
|
name = "mcphub-server"
|
||||||
version = "3.6.0"
|
version = "3.7.0"
|
||||||
description = "AI-native management hub for WordPress, WooCommerce, and self-hosted services via Model Context Protocol (MCP)"
|
description = "AI-native management hub for WordPress, WooCommerce, and self-hosted services via Model Context Protocol (MCP)"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "MCP Hub", email = "contact@mcphub.dev"}
|
{name = "MCP Hub", email = "contact@mcphub.dev"}
|
||||||
|
|||||||
18
server.py
18
server.py
@@ -1885,6 +1885,24 @@ def register_project_tools():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to generate Directus tools: {e}", exc_info=True)
|
logger.error(f"Failed to generate Directus tools: {e}", exc_info=True)
|
||||||
|
|
||||||
|
# Generate tools for Coolify (Phase F.17 - Deployment Management)
|
||||||
|
logger.info("Generating Coolify tools from plugin specifications...")
|
||||||
|
try:
|
||||||
|
from plugins.coolify.plugin import CoolifyPlugin
|
||||||
|
|
||||||
|
coolify_tools = tool_generator.generate_tools(CoolifyPlugin, "coolify")
|
||||||
|
logger.info(f"Generated {len(coolify_tools)} Coolify tools from ToolGenerator")
|
||||||
|
|
||||||
|
# Register Coolify tools in ToolRegistry
|
||||||
|
for tool_def in coolify_tools:
|
||||||
|
try:
|
||||||
|
tool_registry.register(tool_def)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to register Coolify tool {tool_def.name}: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to generate Coolify tools: {e}", exc_info=True)
|
||||||
|
|
||||||
logger.info(f"Registered {tool_registry.get_count()} tools in ToolRegistry")
|
logger.info(f"Registered {tool_registry.get_count()} tools in ToolRegistry")
|
||||||
|
|
||||||
# Register tools with FastMCP
|
# Register tools with FastMCP
|
||||||
|
|||||||
452
tests/test_coolify.py
Normal file
452
tests/test_coolify.py
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
"""
|
||||||
|
Tests for Coolify MCP Plugin
|
||||||
|
|
||||||
|
Unit tests with mocked HTTP responses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from plugins.coolify.client import CoolifyClient
|
||||||
|
from plugins.coolify.handlers import applications, deployments, servers
|
||||||
|
from plugins.coolify.plugin import CoolifyPlugin
|
||||||
|
|
||||||
|
# === Fixtures ===
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
"""Create a CoolifyClient instance for testing."""
|
||||||
|
return CoolifyClient(site_url="https://coolify.test.com", token="test-token-123")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def plugin():
|
||||||
|
"""Create a CoolifyPlugin instance for testing."""
|
||||||
|
config = {"url": "https://coolify.test.com", "token": "test-token-123"}
|
||||||
|
return CoolifyPlugin(config, project_id="coolify_test")
|
||||||
|
|
||||||
|
|
||||||
|
# === Client Tests ===
|
||||||
|
|
||||||
|
|
||||||
|
class TestCoolifyClient:
|
||||||
|
"""Tests for CoolifyClient."""
|
||||||
|
|
||||||
|
def test_init(self, client):
|
||||||
|
"""Test client initialization."""
|
||||||
|
assert client.api_base == "https://coolify.test.com/api/v1"
|
||||||
|
assert client.token == "test-token-123"
|
||||||
|
|
||||||
|
def test_headers(self, client):
|
||||||
|
"""Test Bearer token authentication headers."""
|
||||||
|
headers = client._get_headers()
|
||||||
|
assert headers["Authorization"] == "Bearer test-token-123"
|
||||||
|
assert headers["Content-Type"] == "application/json"
|
||||||
|
assert headers["Accept"] == "application/json"
|
||||||
|
|
||||||
|
def test_url_trailing_slash(self):
|
||||||
|
"""Test URL normalization."""
|
||||||
|
client = CoolifyClient(site_url="https://coolify.test.com/", token="t")
|
||||||
|
assert client.api_base == "https://coolify.test.com/api/v1"
|
||||||
|
|
||||||
|
|
||||||
|
# === Plugin Tests ===
|
||||||
|
|
||||||
|
|
||||||
|
class TestCoolifyPlugin:
|
||||||
|
"""Tests for CoolifyPlugin."""
|
||||||
|
|
||||||
|
def test_plugin_name(self):
|
||||||
|
"""Test plugin name."""
|
||||||
|
assert CoolifyPlugin.get_plugin_name() == "coolify"
|
||||||
|
|
||||||
|
def test_required_config_keys(self):
|
||||||
|
"""Test required config keys."""
|
||||||
|
assert CoolifyPlugin.get_required_config_keys() == ["url", "token"]
|
||||||
|
|
||||||
|
def test_missing_config_raises(self):
|
||||||
|
"""Test that missing config raises ValueError."""
|
||||||
|
with pytest.raises(ValueError, match="Missing required"):
|
||||||
|
CoolifyPlugin({"url": "https://coolify.test.com"})
|
||||||
|
|
||||||
|
def test_tool_specifications(self):
|
||||||
|
"""Test that tool specifications are returned."""
|
||||||
|
specs = CoolifyPlugin.get_tool_specifications()
|
||||||
|
assert len(specs) == 30 # 17 apps + 5 deployments + 8 servers
|
||||||
|
|
||||||
|
# Check all specs have required fields
|
||||||
|
for spec in specs:
|
||||||
|
assert "name" in spec
|
||||||
|
assert "method_name" in spec
|
||||||
|
assert "description" in spec
|
||||||
|
assert "schema" in spec
|
||||||
|
assert "scope" in spec
|
||||||
|
assert spec["scope"] in ("read", "write", "admin")
|
||||||
|
|
||||||
|
def test_tool_names_unique(self):
|
||||||
|
"""Test that all tool names are unique."""
|
||||||
|
specs = CoolifyPlugin.get_tool_specifications()
|
||||||
|
names = [s["name"] for s in specs]
|
||||||
|
assert len(names) == len(set(names))
|
||||||
|
|
||||||
|
def test_plugin_init(self, plugin):
|
||||||
|
"""Test plugin initialization."""
|
||||||
|
assert plugin.client is not None
|
||||||
|
assert plugin.client.token == "test-token-123"
|
||||||
|
|
||||||
|
|
||||||
|
# === Application Handler Tests ===
|
||||||
|
|
||||||
|
|
||||||
|
class TestApplicationHandlers:
|
||||||
|
"""Tests for application handler functions."""
|
||||||
|
|
||||||
|
def test_app_tool_count(self):
|
||||||
|
"""Test application tool specification count."""
|
||||||
|
specs = applications.get_tool_specifications()
|
||||||
|
assert len(specs) == 17
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_applications(self, client):
|
||||||
|
"""Test list_applications handler."""
|
||||||
|
mock_apps = [
|
||||||
|
{"uuid": "app-1", "name": "test-app", "status": "running"},
|
||||||
|
{"uuid": "app-2", "name": "test-app-2", "status": "stopped"},
|
||||||
|
]
|
||||||
|
client.list_applications = AsyncMock(return_value=mock_apps)
|
||||||
|
|
||||||
|
result = await applications.list_applications(client)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["count"] == 2
|
||||||
|
assert len(data["applications"]) == 2
|
||||||
|
client.list_applications.assert_called_once_with(tag=None)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_application(self, client):
|
||||||
|
"""Test get_application handler."""
|
||||||
|
mock_app = {"uuid": "app-1", "name": "mcphub", "status": "running", "fqdn": "mcp.test.com"}
|
||||||
|
client.get_application = AsyncMock(return_value=mock_app)
|
||||||
|
|
||||||
|
result = await applications.get_application(client, uuid="app-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["application"]["name"] == "mcphub"
|
||||||
|
client.get_application.assert_called_once_with("app-1")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_application(self, client):
|
||||||
|
"""Test start_application handler."""
|
||||||
|
mock_response = {"message": "Deployment request queued.", "deployment_uuid": "dep-123"}
|
||||||
|
client.start_application = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await applications.start_application(client, uuid="app-1", force=True)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "deployment queued" in data["message"]
|
||||||
|
client.start_application.assert_called_once_with("app-1", force=True, instant_deploy=False)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_stop_application(self, client):
|
||||||
|
"""Test stop_application handler."""
|
||||||
|
mock_response = {"message": "Application stopping request queued."}
|
||||||
|
client.stop_application = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await applications.stop_application(client, uuid="app-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.stop_application.assert_called_once_with("app-1", docker_cleanup=True)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_restart_application(self, client):
|
||||||
|
"""Test restart_application handler."""
|
||||||
|
mock_response = {"message": "Restart request queued.", "deployment_uuid": "dep-456"}
|
||||||
|
client.restart_application = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await applications.restart_application(client, uuid="app-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.restart_application.assert_called_once_with("app-1")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_application_logs(self, client):
|
||||||
|
"""Test get_application_logs handler."""
|
||||||
|
mock_logs = {"logs": "2026-04-02 Starting application..."}
|
||||||
|
client.get_application_logs = AsyncMock(return_value=mock_logs)
|
||||||
|
|
||||||
|
result = await applications.get_application_logs(client, uuid="app-1", lines=50)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.get_application_logs.assert_called_once_with("app-1", lines=50)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_application_envs(self, client):
|
||||||
|
"""Test list_application_envs handler."""
|
||||||
|
mock_envs = [
|
||||||
|
{"key": "DATABASE_URL", "value": "postgres://..."},
|
||||||
|
{"key": "SECRET_KEY", "value": "***"},
|
||||||
|
]
|
||||||
|
client.list_application_envs = AsyncMock(return_value=mock_envs)
|
||||||
|
|
||||||
|
result = await applications.list_application_envs(client, uuid="app-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["count"] == 2
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_application_env(self, client):
|
||||||
|
"""Test create_application_env handler."""
|
||||||
|
mock_response = {"uuid": "env-123"}
|
||||||
|
client.create_application_env = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await applications.create_application_env(
|
||||||
|
client, uuid="app-1", key="NEW_VAR", value="test_value"
|
||||||
|
)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "NEW_VAR" in data["message"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_application_env(self, client):
|
||||||
|
"""Test delete_application_env handler."""
|
||||||
|
client.delete_application_env = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
result = await applications.delete_application_env(client, uuid="app-1", env_uuid="env-123")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.delete_application_env.assert_called_once_with("app-1", "env-123")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_application_public(self, client):
|
||||||
|
"""Test create_application_public handler."""
|
||||||
|
mock_response = {"uuid": "new-app-uuid"}
|
||||||
|
client.create_application_public = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await applications.create_application_public(
|
||||||
|
client,
|
||||||
|
project_uuid="proj-1",
|
||||||
|
server_uuid="srv-1",
|
||||||
|
environment_name="production",
|
||||||
|
git_repository="https://github.com/test/repo",
|
||||||
|
git_branch="main",
|
||||||
|
build_pack="nixpacks",
|
||||||
|
ports_exposes="3000",
|
||||||
|
)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["application"]["uuid"] == "new-app-uuid"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_application(self, client):
|
||||||
|
"""Test update_application handler."""
|
||||||
|
mock_response = {"uuid": "app-1"}
|
||||||
|
client.update_application = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await applications.update_application(
|
||||||
|
client, uuid="app-1", name="new-name", domains="app.test.com"
|
||||||
|
)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.update_application.assert_called_once_with(
|
||||||
|
"app-1", {"name": "new-name", "domains": "app.test.com"}
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_application(self, client):
|
||||||
|
"""Test delete_application handler."""
|
||||||
|
mock_response = {"message": "Application deleted."}
|
||||||
|
client.delete_application = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await applications.delete_application(client, uuid="app-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
|
||||||
|
|
||||||
|
# === Deployment Handler Tests ===
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeploymentHandlers:
|
||||||
|
"""Tests for deployment handler functions."""
|
||||||
|
|
||||||
|
def test_deployment_tool_count(self):
|
||||||
|
"""Test deployment tool specification count."""
|
||||||
|
specs = deployments.get_tool_specifications()
|
||||||
|
assert len(specs) == 5
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_deployments(self, client):
|
||||||
|
"""Test list_deployments handler."""
|
||||||
|
mock_deps = [
|
||||||
|
{"deployment_uuid": "dep-1", "status": "in_progress"},
|
||||||
|
{"deployment_uuid": "dep-2", "status": "queued"},
|
||||||
|
]
|
||||||
|
client.list_deployments = AsyncMock(return_value=mock_deps)
|
||||||
|
|
||||||
|
result = await deployments.list_deployments(client)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["count"] == 2
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_deploy(self, client):
|
||||||
|
"""Test deploy handler."""
|
||||||
|
mock_response = {
|
||||||
|
"deployments": [
|
||||||
|
{"message": "Deploying", "resource_uuid": "app-1", "deployment_uuid": "dep-1"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
client.deploy = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await deployments.deploy(client, uuid="app-1", force=True)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.deploy.assert_called_once_with(tag=None, uuid="app-1", force=True)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cancel_deployment(self, client):
|
||||||
|
"""Test cancel_deployment handler."""
|
||||||
|
mock_response = {"message": "Cancelled", "status": "cancelled-by-user"}
|
||||||
|
client.cancel_deployment = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await deployments.cancel_deployment(client, uuid="dep-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
client.cancel_deployment.assert_called_once_with("dep-1")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_app_deployments(self, client):
|
||||||
|
"""Test list_app_deployments handler."""
|
||||||
|
mock_deps = [{"deployment_uuid": "dep-1", "status": "finished"}]
|
||||||
|
client.list_app_deployments = AsyncMock(return_value=mock_deps)
|
||||||
|
|
||||||
|
result = await deployments.list_app_deployments(client, uuid="app-1", skip=0, take=5)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["count"] == 1
|
||||||
|
client.list_app_deployments.assert_called_once_with("app-1", skip=0, take=5)
|
||||||
|
|
||||||
|
|
||||||
|
# === Server Handler Tests ===
|
||||||
|
|
||||||
|
|
||||||
|
class TestServerHandlers:
|
||||||
|
"""Tests for server handler functions."""
|
||||||
|
|
||||||
|
def test_server_tool_count(self):
|
||||||
|
"""Test server tool specification count."""
|
||||||
|
specs = servers.get_tool_specifications()
|
||||||
|
assert len(specs) == 8
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_servers(self, client):
|
||||||
|
"""Test list_servers handler."""
|
||||||
|
mock_servers = [
|
||||||
|
{"uuid": "srv-1", "name": "main-server", "ip": "1.2.3.4"},
|
||||||
|
]
|
||||||
|
client.list_servers = AsyncMock(return_value=mock_servers)
|
||||||
|
|
||||||
|
result = await servers.list_servers(client)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["count"] == 1
|
||||||
|
assert data["servers"][0]["name"] == "main-server"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_server(self, client):
|
||||||
|
"""Test get_server handler."""
|
||||||
|
mock_server = {
|
||||||
|
"uuid": "srv-1",
|
||||||
|
"name": "main-server",
|
||||||
|
"ip": "1.2.3.4",
|
||||||
|
"settings": {"is_reachable": True},
|
||||||
|
}
|
||||||
|
client.get_server = AsyncMock(return_value=mock_server)
|
||||||
|
|
||||||
|
result = await servers.get_server(client, uuid="srv-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["server"]["settings"]["is_reachable"] is True
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_server_resources(self, client):
|
||||||
|
"""Test get_server_resources handler."""
|
||||||
|
mock_resources = [
|
||||||
|
{"uuid": "app-1", "name": "mcphub", "type": "application", "status": "running"},
|
||||||
|
{"uuid": "db-1", "name": "postgres", "type": "database", "status": "running"},
|
||||||
|
]
|
||||||
|
client.get_server_resources = AsyncMock(return_value=mock_resources)
|
||||||
|
|
||||||
|
result = await servers.get_server_resources(client, uuid="srv-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["count"] == 2
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_server_domains(self, client):
|
||||||
|
"""Test get_server_domains handler."""
|
||||||
|
mock_domains = [{"ip": "1.2.3.4", "domains": ["mcp.test.com", "blog.test.com"]}]
|
||||||
|
client.get_server_domains = AsyncMock(return_value=mock_domains)
|
||||||
|
|
||||||
|
result = await servers.get_server_domains(client, uuid="srv-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert len(data["domains"]) == 1
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_server(self, client):
|
||||||
|
"""Test validate_server handler."""
|
||||||
|
mock_response = {"message": "Validation started."}
|
||||||
|
client.validate_server = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
result = await servers.validate_server(client, uuid="srv-1")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "validation started" in data["message"].lower()
|
||||||
|
|
||||||
|
|
||||||
|
# === Health Check Tests ===
|
||||||
|
|
||||||
|
|
||||||
|
class TestHealthCheck:
|
||||||
|
"""Tests for plugin health check."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_health_check_success(self, plugin):
|
||||||
|
"""Test successful health check."""
|
||||||
|
plugin.client.request = AsyncMock(return_value={"version": "4.0.0"})
|
||||||
|
|
||||||
|
result = await plugin.health_check()
|
||||||
|
assert result["healthy"] is True
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_health_check_failure(self, plugin):
|
||||||
|
"""Test failed health check."""
|
||||||
|
plugin.client.request = AsyncMock(side_effect=Exception("Connection refused"))
|
||||||
|
|
||||||
|
result = await plugin.health_check()
|
||||||
|
assert result["healthy"] is False
|
||||||
|
assert "Connection refused" in result["message"]
|
||||||
Reference in New Issue
Block a user