pluglayer-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pluglayer_mcp-0.1.0/.github/workflows/deploy-uvx-main.yml +70 -0
- pluglayer_mcp-0.1.0/PKG-INFO +144 -0
- pluglayer_mcp-0.1.0/README.md +130 -0
- pluglayer_mcp-0.1.0/pluglayer_mcp/__init__.py +2 -0
- pluglayer_mcp-0.1.0/pluglayer_mcp/client.py +59 -0
- pluglayer_mcp-0.1.0/pluglayer_mcp/server.py +69 -0
- pluglayer_mcp-0.1.0/pluglayer_mcp/settings.py +27 -0
- pluglayer_mcp-0.1.0/pluglayer_mcp/tools/__init__.py +1 -0
- pluglayer_mcp-0.1.0/pluglayer_mcp/tools/cicd_health.py +42 -0
- pluglayer_mcp-0.1.0/pluglayer_mcp/tools/compute.py +92 -0
- pluglayer_mcp-0.1.0/pluglayer_mcp/tools/deployments.py +180 -0
- pluglayer_mcp-0.1.0/pluglayer_mcp/tools/domains.py +81 -0
- pluglayer_mcp-0.1.0/pluglayer_mcp/tools/identity_projects.py +95 -0
- pluglayer_mcp-0.1.0/pluglayer_mcp/tools/shared.py +53 -0
- pluglayer_mcp-0.1.0/pluglayer_mcp/tools/tasks_admin.py +122 -0
- pluglayer_mcp-0.1.0/pyproject.toml +25 -0
- pluglayer_mcp-0.1.0/uv.lock +1057 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: Publish pluglayer-mcp
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
paths:
|
|
8
|
+
- pyproject.toml
|
|
9
|
+
- uv.lock
|
|
10
|
+
- README.md
|
|
11
|
+
- pluglayer_mcp/**
|
|
12
|
+
- .github/workflows/deploy-uvx.yml
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
id-token: write
|
|
18
|
+
|
|
19
|
+
concurrency:
|
|
20
|
+
group: publish-pluglayer-mcp
|
|
21
|
+
cancel-in-progress: false
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
publish:
|
|
25
|
+
name: Build and publish to PyPI
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
environment:
|
|
28
|
+
name: pypi
|
|
29
|
+
steps:
|
|
30
|
+
- name: Checkout package repo
|
|
31
|
+
uses: actions/checkout@v4
|
|
32
|
+
|
|
33
|
+
- name: Set up Python
|
|
34
|
+
uses: actions/setup-python@v5
|
|
35
|
+
with:
|
|
36
|
+
python-version: "3.12"
|
|
37
|
+
|
|
38
|
+
- name: Set up uv
|
|
39
|
+
uses: astral-sh/setup-uv@v4
|
|
40
|
+
|
|
41
|
+
- name: Validate package metadata
|
|
42
|
+
shell: bash
|
|
43
|
+
run: |
|
|
44
|
+
set -euo pipefail
|
|
45
|
+
|
|
46
|
+
test -f pyproject.toml
|
|
47
|
+
test -f pluglayer_mcp/server.py
|
|
48
|
+
test -f README.md
|
|
49
|
+
|
|
50
|
+
uv sync --frozen
|
|
51
|
+
uv run python -m compileall pluglayer_mcp
|
|
52
|
+
|
|
53
|
+
- name: Build distributions
|
|
54
|
+
shell: bash
|
|
55
|
+
run: |
|
|
56
|
+
set -euo pipefail
|
|
57
|
+
uv build
|
|
58
|
+
|
|
59
|
+
- name: Publish to PyPI
|
|
60
|
+
shell: bash
|
|
61
|
+
env:
|
|
62
|
+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
|
63
|
+
run: |
|
|
64
|
+
set -euo pipefail
|
|
65
|
+
|
|
66
|
+
if [[ -n "${UV_PUBLISH_TOKEN:-}" ]]; then
|
|
67
|
+
uv publish
|
|
68
|
+
else
|
|
69
|
+
uv publish --trusted-publishing always
|
|
70
|
+
fi
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pluglayer-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: PlugLayer MCP server — deploy and manage infrastructure via AI
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: deployment,infrastructure,kubernetes,mcp,pluglayer
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: httpx>=0.27.0
|
|
9
|
+
Requires-Dist: mcp[cli]>=1.3.0
|
|
10
|
+
Requires-Dist: pydantic-settings>=2.4.0
|
|
11
|
+
Requires-Dist: pydantic>=2.8.0
|
|
12
|
+
Requires-Dist: uvicorn[standard]>=0.30.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# PlugLayer MCP Server
|
|
16
|
+
|
|
17
|
+
Deploy and manage your infrastructure through natural language with any MCP-compatible AI assistant.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
### Option 1: uvx (recommended — no install needed)
|
|
22
|
+
```bash
|
|
23
|
+
PLUGLAYER_API_KEY=your-pluglayer-api-token uvx pluglayer-mcp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Optional:
|
|
27
|
+
```bash
|
|
28
|
+
PLUGLAYER_API_BASE_URL=https://api.pluglayer.com
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Option 2: pip
|
|
32
|
+
```bash
|
|
33
|
+
pip install pluglayer-mcp
|
|
34
|
+
PLUGLAYER_API_KEY=your-pluglayer-api-token pluglayer-mcp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
### Claude Desktop
|
|
40
|
+
Add to `~/.config/Claude/claude_desktop_config.json`:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"pluglayer": {
|
|
46
|
+
"command": "uvx",
|
|
47
|
+
"args": ["pluglayer-mcp"],
|
|
48
|
+
"env": {
|
|
49
|
+
"PLUGLAYER_API_KEY": "your-pluglayer-api-token",
|
|
50
|
+
"PLUGLAYER_API_BASE_URL": "https://api.pluglayer.com"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Cursor
|
|
58
|
+
Add to `~/.cursor/mcp.json`:
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"pluglayer": {
|
|
62
|
+
"command": "uvx",
|
|
63
|
+
"args": ["pluglayer-mcp"],
|
|
64
|
+
"env": {
|
|
65
|
+
"PLUGLAYER_API_KEY": "your-pluglayer-api-token",
|
|
66
|
+
"PLUGLAYER_API_BASE_URL": "https://api.pluglayer.com"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Remote HTTP (hosted)
|
|
73
|
+
The remote MCP server runs at `mcp.pluglayer.com`. Pass your token as:
|
|
74
|
+
```
|
|
75
|
+
Authorization: Bearer your-pluglayer-api-token
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### API base URL behavior
|
|
79
|
+
|
|
80
|
+
- `PLUGLAYER_API_BASE_URL` is the preferred environment variable for the backend API origin.
|
|
81
|
+
- If it is unset or empty, the MCP defaults to `https://api.pluglayer.com`.
|
|
82
|
+
- `PLUGLAYER_API_URL` is still accepted as a legacy fallback during migration.
|
|
83
|
+
|
|
84
|
+
## Available Tools
|
|
85
|
+
|
|
86
|
+
The MCP calls the PlugLayer FastAPI backend instead of re-implementing backend business logic. Auth, roles, ownership, compute guards, k3s orchestration, and admin checks remain in the backend. MCP and editor plugins should authenticate with a **PlugLayer API token** created in the PlugLayer Settings page, not the browser/session auth token.
|
|
87
|
+
|
|
88
|
+
Managed registries are configured by PlugLayer admins in the platform UI/API. When `deploy_image` uses mirroring, the backend picks a registry the current user is allowed to use and keeps Kubernetes pull secrets in sync automatically.
|
|
89
|
+
|
|
90
|
+
| Tool | Description |
|
|
91
|
+
|------|-------------|
|
|
92
|
+
| `get_current_user` | Show the Authentik-backed user and `roles` |
|
|
93
|
+
| `list_projects` | List authenticated user's projects |
|
|
94
|
+
| `create_project` | Create a new project namespace |
|
|
95
|
+
| `get_project` | Get project details |
|
|
96
|
+
| `get_compute_summary` | Show account-level personal + shared compute capacity |
|
|
97
|
+
| `list_nodes` | List accessible compute nodes |
|
|
98
|
+
| `add_node_ssh` | Add a personal SSH node usable by all of the user's projects |
|
|
99
|
+
| `list_registries` | List the registries currently available to the user |
|
|
100
|
+
| `deploy_image` | Mirror a Docker image into PlugLayer's managed Docker Hub namespace, then deploy it after backend compute checks |
|
|
101
|
+
| `deploy_compose` | Deploy from docker-compose.yml after backend compute checks |
|
|
102
|
+
| `list_deployments` | List running apps/deployments |
|
|
103
|
+
| `get_deployment_status` | Check app status and URL |
|
|
104
|
+
| `get_logs` | Get app logs |
|
|
105
|
+
| `redeploy` | Redeploy an app |
|
|
106
|
+
| `rollback` | Roll back to previous version |
|
|
107
|
+
| `delete_deployment` | Delete an app |
|
|
108
|
+
| `list_project_domains` | List custom domains for a project |
|
|
109
|
+
| `add_custom_domain` | Add a single or wildcard custom domain and return DNS records |
|
|
110
|
+
| `verify_custom_domain` | Verify TXT/CNAME DNS and activate if attached |
|
|
111
|
+
| `attach_custom_domain` | Attach a verified custom domain to an app |
|
|
112
|
+
| `detach_custom_domain` | Detach a domain while keeping verification |
|
|
113
|
+
| `remove_custom_domain` | Remove a domain and its route |
|
|
114
|
+
| `get_task_status` | Poll async operation progress |
|
|
115
|
+
| `admin_get_overview` | Admin-only platform summary |
|
|
116
|
+
| `admin_set_compute_defaults` | Admin-only default compute quota metadata |
|
|
117
|
+
| `admin_set_node_shared` | Admin-only mark node shared/private |
|
|
118
|
+
| `admin_add_shared_ssh_node` | Admin-only add shared PlugLayer SSH compute |
|
|
119
|
+
| `generate_github_actions` | Get CI/CD pipeline YAML |
|
|
120
|
+
| `get_cluster_health` | Check cluster status |
|
|
121
|
+
|
|
122
|
+
## Example Conversations
|
|
123
|
+
|
|
124
|
+
**Deploy your first app:**
|
|
125
|
+
> "I have a FastAPI app at `ghcr.io/myorg/api:latest` that runs on port 8000. Deploy it to my `production` project."
|
|
126
|
+
|
|
127
|
+
**Convert docker-compose:**
|
|
128
|
+
> "Here's my docker-compose.yml: [paste]. Deploy this to PlugLayer."
|
|
129
|
+
|
|
130
|
+
**Add a node:**
|
|
131
|
+
> "Add my server at 192.168.1.100 as personal compute. Here's my SSH key: [paste]"
|
|
132
|
+
|
|
133
|
+
**CI/CD setup:**
|
|
134
|
+
> "Generate a GitHub Actions workflow for my `api` deployment so it auto-deploys on push to main."
|
|
135
|
+
|
|
136
|
+
**Add a custom domain:**
|
|
137
|
+
> "Add `api.example.com` to my production project, show me the DNS records, then verify it and attach it to my API app."
|
|
138
|
+
|
|
139
|
+
## Getting Your API Key
|
|
140
|
+
|
|
141
|
+
1. Go to PlugLayer Settings
|
|
142
|
+
2. Create a **PlugLayer API token**
|
|
143
|
+
3. Copy it once and store it safely
|
|
144
|
+
4. Use it as `PLUGLAYER_API_KEY` for MCP, editor plugins, and CI/CD webhook deploys
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# PlugLayer MCP Server
|
|
2
|
+
|
|
3
|
+
Deploy and manage your infrastructure through natural language with any MCP-compatible AI assistant.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Option 1: uvx (recommended — no install needed)
|
|
8
|
+
```bash
|
|
9
|
+
PLUGLAYER_API_KEY=your-pluglayer-api-token uvx pluglayer-mcp
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Optional:
|
|
13
|
+
```bash
|
|
14
|
+
PLUGLAYER_API_BASE_URL=https://api.pluglayer.com
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Option 2: pip
|
|
18
|
+
```bash
|
|
19
|
+
pip install pluglayer-mcp
|
|
20
|
+
PLUGLAYER_API_KEY=your-pluglayer-api-token pluglayer-mcp
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
### Claude Desktop
|
|
26
|
+
Add to `~/.config/Claude/claude_desktop_config.json`:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"pluglayer": {
|
|
32
|
+
"command": "uvx",
|
|
33
|
+
"args": ["pluglayer-mcp"],
|
|
34
|
+
"env": {
|
|
35
|
+
"PLUGLAYER_API_KEY": "your-pluglayer-api-token",
|
|
36
|
+
"PLUGLAYER_API_BASE_URL": "https://api.pluglayer.com"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Cursor
|
|
44
|
+
Add to `~/.cursor/mcp.json`:
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"pluglayer": {
|
|
48
|
+
"command": "uvx",
|
|
49
|
+
"args": ["pluglayer-mcp"],
|
|
50
|
+
"env": {
|
|
51
|
+
"PLUGLAYER_API_KEY": "your-pluglayer-api-token",
|
|
52
|
+
"PLUGLAYER_API_BASE_URL": "https://api.pluglayer.com"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Remote HTTP (hosted)
|
|
59
|
+
The remote MCP server runs at `mcp.pluglayer.com`. Pass your token as:
|
|
60
|
+
```
|
|
61
|
+
Authorization: Bearer your-pluglayer-api-token
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### API base URL behavior
|
|
65
|
+
|
|
66
|
+
- `PLUGLAYER_API_BASE_URL` is the preferred environment variable for the backend API origin.
|
|
67
|
+
- If it is unset or empty, the MCP defaults to `https://api.pluglayer.com`.
|
|
68
|
+
- `PLUGLAYER_API_URL` is still accepted as a legacy fallback during migration.
|
|
69
|
+
|
|
70
|
+
## Available Tools
|
|
71
|
+
|
|
72
|
+
The MCP calls the PlugLayer FastAPI backend instead of re-implementing backend business logic. Auth, roles, ownership, compute guards, k3s orchestration, and admin checks remain in the backend. MCP and editor plugins should authenticate with a **PlugLayer API token** created in the PlugLayer Settings page, not the browser/session auth token.
|
|
73
|
+
|
|
74
|
+
Managed registries are configured by PlugLayer admins in the platform UI/API. When `deploy_image` uses mirroring, the backend picks a registry the current user is allowed to use and keeps Kubernetes pull secrets in sync automatically.
|
|
75
|
+
|
|
76
|
+
| Tool | Description |
|
|
77
|
+
|------|-------------|
|
|
78
|
+
| `get_current_user` | Show the Authentik-backed user and `roles` |
|
|
79
|
+
| `list_projects` | List authenticated user's projects |
|
|
80
|
+
| `create_project` | Create a new project namespace |
|
|
81
|
+
| `get_project` | Get project details |
|
|
82
|
+
| `get_compute_summary` | Show account-level personal + shared compute capacity |
|
|
83
|
+
| `list_nodes` | List accessible compute nodes |
|
|
84
|
+
| `add_node_ssh` | Add a personal SSH node usable by all of the user's projects |
|
|
85
|
+
| `list_registries` | List the registries currently available to the user |
|
|
86
|
+
| `deploy_image` | Mirror a Docker image into PlugLayer's managed Docker Hub namespace, then deploy it after backend compute checks |
|
|
87
|
+
| `deploy_compose` | Deploy from docker-compose.yml after backend compute checks |
|
|
88
|
+
| `list_deployments` | List running apps/deployments |
|
|
89
|
+
| `get_deployment_status` | Check app status and URL |
|
|
90
|
+
| `get_logs` | Get app logs |
|
|
91
|
+
| `redeploy` | Redeploy an app |
|
|
92
|
+
| `rollback` | Roll back to previous version |
|
|
93
|
+
| `delete_deployment` | Delete an app |
|
|
94
|
+
| `list_project_domains` | List custom domains for a project |
|
|
95
|
+
| `add_custom_domain` | Add a single or wildcard custom domain and return DNS records |
|
|
96
|
+
| `verify_custom_domain` | Verify TXT/CNAME DNS and activate if attached |
|
|
97
|
+
| `attach_custom_domain` | Attach a verified custom domain to an app |
|
|
98
|
+
| `detach_custom_domain` | Detach a domain while keeping verification |
|
|
99
|
+
| `remove_custom_domain` | Remove a domain and its route |
|
|
100
|
+
| `get_task_status` | Poll async operation progress |
|
|
101
|
+
| `admin_get_overview` | Admin-only platform summary |
|
|
102
|
+
| `admin_set_compute_defaults` | Admin-only default compute quota metadata |
|
|
103
|
+
| `admin_set_node_shared` | Admin-only mark node shared/private |
|
|
104
|
+
| `admin_add_shared_ssh_node` | Admin-only add shared PlugLayer SSH compute |
|
|
105
|
+
| `generate_github_actions` | Get CI/CD pipeline YAML |
|
|
106
|
+
| `get_cluster_health` | Check cluster status |
|
|
107
|
+
|
|
108
|
+
## Example Conversations
|
|
109
|
+
|
|
110
|
+
**Deploy your first app:**
|
|
111
|
+
> "I have a FastAPI app at `ghcr.io/myorg/api:latest` that runs on port 8000. Deploy it to my `production` project."
|
|
112
|
+
|
|
113
|
+
**Convert docker-compose:**
|
|
114
|
+
> "Here's my docker-compose.yml: [paste]. Deploy this to PlugLayer."
|
|
115
|
+
|
|
116
|
+
**Add a node:**
|
|
117
|
+
> "Add my server at 192.168.1.100 as personal compute. Here's my SSH key: [paste]"
|
|
118
|
+
|
|
119
|
+
**CI/CD setup:**
|
|
120
|
+
> "Generate a GitHub Actions workflow for my `api` deployment so it auto-deploys on push to main."
|
|
121
|
+
|
|
122
|
+
**Add a custom domain:**
|
|
123
|
+
> "Add `api.example.com` to my production project, show me the DNS records, then verify it and attach it to my API app."
|
|
124
|
+
|
|
125
|
+
## Getting Your API Key
|
|
126
|
+
|
|
127
|
+
1. Go to PlugLayer Settings
|
|
128
|
+
2. Create a **PlugLayer API token**
|
|
129
|
+
3. Copy it once and store it safely
|
|
130
|
+
4. Use it as `PLUGLAYER_API_KEY` for MCP, editor plugins, and CI/CD webhook deploys
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from typing import Optional, Any
|
|
3
|
+
from pluglayer_mcp.settings import settings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PlugLayerClient:
|
|
7
|
+
"""HTTP client for the PlugLayer API."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
10
|
+
self.api_key = api_key or settings.PLUGLAYER_API_KEY
|
|
11
|
+
self.base_url = (base_url or settings.resolved_api_base_url).rstrip("/")
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def headers(self) -> dict:
|
|
15
|
+
return {
|
|
16
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
"User-Agent": "pluglayer-mcp/0.1.0",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async def _request(self, method: str, path: str, *, params: dict = None, data: dict = None, timeout: float = 30.0) -> Any:
|
|
22
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
23
|
+
resp = await client.request(
|
|
24
|
+
method,
|
|
25
|
+
f"{self.base_url}{path}",
|
|
26
|
+
headers=self.headers,
|
|
27
|
+
params=params,
|
|
28
|
+
json=data,
|
|
29
|
+
)
|
|
30
|
+
try:
|
|
31
|
+
resp.raise_for_status()
|
|
32
|
+
except httpx.HTTPStatusError as exc:
|
|
33
|
+
detail = resp.text[:500]
|
|
34
|
+
raise RuntimeError(f"{resp.status_code} {resp.reason_phrase}: {detail}") from exc
|
|
35
|
+
if resp.status_code == 204 or not resp.content:
|
|
36
|
+
return {}
|
|
37
|
+
data = resp.json()
|
|
38
|
+
if isinstance(data, dict) and data.get("ok") is True and "data" in data:
|
|
39
|
+
return data["data"]
|
|
40
|
+
return data
|
|
41
|
+
|
|
42
|
+
async def get(self, path: str, params: dict = None) -> Any:
|
|
43
|
+
return await self._request("GET", path, params=params, timeout=30.0)
|
|
44
|
+
|
|
45
|
+
async def post(self, path: str, data: dict = None, params: dict = None) -> Any:
|
|
46
|
+
return await self._request("POST", path, params=params, data=data or {}, timeout=60.0)
|
|
47
|
+
|
|
48
|
+
async def delete(self, path: str) -> Any:
|
|
49
|
+
return await self._request("DELETE", path, timeout=30.0)
|
|
50
|
+
|
|
51
|
+
async def patch(self, path: str, data: dict) -> Any:
|
|
52
|
+
return await self._request("PATCH", path, data=data, timeout=30.0)
|
|
53
|
+
|
|
54
|
+
async def put(self, path: str, data: dict) -> Any:
|
|
55
|
+
return await self._request("PUT", path, data=data, timeout=30.0)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_client(api_key: Optional[str] = None) -> PlugLayerClient:
|
|
59
|
+
return PlugLayerClient(api_key=api_key)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlugLayer MCP Server
|
|
3
|
+
|
|
4
|
+
Exposes PlugLayer project, compute, deployment, CI/CD, and admin tools to AI
|
|
5
|
+
assistants through the Model Context Protocol (MCP). The MCP intentionally goes
|
|
6
|
+
through the FastAPI backend endpoints so auth, roles, ownership, quotas, compute
|
|
7
|
+
checks, k3s orchestration, and admin guards stay in one backend implementation.
|
|
8
|
+
"""
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from mcp.server.fastmcp import FastMCP
|
|
12
|
+
|
|
13
|
+
from pluglayer_mcp.settings import settings
|
|
14
|
+
|
|
15
|
+
mcp = FastMCP(
|
|
16
|
+
"PlugLayer",
|
|
17
|
+
instructions="""You are the PlugLayer infrastructure operator.
|
|
18
|
+
You help users deploy, manage, and monitor applications on PlugLayer.
|
|
19
|
+
|
|
20
|
+
Current PlugLayer rules:
|
|
21
|
+
- Authentik groups are exposed by PlugLayer as user.roles. Do not use groups/permissions fields.
|
|
22
|
+
- Admin tools require the user to have pluglayer-admin or pluglayer-superadmin in roles.
|
|
23
|
+
- Compute is account-level: personal SSH nodes and shared PlugLayer nodes can be used by all projects the user owns.
|
|
24
|
+
- A project is a k3s namespace. A deployment is an app inside a project.
|
|
25
|
+
- Custom domains are verified and routed by backend v1 domain endpoints; do not invent DNS or Traefik state.
|
|
26
|
+
- Async operations return task IDs; always poll get_task_status until completion.
|
|
27
|
+
|
|
28
|
+
Deployment workflow:
|
|
29
|
+
1. Run get_current_user and get_compute_summary.
|
|
30
|
+
2. List or create a project.
|
|
31
|
+
3. Ensure can_deploy is true. If false, add a personal SSH node or ask an admin to assign shared compute.
|
|
32
|
+
4. Deploy from image or docker-compose.
|
|
33
|
+
5. Poll the returned task and report the public URL.
|
|
34
|
+
|
|
35
|
+
Confirm destructive actions such as delete and rollback before executing them.
|
|
36
|
+
""",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
from pluglayer_mcp.tools.cicd_health import register_cicd_health_tools
|
|
40
|
+
from pluglayer_mcp.tools.compute import register_compute_tools
|
|
41
|
+
from pluglayer_mcp.tools.deployments import register_deployment_tools
|
|
42
|
+
from pluglayer_mcp.tools.domains import register_domain_tools
|
|
43
|
+
from pluglayer_mcp.tools.identity_projects import register_identity_project_tools
|
|
44
|
+
from pluglayer_mcp.tools.tasks_admin import register_task_admin_tools
|
|
45
|
+
|
|
46
|
+
register_identity_project_tools(mcp)
|
|
47
|
+
register_compute_tools(mcp)
|
|
48
|
+
register_deployment_tools(mcp)
|
|
49
|
+
register_domain_tools(mcp)
|
|
50
|
+
register_task_admin_tools(mcp)
|
|
51
|
+
register_cicd_health_tools(mcp)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
"""Entry point for `pluglayer-mcp` command."""
|
|
56
|
+
if not settings.PLUGLAYER_API_KEY:
|
|
57
|
+
print(
|
|
58
|
+
"WARNING: PLUGLAYER_API_KEY not set!\n"
|
|
59
|
+
"Set it as an environment variable:\n"
|
|
60
|
+
" PLUGLAYER_API_KEY=your-token pluglayer-mcp\n\n"
|
|
61
|
+
"Get your token from: https://portal.pluglayer.com/settings",
|
|
62
|
+
file=sys.stderr,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
mcp.run(transport="streamable-http")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
main()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
from pydantic import Field
|
|
3
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Settings(BaseSettings):
|
|
7
|
+
PLUGLAYER_API_BASE_URL: str = "https://api.pluglayer.com"
|
|
8
|
+
PLUGLAYER_API_URL: str = Field(default="") # legacy fallback
|
|
9
|
+
PLUGLAYER_API_KEY: str = "" # Set by user via env var
|
|
10
|
+
MCP_HOST: str = "0.0.0.0"
|
|
11
|
+
MCP_PORT: int = 8001
|
|
12
|
+
DEBUG: bool = False
|
|
13
|
+
|
|
14
|
+
model_config = SettingsConfigDict(env_file=".env")
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def resolved_api_base_url(self) -> str:
|
|
18
|
+
candidate = (self.PLUGLAYER_API_BASE_URL or "").strip() or (self.PLUGLAYER_API_URL or "").strip()
|
|
19
|
+
return candidate or "https://api.pluglayer.com"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@lru_cache()
|
|
23
|
+
def get_settings() -> Settings:
|
|
24
|
+
return Settings()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
settings = get_settings()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP tool registration modules."""
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Cicd Health MCP tools."""
|
|
2
|
+
|
|
3
|
+
from pluglayer_mcp.tools.shared import _client, _compact_error
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register_cicd_health_tools(mcp):
|
|
7
|
+
@mcp.tool()
|
|
8
|
+
async def generate_github_actions(project_id: str, deployment_id: str, github_org: str = "your-org") -> str:
|
|
9
|
+
"""Generate a GitHub Actions workflow YAML for PlugLayer CI/CD."""
|
|
10
|
+
try:
|
|
11
|
+
data = await _client().get("/v1/plugin/cicd/generate/github-actions", params={
|
|
12
|
+
"project_id": project_id,
|
|
13
|
+
"deployment_id": deployment_id,
|
|
14
|
+
"repo": github_org,
|
|
15
|
+
})
|
|
16
|
+
workflow = data.get("workflow_yaml", "")
|
|
17
|
+
filename = data.get("filename", ".github/workflows/deploy-pluglayer.yml")
|
|
18
|
+
return (
|
|
19
|
+
f"📋 **GitHub Actions Workflow**\n"
|
|
20
|
+
f"Save as: `{filename}`\n\n"
|
|
21
|
+
f"```yaml\n{workflow}\n```\n\n"
|
|
22
|
+
"Setup steps:\n"
|
|
23
|
+
"1. Create this file in your repo.\n"
|
|
24
|
+
"2. Add `PLUGLAYER_API_KEY` as a GitHub secret.\n"
|
|
25
|
+
"3. Push to main/master to trigger deploys."
|
|
26
|
+
)
|
|
27
|
+
except Exception as e:
|
|
28
|
+
return _compact_error("Error generating pipeline", e)
|
|
29
|
+
|
|
30
|
+
@mcp.tool()
|
|
31
|
+
async def get_cluster_health() -> str:
|
|
32
|
+
"""Check PlugLayer API and k3s cluster health."""
|
|
33
|
+
try:
|
|
34
|
+
health = await _client().get("/v1/plugin/health")
|
|
35
|
+
k3s = await _client().get("/v1/plugin/health/k3s")
|
|
36
|
+
return (
|
|
37
|
+
"🩺 **PlugLayer Health**\n"
|
|
38
|
+
f"API: {health.get('api', 'unknown')}\n"
|
|
39
|
+
f"k3s: {'healthy' if k3s.get('ok') else 'unavailable'} — {k3s.get('message', '')}"
|
|
40
|
+
)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
return _compact_error("Error checking health", e)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Compute MCP tools."""
|
|
2
|
+
|
|
3
|
+
from pluglayer_mcp.tools.shared import _client, _compact_error, _fmt_compute, _fmt_node, _fmt_task_hint, _get_compute_summary
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register_compute_tools(mcp):
|
|
7
|
+
# ── Compute / nodes ───────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
async def get_compute_summary() -> str:
|
|
12
|
+
"""Show accessible account-level compute: personal SSH nodes plus shared PlugLayer nodes."""
|
|
13
|
+
try:
|
|
14
|
+
data = await _get_compute_summary()
|
|
15
|
+
counts = data.get("counts", {})
|
|
16
|
+
lines = [
|
|
17
|
+
"🧮 **Compute Summary**",
|
|
18
|
+
f"Can deploy: {'yes' if data.get('can_deploy') else 'no'}",
|
|
19
|
+
f"Message: {data.get('message')}",
|
|
20
|
+
f"Accessible nodes: {counts.get('accessible', 0)} total, {counts.get('ready', 0)} ready",
|
|
21
|
+
f"Personal nodes: {counts.get('personal', 0)} total, {counts.get('personal_ready', 0)} ready",
|
|
22
|
+
f"PlugLayer shared nodes: {counts.get('pluglayer', 0)} total, {counts.get('pluglayer_ready', 0)} ready",
|
|
23
|
+
f"Total ready compute: {_fmt_compute(data.get('total_compute'))}",
|
|
24
|
+
f"Personal ready compute: {_fmt_compute(data.get('personal_compute'))}",
|
|
25
|
+
f"Shared ready compute: {_fmt_compute(data.get('pluglayer_compute'))}",
|
|
26
|
+
]
|
|
27
|
+
purchase = data.get("purchase") or {}
|
|
28
|
+
if purchase.get("message"):
|
|
29
|
+
lines.append(f"Purchase: {purchase['message']}")
|
|
30
|
+
return "\n".join(lines)
|
|
31
|
+
except Exception as e:
|
|
32
|
+
return _compact_error("Error loading compute summary", e)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@mcp.tool()
|
|
36
|
+
async def list_nodes(project_id: str = "") -> str:
|
|
37
|
+
"""
|
|
38
|
+
List compute nodes accessible to the authenticated user.
|
|
39
|
+
Compute is account-level; project_id is accepted only for backwards compatibility.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
params = {"project_id": project_id} if project_id else {}
|
|
43
|
+
data = await _client().get("/v1/plugin/compute/nodes", params=params)
|
|
44
|
+
nodes = data.get("nodes", [])
|
|
45
|
+
if not nodes:
|
|
46
|
+
return "No accessible compute nodes found. Add one with add_node_ssh(), or ask an admin to assign shared compute."
|
|
47
|
+
lines = ["Accessible compute nodes:\n"]
|
|
48
|
+
lines.extend(_fmt_node(n) for n in nodes)
|
|
49
|
+
return "\n".join(lines)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
return _compact_error("Error listing compute nodes", e)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@mcp.tool()
|
|
55
|
+
async def add_node_ssh(
|
|
56
|
+
project_id: str,
|
|
57
|
+
name: str,
|
|
58
|
+
host: str,
|
|
59
|
+
ssh_private_key: str,
|
|
60
|
+
user: str = "root",
|
|
61
|
+
port: int = 22,
|
|
62
|
+
) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Add a personal SSH node/VM as account-level compute.
|
|
65
|
+
|
|
66
|
+
project_id is optional/backwards-compatible setup context. The node belongs to the authenticated
|
|
67
|
+
user and can be used by all of that user's projects. Pass an empty string when no project context is needed.
|
|
68
|
+
"""
|
|
69
|
+
if not name or not host or not ssh_private_key:
|
|
70
|
+
return "Missing required fields: name, host, and ssh_private_key are required."
|
|
71
|
+
try:
|
|
72
|
+
payload = {
|
|
73
|
+
"name": name,
|
|
74
|
+
"provider": "ssh",
|
|
75
|
+
"ssh_host": host,
|
|
76
|
+
"ssh_port": port,
|
|
77
|
+
"ssh_user": user,
|
|
78
|
+
"ssh_private_key": ssh_private_key,
|
|
79
|
+
}
|
|
80
|
+
if project_id:
|
|
81
|
+
payload["project_id"] = project_id
|
|
82
|
+
data = await _client().post("/v1/plugin/compute/nodes", payload)
|
|
83
|
+
task_id = data.get("task_id")
|
|
84
|
+
node = data.get("node", {})
|
|
85
|
+
return (
|
|
86
|
+
f"✅ SSH node queued as personal account compute.\n"
|
|
87
|
+
f"Node: **{node.get('name', name)}** (id: `{node.get('id')}`)\n"
|
|
88
|
+
f"Task ID: `{task_id}`\n\n"
|
|
89
|
+
f"⚙️ Installing k3s agent and detecting CPU/RAM/storage/GPU. {_fmt_task_hint(task_id)}"
|
|
90
|
+
)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
return _compact_error("Failed to add SSH node", e)
|