Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 151ba8e6e6 | |||
| bf893a743a | |||
| 2c9d4ed3d2 | |||
| 4f0c2f9bb0 | |||
| d79c2fd94a |
18
CHANGELOG.md
18
CHANGELOG.md
@@ -5,6 +5,24 @@ All notable changes to MCP Hub will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [3.6.0] — 2026-04-02
|
||||
|
||||
### Gitea Plugin — Public Release (Track F.16)
|
||||
|
||||
Gitea plugin fully reviewed, tested, and published for public use. Two missing tools added, comprehensive test suite created.
|
||||
|
||||
#### Added
|
||||
- **`update_webhook` tool**: Update existing webhook configuration, events, and active status (admin scope)
|
||||
- **`delete_label` tool**: Delete labels from repositories (write scope)
|
||||
- **`tests/test_gitea_plugin.py`**: Comprehensive test suite — 85+ tests covering client init, headers, tool specs, all 5 handler groups, health check, and plugin delegation
|
||||
|
||||
#### Changed
|
||||
- **Gitea plugin now public**: Added `gitea` to `DEFAULT_PUBLIC_PLUGINS` — available to all OAuth users
|
||||
- **Tool count**: 565 → 567 (added update_webhook + delete_label)
|
||||
- **env.example**: Updated default `ENABLED_PLUGINS` to include `gitea`
|
||||
|
||||
---
|
||||
|
||||
## [3.5.0] — 2026-04-02
|
||||
|
||||
### FastMCP Upgrade & Legacy Cleanup (Track F.15)
|
||||
|
||||
10
CLAUDE.md
10
CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## 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 565 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
|
||||
|
||||
@@ -116,7 +116,7 @@ plugins/{name}/
|
||||
└── 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`.
|
||||
|
||||
@@ -185,14 +185,14 @@ Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
|
||||
|
||||
v4 development cycle with 15 phases. See `docs/ROADMAP.md` (Track F) and `.claude/plans/structured-popping-adleman.md` for full plan.
|
||||
|
||||
**Active plugins for public users**: WordPress, WooCommerce, Supabase, OpenPanel (configurable via `ENABLED_PLUGINS` env var)
|
||||
**Active plugins for public users**: WordPress, WooCommerce, Supabase, OpenPanel, Gitea (configurable via `ENABLED_PLUGINS` env var)
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Test files exist in both `tests/` (proper) and root directory (legacy `test_*.py`). Run `pytest tests/` for organized tests only.
|
||||
- `wordpress-plugin/` contains companion WP plugins (openpanel, airano-mcp-seo-bridge) — these are PHP, not Python
|
||||
- `env.example` has "FUTURE" labels for Supabase/Gitea but both are fully implemented
|
||||
- 4 plugins are tested for public use: WordPress, WooCommerce, Supabase, OpenPanel. Others are admin-only or disabled.
|
||||
- 5 plugins are tested for public use: WordPress, WooCommerce, Supabase, OpenPanel, Gitea. Others are admin-only or disabled.
|
||||
- OAuth Clients (Client ID/Secret) are for MCP endpoint auth, NOT the same as GitHub/Google dashboard login
|
||||
- All sites stored in SQLite (`core/database.py`), managed via web dashboard
|
||||
- Dashboard templates live in `core/templates/` (included in pip package as `package_data`)
|
||||
@@ -208,4 +208,4 @@ v4 development cycle with 15 phases. See `docs/ROADMAP.md` (Track F) and `.claud
|
||||
- Required env vars: `MASTER_API_KEY`, `OAUTH_JWT_SECRET_KEY`, `OAUTH_BASE_URL`
|
||||
- OAuth env vars: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `PUBLIC_URL`
|
||||
- User limits: `MAX_SITES_PER_USER=10`, `USER_RATE_LIMIT_PER_MIN=30`, `USER_RATE_LIMIT_PER_HR=500`
|
||||
- Plugin visibility: `ENABLED_PLUGINS=wordpress,woocommerce,supabase,openpanel` (default)
|
||||
- Plugin visibility: `ENABLED_PLUGINS=wordpress,woocommerce,supabase,openpanel,gitea` (default)
|
||||
|
||||
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://pypi.org/project/mcphub-server/)
|
||||
[](https://hub.docker.com/r/airano/mcphub)
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
[](https://github.com/airano-ir/mcphub/actions/workflows/ci.yml)
|
||||
|
||||
</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.
|
||||
|
||||
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"*
|
||||
>
|
||||
@@ -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 |
|
||||
| **Appwrite** | 100 | Databases, auth, storage, functions, teams, messaging |
|
||||
| **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 |
|
||||
| **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 |
|
||||
| Appwrite | URL, API Key, Project ID | Appwrite Console → Settings → API Keys |
|
||||
| Directus | URL, Static Token | Directus Admin → Settings |
|
||||
| Coolify | URL, API Token | Coolify → Keys & Tokens → API tokens |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -276,11 +278,12 @@ MCP Hub supports **Open Dynamic Client Registration** (RFC 7591). ChatGPT can au
|
||||
/openpanel/mcp → OpenPanel tools (42 tools)
|
||||
/appwrite/mcp → Appwrite tools (100 tools)
|
||||
/directus/mcp → Directus tools (100 tools)
|
||||
/coolify/mcp → Coolify tools (30 tools)
|
||||
/project/{alias}/mcp → Per-project endpoint (auto-injects site)
|
||||
/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 |
|
||||
|----------|----------|------:|
|
||||
|
||||
@@ -850,7 +850,12 @@ async def get_all_projects(
|
||||
is_admin = False
|
||||
current_user_id = None
|
||||
if user_session:
|
||||
if hasattr(user_session, "user_type") and user_session.user_type == "master" or isinstance(user_session, dict) and user_session.get("role") == "admin":
|
||||
if (
|
||||
hasattr(user_session, "user_type")
|
||||
and user_session.user_type == "master"
|
||||
or isinstance(user_session, dict)
|
||||
and user_session.get("role") == "admin"
|
||||
):
|
||||
is_admin = True
|
||||
elif isinstance(user_session, dict) and "user_id" in user_session:
|
||||
current_user_id = user_session["user_id"]
|
||||
|
||||
@@ -8,14 +8,14 @@ Usage:
|
||||
from core.plugin_visibility import get_public_plugin_types, is_plugin_public
|
||||
|
||||
public_types = get_public_plugin_types() # {"wordpress", "woocommerce", "supabase"}
|
||||
if is_plugin_public("gitea"): # False
|
||||
if is_plugin_public("gitea"): # True
|
||||
...
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Default plugins available to public (OAuth) users
|
||||
DEFAULT_PUBLIC_PLUGINS = {"wordpress", "woocommerce", "supabase", "openpanel"}
|
||||
DEFAULT_PUBLIC_PLUGINS = {"wordpress", "woocommerce", "supabase", "openpanel", "gitea"}
|
||||
|
||||
|
||||
def _parse_plugins(val: str) -> set[str]:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Usage:
|
||||
from core.settings import get_setting
|
||||
|
||||
enabled = await get_setting("ENABLED_PLUGINS", "wordpress,woocommerce,supabase,openpanel")
|
||||
enabled = await get_setting("ENABLED_PLUGINS", "wordpress,woocommerce,supabase,openpanel,gitea")
|
||||
max_sites = int(await get_setting("MAX_SITES_PER_USER", "10"))
|
||||
"""
|
||||
|
||||
@@ -17,7 +17,7 @@ _cached_plugins: set[str] | None = None
|
||||
|
||||
# Default values for all managed settings
|
||||
SETTING_DEFAULTS: dict[str, str] = {
|
||||
"ENABLED_PLUGINS": "wordpress,woocommerce,supabase,openpanel",
|
||||
"ENABLED_PLUGINS": "wordpress,woocommerce,supabase,openpanel,gitea",
|
||||
"MAX_SITES_PER_USER": "10",
|
||||
"USER_RATE_LIMIT_PER_MIN": "30",
|
||||
"USER_RATE_LIMIT_PER_HR": "500",
|
||||
|
||||
@@ -207,6 +207,15 @@ PLUGIN_CREDENTIAL_FIELDS: dict[str, list[dict[str, Any]]] = {
|
||||
"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
|
||||
@@ -220,6 +229,7 @@ PLUGIN_DISPLAY_NAMES: dict[str, str] = {
|
||||
"openpanel": "OpenPanel",
|
||||
"appwrite": "Appwrite",
|
||||
"directus": "Directus",
|
||||
"coolify": "Coolify",
|
||||
}
|
||||
|
||||
# Health check endpoints per plugin type
|
||||
@@ -233,6 +243,7 @@ _HEALTH_ENDPOINTS: dict[str, dict[str, Any]] = {
|
||||
"openpanel": {"path": "/healthcheck", "method": "GET"},
|
||||
"appwrite": {"path": "/v1/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":
|
||||
headers["X-Appwrite-Project"] = credentials.get("project_id", "")
|
||||
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', '')}"
|
||||
elif plugin_type == "openpanel":
|
||||
headers["openpanel-client-id"] = credentials.get("client_id", "")
|
||||
|
||||
90
docs/prompts/F16-gitea-public-release.md
Normal file
90
docs/prompts/F16-gitea-public-release.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# F.16 — Gitea Plugin Review, Test & Public Release
|
||||
|
||||
> Session prompt. Copy and paste into a new Claude Code conversation.
|
||||
|
||||
---
|
||||
|
||||
## Prompt:
|
||||
|
||||
```
|
||||
از مهارت project-ops برای آشنایی با محیط استفاده کن. پروژه mcphub-internal روی branch Phase-1 است.
|
||||
فایلهای مرجع:
|
||||
- docs/plans/2026-03-25-v4-development-cycle.md
|
||||
- CLAUDE.md
|
||||
- CHANGELOG.md
|
||||
|
||||
## وضعیت فعلی
|
||||
- نسخه: v3.5.0
|
||||
- 565 ابزار در 9 پلاگین (4 پلاگین عمومی: WordPress, WooCommerce, Supabase, OpenPanel)
|
||||
- FastMCP: `>=3.0.0,<4.0.0`
|
||||
- CI سبز
|
||||
- F.15 تکمیل شده — FastMCP 3.x upgrade + legacy cleanup
|
||||
|
||||
## هدف: فاز F.16 — Gitea Plugin Review, Test & Public Enablement
|
||||
|
||||
مشابه F.10 (OpenPanel) — بررسی، تست و فعالسازی عمومی پلاگین Gitea.
|
||||
|
||||
### بخش 1: بررسی و تحلیل
|
||||
1. **ساختار پلاگین**: `plugins/gitea/` شامل 56 ابزار در 5 هندلر:
|
||||
- `handlers/repositories.py` — 16 tools (CRUD repos, branches, tags, files)
|
||||
- `handlers/issues.py` — 12 tools (issues, comments, labels, milestones)
|
||||
- `handlers/pull_requests.py` — 15 tools (PRs, reviews, merge, diff)
|
||||
- `handlers/users.py` — 8 tools (users, orgs, teams)
|
||||
- `handlers/webhooks.py` — 5 tools (CRUD webhooks, test)
|
||||
2. **مشکلات شناساییشده**:
|
||||
- `update_webhook` در client.py هست ولی به عنوان tool expose نشده
|
||||
- هیچ تست اختصاصیای ندارد (0 تست!)
|
||||
- فعلاً admin-only هست (نه در ENABLED_PLUGINS)
|
||||
3. **تست واقعی**: سایت Gitea از MCPHub MCP endpoint:
|
||||
- از ابزارهای Supabase MCP (`mcp.example.com`) استفاده کنید تا سایت Gitea موجود را پیدا کنید
|
||||
- یا یک Gitea instance جدید از طریق داشبورد اضافه کنید
|
||||
- هر دسته tool را با API واقعی تست کنید
|
||||
|
||||
### بخش 2: تست نوشتن
|
||||
مشابه `tests/test_openpanel_plugin.py` (الگو — 767 خط, 62 تست):
|
||||
1. `tests/test_gitea_plugin.py` بنویسید:
|
||||
- Client initialization + health check
|
||||
- Tool spec validation (نامها، پارامترها، type ها)
|
||||
- Handler delegation (mock client)
|
||||
- Error handling
|
||||
- Pagination
|
||||
2. هدف: حداقل 80 تست (56 tool + edge cases)
|
||||
|
||||
### بخش 3: رفع مشکلات
|
||||
1. `update_webhook` را به عنوان tool expose کنید
|
||||
2. هر tool که در تست واقعی مشکل دارد را fix کنید
|
||||
3. توضیحات service page برای Gitea بنویسید
|
||||
4. health check endpoint را بررسی کنید
|
||||
|
||||
### بخش 4: فعالسازی عمومی
|
||||
1. `core/plugin_visibility.py`: اضافه کردن `gitea` به `DEFAULT_PUBLIC_PLUGINS`
|
||||
2. `env.example`: آپدیت ENABLED_PLUGINS default
|
||||
3. `glama.json`: آپدیت description اگر لازمه
|
||||
|
||||
### بخش 5: Release
|
||||
1. Version bump: `3.5.0` → `3.6.0`
|
||||
2. CHANGELOG update
|
||||
3. Lint: `uvx --python 3.12 black .` && `uvx ruff check --fix .`
|
||||
4. Commit + push Phase-1
|
||||
5. Sync: `python3.11 scripts/community-build/sync.py --output ../mcphub/`
|
||||
6. بعد از sync حتماً `uvx --python 3.12 black .` در public repo هم بزنید!
|
||||
7. Commit + push public repo
|
||||
|
||||
### مراحل پیشنهادی
|
||||
1. **تحلیل**: خواندن کامل هر handler + client + schemas
|
||||
2. **تست واقعی**: اتصال به Gitea instance و تست ابزارها
|
||||
3. **پلن اجرایی**: ارائه پلن و تایید قبل از شروع
|
||||
4. **تست نوشتن**: test_gitea_plugin.py
|
||||
5. **رفع مشکلات**: fix هر tool مشکلدار
|
||||
6. **فعالسازی**: ENABLED_PLUGINS update
|
||||
7. **Release**: v3.6.0
|
||||
|
||||
### نکات فنی
|
||||
- Private repo: /config/workspace/mcphub-internal (branch Phase-1)
|
||||
- Public repo: /config/workspace/mcphub (branch main)
|
||||
- Lint: `uvx --python 3.12 black .` && `uvx ruff check --fix .`
|
||||
- Sync: `python3.11 scripts/community-build/sync.py --output ../mcphub/`
|
||||
- ایمیل git عمومی: hi.airano@gmail.com
|
||||
- **مهم**: اول پلن بده، بدون تایید شروع نکن
|
||||
- **الگوی مرجع**: F.10 (OpenPanel) — بخش "Phase F.10" در plan و `tests/test_openpanel_plugin.py`
|
||||
```
|
||||
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
|
||||
```
|
||||
12
env.example
12
env.example
@@ -32,8 +32,8 @@ DASHBOARD_SESSION_SECRET=
|
||||
# ============================================
|
||||
# Comma-separated list of plugins visible to public (OAuth) users.
|
||||
# Admin users always see all plugins regardless of this setting.
|
||||
# Default (if not set): wordpress,woocommerce,supabase,openpanel
|
||||
# ENABLED_PLUGINS=wordpress,woocommerce,supabase,openpanel
|
||||
# Default (if not set): wordpress,woocommerce,supabase,openpanel,gitea
|
||||
# ENABLED_PLUGINS=wordpress,woocommerce,supabase,openpanel,gitea
|
||||
|
||||
# ============================================
|
||||
# ADMIN SYSTEM (Track F.4)
|
||||
@@ -89,6 +89,14 @@ DASHBOARD_SESSION_SECRET=
|
||||
# USER_RATE_LIMIT_PER_MIN=30
|
||||
# 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)
|
||||
# ============================================
|
||||
|
||||
36
glama.json
Normal file
36
glama.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "https://glama.ai/mcp/schemas/server.json",
|
||||
"maintainers": ["airano-ir"],
|
||||
"name": "mcphub",
|
||||
"display_name": "MCP Hub",
|
||||
"description": "Unified MCP server for managing WordPress, WooCommerce, Gitea, n8n, Supabase, OpenPanel, Appwrite, and Directus with 567 tools, multi-site support, and OAuth 2.1 authentication.",
|
||||
"repository": "https://github.com/airano-ir/mcphub",
|
||||
"homepage": "https://mcp.palebluedot.live",
|
||||
"version": "3.6.0",
|
||||
"license": "MIT",
|
||||
"runtime": "python",
|
||||
"transport": ["stdio", "streamable-http"],
|
||||
"categories": ["developer-tools", "cms", "databases", "automation"],
|
||||
"keywords": ["mcp", "wordpress", "woocommerce", "supabase", "gitea", "n8n", "openpanel", "appwrite", "directus", "self-hosted", "multi-site"],
|
||||
"author": {
|
||||
"name": "airano-ir",
|
||||
"url": "https://github.com/airano-ir"
|
||||
},
|
||||
"capabilities": {
|
||||
"tools": true,
|
||||
"prompts": false,
|
||||
"resources": false
|
||||
},
|
||||
"relatedServers": [
|
||||
{
|
||||
"name": "WordPress MCP",
|
||||
"url": "https://glama.ai/mcp/servers/developer-in-world/wordpress-mcp",
|
||||
"description": "WordPress REST API MCP server"
|
||||
},
|
||||
{
|
||||
"name": "Supabase MCP",
|
||||
"url": "https://glama.ai/mcp/servers/supabase-community/supabase-mcp",
|
||||
"description": "Official Supabase MCP server"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,6 +11,7 @@ v2.3.0 (Phase G): Supabase Self-Hosted Plugin added
|
||||
|
||||
from plugins.appwrite.plugin import AppwritePlugin
|
||||
from plugins.base import BasePlugin, PluginRegistry
|
||||
from plugins.coolify.plugin import CoolifyPlugin
|
||||
from plugins.directus.plugin import DirectusPlugin
|
||||
from plugins.gitea.plugin import GiteaPlugin
|
||||
from plugins.n8n.plugin import N8nPlugin
|
||||
@@ -33,6 +34,7 @@ registry.register("supabase", SupabasePlugin)
|
||||
registry.register("openpanel", OpenPanelPlugin)
|
||||
registry.register("appwrite", AppwritePlugin)
|
||||
registry.register("directus", DirectusPlugin)
|
||||
registry.register("coolify", CoolifyPlugin)
|
||||
|
||||
__all__ = [
|
||||
"BasePlugin",
|
||||
@@ -47,4 +49,5 @@ __all__ = [
|
||||
"OpenPanelPlugin",
|
||||
"AppwritePlugin",
|
||||
"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
|
||||
@@ -295,6 +295,25 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "delete_label",
|
||||
"method_name": "delete_label",
|
||||
"description": "Delete a label from a repository.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
|
||||
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
||||
"label_id": {
|
||||
"type": "integer",
|
||||
"description": "Label ID to delete",
|
||||
"minimum": 1,
|
||||
},
|
||||
},
|
||||
"required": ["owner", "repo", "label_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
# === MILESTONES ===
|
||||
{
|
||||
"name": "list_milestones",
|
||||
@@ -499,6 +518,13 @@ async def create_label(
|
||||
return json.dumps(result, indent=2)
|
||||
|
||||
|
||||
async def delete_label(client: GiteaClient, owner: str, repo: str, label_id: int) -> str:
|
||||
"""Delete a label"""
|
||||
await client.delete_label(owner, repo, label_id)
|
||||
result = {"success": True, "message": f"Label {label_id} deleted successfully"}
|
||||
return json.dumps(result, indent=2)
|
||||
|
||||
|
||||
# Milestone operations
|
||||
async def list_milestones(
|
||||
client: GiteaClient, owner: str, repo: str, state: str | None = None
|
||||
|
||||
@@ -526,12 +526,13 @@ async def merge_pull_request(
|
||||
delete_branch_after_merge: bool = False,
|
||||
) -> str:
|
||||
"""Merge a pull request"""
|
||||
data = {
|
||||
"Do": method,
|
||||
"MergeTitleField": title,
|
||||
"MergeMessageField": message,
|
||||
"delete_branch_after_merge": delete_branch_after_merge,
|
||||
}
|
||||
data: dict = {"Do": method}
|
||||
if title is not None:
|
||||
data["MergeTitleField"] = title
|
||||
if message is not None:
|
||||
data["MergeMessageField"] = message
|
||||
if delete_branch_after_merge:
|
||||
data["delete_branch_after_merge"] = True
|
||||
merge_result = await client.merge_pull_request(owner, repo, pr_number, data)
|
||||
result = {
|
||||
"success": True,
|
||||
|
||||
@@ -159,6 +159,79 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "update_webhook",
|
||||
"method_name": "update_webhook",
|
||||
"description": "Update an existing webhook's configuration, events, or active status.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
|
||||
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
||||
"webhook_id": {
|
||||
"type": "integer",
|
||||
"description": "Webhook ID to update",
|
||||
"minimum": 1,
|
||||
},
|
||||
"url": {
|
||||
"anyOf": [{"type": "string", "format": "uri"}, {"type": "null"}],
|
||||
"description": "New webhook URL",
|
||||
},
|
||||
"events": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"create",
|
||||
"delete",
|
||||
"fork",
|
||||
"push",
|
||||
"issues",
|
||||
"issue_assign",
|
||||
"issue_label",
|
||||
"issue_milestone",
|
||||
"issue_comment",
|
||||
"pull_request",
|
||||
"pull_request_assign",
|
||||
"pull_request_label",
|
||||
"pull_request_milestone",
|
||||
"pull_request_comment",
|
||||
"pull_request_review_approved",
|
||||
"pull_request_review_rejected",
|
||||
"pull_request_review_comment",
|
||||
"pull_request_sync",
|
||||
"wiki",
|
||||
"repository",
|
||||
"release",
|
||||
],
|
||||
},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"description": "Updated list of events to trigger webhook",
|
||||
},
|
||||
"content_type": {
|
||||
"anyOf": [
|
||||
{"type": "string", "enum": ["json", "form"]},
|
||||
{"type": "null"},
|
||||
],
|
||||
"description": "Content type for webhook payload",
|
||||
},
|
||||
"secret": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Secret for webhook signature verification",
|
||||
},
|
||||
"active": {
|
||||
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||
"description": "Activate or deactivate webhook",
|
||||
},
|
||||
},
|
||||
"required": ["owner", "repo", "webhook_id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -220,3 +293,40 @@ async def get_webhook(client: GiteaClient, owner: str, repo: str, webhook_id: in
|
||||
webhook = await client.get_webhook(owner, repo, webhook_id)
|
||||
result = {"success": True, "webhook": webhook}
|
||||
return json.dumps(result, indent=2)
|
||||
|
||||
|
||||
async def update_webhook(
|
||||
client: GiteaClient,
|
||||
owner: str,
|
||||
repo: str,
|
||||
webhook_id: int,
|
||||
url: str | None = None,
|
||||
events: list[str] | None = None,
|
||||
content_type: str | None = None,
|
||||
secret: str | None = None,
|
||||
active: bool | None = None,
|
||||
) -> str:
|
||||
"""Update a webhook"""
|
||||
data: dict = {}
|
||||
if events is not None:
|
||||
data["events"] = events
|
||||
if active is not None:
|
||||
data["active"] = active
|
||||
|
||||
config: dict = {}
|
||||
if url is not None:
|
||||
config["url"] = url
|
||||
if content_type is not None:
|
||||
config["content_type"] = content_type
|
||||
if secret is not None:
|
||||
config["secret"] = secret
|
||||
if config:
|
||||
data["config"] = config
|
||||
|
||||
webhook = await client.update_webhook(owner, repo, webhook_id, data)
|
||||
result = {
|
||||
"success": True,
|
||||
"message": f"Webhook {webhook_id} updated successfully",
|
||||
"webhook": webhook,
|
||||
}
|
||||
return json.dumps(result, indent=2)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "mcphub-server"
|
||||
version = "3.5.0"
|
||||
version = "3.7.0"
|
||||
description = "AI-native management hub for WordPress, WooCommerce, and self-hosted services via Model Context Protocol (MCP)"
|
||||
authors = [
|
||||
{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:
|
||||
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")
|
||||
|
||||
# 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"]
|
||||
1135
tests/test_gitea_plugin.py
Normal file
1135
tests/test_gitea_plugin.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -78,11 +78,11 @@ class TestIsPluginPublic:
|
||||
os.environ.pop("ENABLED_PLUGINS", None)
|
||||
assert is_plugin_public("supabase") is True
|
||||
|
||||
def test_default_gitea_not_public(self):
|
||||
"""Gitea is not public by default."""
|
||||
def test_default_gitea_public(self):
|
||||
"""Gitea is public by default (since F.16)."""
|
||||
with patch.dict(os.environ, {}, clear=False):
|
||||
os.environ.pop("ENABLED_PLUGINS", None)
|
||||
assert is_plugin_public("gitea") is False
|
||||
assert is_plugin_public("gitea") is True
|
||||
|
||||
def test_default_wordpress_advanced_not_public(self):
|
||||
"""WordPress Advanced is not public by default."""
|
||||
@@ -118,8 +118,9 @@ class TestSiteApiIntegration:
|
||||
assert "wordpress" in fields
|
||||
assert "woocommerce" in fields
|
||||
assert "supabase" in fields
|
||||
assert "gitea" in fields
|
||||
assert "openpanel" in fields
|
||||
assert "wordpress_advanced" not in fields
|
||||
assert "gitea" not in fields
|
||||
assert "n8n" not in fields
|
||||
|
||||
def test_get_user_plugin_names_filtered(self):
|
||||
@@ -133,8 +134,9 @@ class TestSiteApiIntegration:
|
||||
assert "wordpress" in names
|
||||
assert "woocommerce" in names
|
||||
assert "supabase" in names
|
||||
assert "gitea" in names
|
||||
assert "openpanel" in names
|
||||
assert "wordpress_advanced" not in names
|
||||
assert "gitea" not in names
|
||||
|
||||
def test_custom_env_changes_fields(self):
|
||||
"""Custom ENABLED_PLUGINS changes which credential fields are returned."""
|
||||
|
||||
Reference in New Issue
Block a user