sitebay-mcp 0.1.1751179164__py3-none-any.whl
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.
- sitebay_mcp/__init__.py +15 -0
- sitebay_mcp/auth.py +54 -0
- sitebay_mcp/client.py +359 -0
- sitebay_mcp/exceptions.py +45 -0
- sitebay_mcp/resources.py +171 -0
- sitebay_mcp/server.py +909 -0
- sitebay_mcp/tools/__init__.py +5 -0
- sitebay_mcp/tools/operations.py +285 -0
- sitebay_mcp/tools/sites.py +248 -0
- sitebay_mcp-0.1.1751179164.dist-info/METADATA +271 -0
- sitebay_mcp-0.1.1751179164.dist-info/RECORD +14 -0
- sitebay_mcp-0.1.1751179164.dist-info/WHEEL +4 -0
- sitebay_mcp-0.1.1751179164.dist-info/entry_points.txt +2 -0
- sitebay_mcp-0.1.1751179164.dist-info/licenses/LICENSE +21 -0
sitebay_mcp/__init__.py
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
"""
|
2
|
+
SiteBay MCP Server
|
3
|
+
|
4
|
+
Provides Model Context Protocol (MCP) integration for SiteBay WordPress hosting platform.
|
5
|
+
Allows Claude Code users to manage WordPress sites, execute commands, handle staging,
|
6
|
+
backups, and more through natural language interactions.
|
7
|
+
"""
|
8
|
+
|
9
|
+
__version__ = "0.1.0"
|
10
|
+
__author__ = "SiteBay"
|
11
|
+
__email__ = "support@sitebay.org"
|
12
|
+
|
13
|
+
from .server import main
|
14
|
+
|
15
|
+
__all__ = ["main"]
|
sitebay_mcp/auth.py
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
"""
|
2
|
+
Authentication handling for SiteBay API
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
from typing import Optional
|
7
|
+
from .exceptions import AuthenticationError, ConfigurationError
|
8
|
+
|
9
|
+
|
10
|
+
class SiteBayAuth:
|
11
|
+
"""Handles SiteBay API authentication"""
|
12
|
+
|
13
|
+
def __init__(self, api_token: Optional[str] = None):
|
14
|
+
"""
|
15
|
+
Initialize authentication with API token
|
16
|
+
|
17
|
+
Args:
|
18
|
+
api_token: SiteBay API token. If not provided, will try to get from environment
|
19
|
+
"""
|
20
|
+
self.api_token = api_token or self._get_token_from_env()
|
21
|
+
if not self.api_token:
|
22
|
+
raise ConfigurationError(
|
23
|
+
"SiteBay API token is required. Set SITEBAY_API_TOKEN environment variable "
|
24
|
+
"or pass token directly to the server."
|
25
|
+
)
|
26
|
+
|
27
|
+
def _get_token_from_env(self) -> Optional[str]:
|
28
|
+
"""Get API token from environment variables"""
|
29
|
+
return os.getenv("SITEBAY_API_TOKEN")
|
30
|
+
|
31
|
+
def get_headers(self) -> dict[str, str]:
|
32
|
+
"""Get authentication headers for API requests"""
|
33
|
+
if not self.api_token:
|
34
|
+
raise AuthenticationError("No API token available")
|
35
|
+
|
36
|
+
return {
|
37
|
+
"Authorization": f"Bearer {self.api_token}",
|
38
|
+
"Content-Type": "application/json",
|
39
|
+
"Accept": "application/json",
|
40
|
+
}
|
41
|
+
|
42
|
+
def validate_token(self) -> bool:
|
43
|
+
"""
|
44
|
+
Validate that the API token is properly formatted
|
45
|
+
This is a basic validation - actual verification happens on API calls
|
46
|
+
"""
|
47
|
+
if not self.api_token:
|
48
|
+
return False
|
49
|
+
|
50
|
+
# Basic token format validation
|
51
|
+
if len(self.api_token) < 20: # Reasonable minimum length
|
52
|
+
return False
|
53
|
+
|
54
|
+
return True
|
sitebay_mcp/client.py
ADDED
@@ -0,0 +1,359 @@
|
|
1
|
+
"""
|
2
|
+
SiteBay API client for handling all API communications
|
3
|
+
"""
|
4
|
+
|
5
|
+
import httpx
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
7
|
+
from .auth import SiteBayAuth
|
8
|
+
from .exceptions import APIError, AuthenticationError, SiteNotFoundError, ValidationError
|
9
|
+
|
10
|
+
|
11
|
+
class SiteBayClient:
|
12
|
+
"""Client for interacting with SiteBay API"""
|
13
|
+
|
14
|
+
BASE_URL = "https://my.sitebay.org"
|
15
|
+
API_PREFIX = "/f/api/v1"
|
16
|
+
|
17
|
+
def __init__(self, auth: SiteBayAuth, timeout: float = 30.0):
|
18
|
+
"""
|
19
|
+
Initialize SiteBay API client
|
20
|
+
|
21
|
+
Args:
|
22
|
+
auth: Authentication instance
|
23
|
+
timeout: Request timeout in seconds
|
24
|
+
"""
|
25
|
+
self.auth = auth
|
26
|
+
self.timeout = timeout
|
27
|
+
self._client = httpx.AsyncClient(
|
28
|
+
base_url=self.BASE_URL,
|
29
|
+
timeout=timeout,
|
30
|
+
headers=self.auth.get_headers()
|
31
|
+
)
|
32
|
+
|
33
|
+
async def __aenter__(self):
|
34
|
+
"""Async context manager entry"""
|
35
|
+
return self
|
36
|
+
|
37
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
38
|
+
"""Async context manager exit"""
|
39
|
+
await self._client.aclose()
|
40
|
+
|
41
|
+
async def close(self):
|
42
|
+
"""Close the HTTP client"""
|
43
|
+
await self._client.aclose()
|
44
|
+
|
45
|
+
def _get_url(self, endpoint: str) -> str:
|
46
|
+
"""Get full URL for an API endpoint"""
|
47
|
+
if not endpoint.startswith("/"):
|
48
|
+
endpoint = "/" + endpoint
|
49
|
+
return f"{self.API_PREFIX}{endpoint}"
|
50
|
+
|
51
|
+
def _format_validation_error(self, error_data: dict) -> str:
|
52
|
+
"""
|
53
|
+
Format validation error details for better readability
|
54
|
+
|
55
|
+
Args:
|
56
|
+
error_data: Error response from API
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
Formatted error message with field-specific details
|
60
|
+
"""
|
61
|
+
if not error_data:
|
62
|
+
return "Validation failed"
|
63
|
+
|
64
|
+
# Handle FastAPI/Pydantic validation error format
|
65
|
+
if "detail" in error_data:
|
66
|
+
detail = error_data["detail"]
|
67
|
+
|
68
|
+
# If detail is a string, return it directly
|
69
|
+
if isinstance(detail, str):
|
70
|
+
return f"Validation Error: {detail}"
|
71
|
+
|
72
|
+
# If detail is a list of validation errors
|
73
|
+
if isinstance(detail, list):
|
74
|
+
error_messages = []
|
75
|
+
for error in detail:
|
76
|
+
if isinstance(error, dict):
|
77
|
+
loc = error.get("loc", [])
|
78
|
+
msg = error.get("msg", "Invalid value")
|
79
|
+
field = " -> ".join(str(x) for x in loc) if loc else "unknown field"
|
80
|
+
error_messages.append(f"{field}: {msg}")
|
81
|
+
|
82
|
+
if error_messages:
|
83
|
+
return f"Validation Error:\n" + "\n".join(f" • {msg}" for msg in error_messages)
|
84
|
+
|
85
|
+
# Handle other error formats
|
86
|
+
if "message" in error_data:
|
87
|
+
return f"Validation Error: {error_data['message']}"
|
88
|
+
|
89
|
+
# Fallback - try to extract any useful information
|
90
|
+
if "errors" in error_data:
|
91
|
+
errors = error_data["errors"]
|
92
|
+
if isinstance(errors, dict):
|
93
|
+
error_messages = []
|
94
|
+
for field, messages in errors.items():
|
95
|
+
if isinstance(messages, list):
|
96
|
+
for msg in messages:
|
97
|
+
error_messages.append(f"{field}: {msg}")
|
98
|
+
else:
|
99
|
+
error_messages.append(f"{field}: {messages}")
|
100
|
+
|
101
|
+
if error_messages:
|
102
|
+
return f"Validation Error:\n" + "\n".join(f" • {msg}" for msg in error_messages)
|
103
|
+
|
104
|
+
return f"Validation Error: {str(error_data)}"
|
105
|
+
|
106
|
+
def _extract_field_errors(self, error_data: dict) -> dict:
|
107
|
+
"""
|
108
|
+
Extract field-specific errors for programmatic access
|
109
|
+
|
110
|
+
Args:
|
111
|
+
error_data: Error response from API
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
Dictionary mapping field names to error messages
|
115
|
+
"""
|
116
|
+
field_errors = {}
|
117
|
+
|
118
|
+
if not error_data:
|
119
|
+
return field_errors
|
120
|
+
|
121
|
+
# Handle FastAPI/Pydantic validation error format
|
122
|
+
if "detail" in error_data and isinstance(error_data["detail"], list):
|
123
|
+
for error in error_data["detail"]:
|
124
|
+
if isinstance(error, dict):
|
125
|
+
loc = error.get("loc", [])
|
126
|
+
msg = error.get("msg", "Invalid value")
|
127
|
+
field = " -> ".join(str(x) for x in loc) if loc else "unknown"
|
128
|
+
field_errors[field] = msg
|
129
|
+
|
130
|
+
# Handle other error formats
|
131
|
+
elif "errors" in error_data and isinstance(error_data["errors"], dict):
|
132
|
+
for field, messages in error_data["errors"].items():
|
133
|
+
if isinstance(messages, list):
|
134
|
+
field_errors[field] = "; ".join(messages)
|
135
|
+
else:
|
136
|
+
field_errors[field] = str(messages)
|
137
|
+
|
138
|
+
return field_errors
|
139
|
+
|
140
|
+
async def _request(
|
141
|
+
self,
|
142
|
+
method: str,
|
143
|
+
endpoint: str,
|
144
|
+
params: Optional[Dict[str, Any]] = None,
|
145
|
+
json_data: Optional[Dict[str, Any]] = None,
|
146
|
+
**kwargs
|
147
|
+
) -> Any:
|
148
|
+
"""
|
149
|
+
Make an HTTP request to the SiteBay API
|
150
|
+
|
151
|
+
Args:
|
152
|
+
method: HTTP method (GET, POST, PATCH, DELETE)
|
153
|
+
endpoint: API endpoint path
|
154
|
+
params: Query parameters
|
155
|
+
json_data: JSON body data
|
156
|
+
**kwargs: Additional httpx request arguments
|
157
|
+
|
158
|
+
Returns:
|
159
|
+
Response data (parsed JSON or raw response)
|
160
|
+
|
161
|
+
Raises:
|
162
|
+
APIError: For API errors
|
163
|
+
AuthenticationError: For authentication failures
|
164
|
+
"""
|
165
|
+
url = self._get_url(endpoint)
|
166
|
+
|
167
|
+
try:
|
168
|
+
response = await self._client.request(
|
169
|
+
method=method,
|
170
|
+
url=url,
|
171
|
+
params=params,
|
172
|
+
json=json_data,
|
173
|
+
**kwargs
|
174
|
+
)
|
175
|
+
|
176
|
+
# Handle different response codes
|
177
|
+
if response.status_code == 401:
|
178
|
+
raise AuthenticationError("Invalid or expired API token")
|
179
|
+
elif response.status_code == 404:
|
180
|
+
raise SiteNotFoundError("Requested resource not found")
|
181
|
+
elif response.status_code == 422:
|
182
|
+
# Handle validation errors with detailed information
|
183
|
+
try:
|
184
|
+
error_data = response.json()
|
185
|
+
error_message = self._format_validation_error(error_data)
|
186
|
+
field_errors = self._extract_field_errors(error_data)
|
187
|
+
except Exception:
|
188
|
+
error_message = f"Validation Error: {response.text}"
|
189
|
+
error_data = None
|
190
|
+
field_errors = {}
|
191
|
+
|
192
|
+
raise ValidationError(
|
193
|
+
message=error_message,
|
194
|
+
field_errors=field_errors
|
195
|
+
)
|
196
|
+
elif response.status_code >= 400:
|
197
|
+
try:
|
198
|
+
error_data = response.json()
|
199
|
+
error_message = error_data.get("detail", f"API Error: {response.status_code}")
|
200
|
+
except Exception:
|
201
|
+
error_message = f"API Error: {response.status_code} - {response.text}"
|
202
|
+
|
203
|
+
raise APIError(
|
204
|
+
message=error_message,
|
205
|
+
status_code=response.status_code,
|
206
|
+
response_data=error_data if 'error_data' in locals() else None
|
207
|
+
)
|
208
|
+
|
209
|
+
# Try to parse JSON response
|
210
|
+
try:
|
211
|
+
return response.json()
|
212
|
+
except Exception:
|
213
|
+
# Return raw response if not JSON
|
214
|
+
return response.text
|
215
|
+
|
216
|
+
except httpx.RequestError as e:
|
217
|
+
raise APIError(f"Network error: {str(e)}")
|
218
|
+
|
219
|
+
async def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
220
|
+
"""Make GET request"""
|
221
|
+
return await self._request("GET", endpoint, params=params)
|
222
|
+
|
223
|
+
async def post(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
224
|
+
"""Make POST request"""
|
225
|
+
return await self._request("POST", endpoint, params=params, json_data=json_data)
|
226
|
+
|
227
|
+
async def patch(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
228
|
+
"""Make PATCH request"""
|
229
|
+
return await self._request("PATCH", endpoint, params=params, json_data=json_data)
|
230
|
+
|
231
|
+
async def delete(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
232
|
+
"""Make DELETE request"""
|
233
|
+
return await self._request("DELETE", endpoint, params=params)
|
234
|
+
|
235
|
+
# Site Management Methods
|
236
|
+
async def list_sites(self, team_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
237
|
+
"""List all sites for the user"""
|
238
|
+
params = {"team_id": team_id} if team_id else None
|
239
|
+
response = await self.get("/site", params=params)
|
240
|
+
return response.get("results", [])
|
241
|
+
|
242
|
+
async def get_site(self, fqdn: str) -> Dict[str, Any]:
|
243
|
+
"""Get details for a specific site"""
|
244
|
+
return await self.get(f"/site/{fqdn}")
|
245
|
+
|
246
|
+
async def create_site(self, site_data: Dict[str, Any]) -> Dict[str, Any]:
|
247
|
+
"""Create a new site"""
|
248
|
+
return await self.post("/site", json_data=site_data)
|
249
|
+
|
250
|
+
async def update_site(self, fqdn: str, site_data: Dict[str, Any]) -> Dict[str, Any]:
|
251
|
+
"""Update an existing site"""
|
252
|
+
return await self.patch(f"/site/{fqdn}", json_data=site_data)
|
253
|
+
|
254
|
+
async def delete_site(self, fqdn: str) -> Dict[str, Any]:
|
255
|
+
"""Delete a site"""
|
256
|
+
return await self.delete(f"/site/{fqdn}")
|
257
|
+
|
258
|
+
# Site Operations Methods
|
259
|
+
async def execute_shell_command(self, fqdn: str, command: str) -> Any:
|
260
|
+
"""Execute a shell command on a site"""
|
261
|
+
return await self.post(f"/site/{fqdn}/cmd", json_data={"cmd": command})
|
262
|
+
|
263
|
+
async def edit_file(self, fqdn: str, file_path: str, content: str) -> str:
|
264
|
+
"""Edit a file in the site's wp-content directory"""
|
265
|
+
return await self.post(
|
266
|
+
f"/site/{fqdn}/wpfile_diff_edit",
|
267
|
+
json_data={"path": file_path, "content": content}
|
268
|
+
)
|
269
|
+
|
270
|
+
async def get_site_events(self, fqdn: str, after_datetime: Optional[str] = None) -> List[Dict[str, Any]]:
|
271
|
+
"""Get site events"""
|
272
|
+
params = {"after_datetime": after_datetime} if after_datetime else None
|
273
|
+
response = await self.get(f"/site/{fqdn}/event", params=params)
|
274
|
+
return response.get("results", [])
|
275
|
+
|
276
|
+
# Staging Methods
|
277
|
+
async def create_staging_site(self, fqdn: str, staging_data: Dict[str, Any]) -> Dict[str, Any]:
|
278
|
+
"""Create a staging site"""
|
279
|
+
return await self.post(f"/site/{fqdn}/stage", json_data=staging_data)
|
280
|
+
|
281
|
+
async def delete_staging_site(self, fqdn: str) -> Dict[str, Any]:
|
282
|
+
"""Delete a staging site"""
|
283
|
+
return await self.delete(f"/site/{fqdn}/stage")
|
284
|
+
|
285
|
+
async def commit_staging_site(self, fqdn: str) -> Dict[str, Any]:
|
286
|
+
"""Commit staging site to live"""
|
287
|
+
return await self.post(f"/site/{fqdn}/stage/commit")
|
288
|
+
|
289
|
+
# Backup/Restore Methods
|
290
|
+
async def get_backup_commits(self, fqdn: str, number_to_fetch: int = 10) -> List[Dict[str, Any]]:
|
291
|
+
"""Get backup commits for a site"""
|
292
|
+
params = {"number_to_fetch": number_to_fetch}
|
293
|
+
return await self.get(f"/site/{fqdn}/pit_restore/commits", params=params)
|
294
|
+
|
295
|
+
async def create_restore(self, fqdn: str, restore_data: Dict[str, Any]) -> Dict[str, Any]:
|
296
|
+
"""Create a point-in-time restore"""
|
297
|
+
return await self.post(f"/site/{fqdn}/pit_restore", json_data=restore_data)
|
298
|
+
|
299
|
+
async def list_restores(self, fqdn: str) -> List[Dict[str, Any]]:
|
300
|
+
"""List all restores for a site"""
|
301
|
+
response = await self.get(f"/site/{fqdn}/pit_restore")
|
302
|
+
return response.get("results", [])
|
303
|
+
|
304
|
+
# External Path Methods
|
305
|
+
async def list_external_paths(self, fqdn: str) -> List[Dict[str, Any]]:
|
306
|
+
"""List external paths for a site"""
|
307
|
+
response = await self.get(f"/site/{fqdn}/external_path")
|
308
|
+
return response.get("results", [])
|
309
|
+
|
310
|
+
async def create_external_path(self, fqdn: str, path_data: Dict[str, Any]) -> Dict[str, Any]:
|
311
|
+
"""Create an external path"""
|
312
|
+
return await self.post(f"/site/{fqdn}/external_path", json_data=path_data)
|
313
|
+
|
314
|
+
async def update_external_path(self, fqdn: str, path_id: str, path_data: Dict[str, Any]) -> Dict[str, Any]:
|
315
|
+
"""Update an external path"""
|
316
|
+
return await self.patch(f"/site/{fqdn}/external_path/{path_id}", json_data=path_data)
|
317
|
+
|
318
|
+
async def delete_external_path(self, fqdn: str, path_id: str) -> Dict[str, Any]:
|
319
|
+
"""Delete an external path"""
|
320
|
+
return await self.delete(f"/site/{fqdn}/external_path/{path_id}")
|
321
|
+
|
322
|
+
# Proxy Methods
|
323
|
+
async def wordpress_proxy(self, proxy_data: Dict[str, Any]) -> Any:
|
324
|
+
"""Proxy request to WordPress API"""
|
325
|
+
return await self.post("/wp-proxy", json_data=proxy_data)
|
326
|
+
|
327
|
+
async def shopify_proxy(self, proxy_data: Dict[str, Any]) -> Any:
|
328
|
+
"""Proxy request to Shopify API"""
|
329
|
+
return await self.post("/shopify-proxy", json_data=proxy_data)
|
330
|
+
|
331
|
+
async def posthog_proxy(self, proxy_data: Dict[str, Any]) -> Any:
|
332
|
+
"""Proxy request to PostHog API"""
|
333
|
+
return await self.post("/posthog-proxy", json_data=proxy_data)
|
334
|
+
|
335
|
+
# Team Methods
|
336
|
+
async def list_teams(self) -> List[Dict[str, Any]]:
|
337
|
+
"""List user teams"""
|
338
|
+
response = await self.get("/team")
|
339
|
+
return response.get("results", [])
|
340
|
+
|
341
|
+
# Template and Region Methods
|
342
|
+
async def list_templates(self) -> List[Dict[str, Any]]:
|
343
|
+
"""List available templates"""
|
344
|
+
response = await self.get("/template")
|
345
|
+
return response.get("results", [])
|
346
|
+
|
347
|
+
async def list_regions(self) -> List[Dict[str, Any]]:
|
348
|
+
"""List available regions"""
|
349
|
+
return await self.get("/region")
|
350
|
+
|
351
|
+
# Account Methods
|
352
|
+
async def get_affiliate_referrals(self) -> List[Dict[str, Any]]:
|
353
|
+
"""Get affiliate referrals"""
|
354
|
+
response = await self.get("/account/referred_user")
|
355
|
+
return response.get("results", [])
|
356
|
+
|
357
|
+
async def create_checkout_session(self, checkout_data: Dict[str, Any]) -> Dict[str, Any]:
|
358
|
+
"""Create Stripe checkout session"""
|
359
|
+
return await self.post("/create_checkout_session", json_data=checkout_data)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
"""
|
2
|
+
Custom exceptions for SiteBay MCP Server
|
3
|
+
"""
|
4
|
+
|
5
|
+
|
6
|
+
class SiteBayError(Exception):
|
7
|
+
"""Base exception for SiteBay MCP operations"""
|
8
|
+
pass
|
9
|
+
|
10
|
+
|
11
|
+
class AuthenticationError(SiteBayError):
|
12
|
+
"""Raised when authentication fails"""
|
13
|
+
pass
|
14
|
+
|
15
|
+
|
16
|
+
class APIError(SiteBayError):
|
17
|
+
"""Raised when SiteBay API returns an error"""
|
18
|
+
|
19
|
+
def __init__(self, message: str, status_code: int = None, response_data: dict = None):
|
20
|
+
super().__init__(message)
|
21
|
+
self.status_code = status_code
|
22
|
+
self.response_data = response_data
|
23
|
+
|
24
|
+
|
25
|
+
class ValidationError(SiteBayError):
|
26
|
+
"""Raised when request validation fails"""
|
27
|
+
|
28
|
+
def __init__(self, message: str, field_errors: dict = None):
|
29
|
+
super().__init__(message)
|
30
|
+
self.field_errors = field_errors or {}
|
31
|
+
|
32
|
+
|
33
|
+
class SiteNotFoundError(SiteBayError):
|
34
|
+
"""Raised when requested site is not found"""
|
35
|
+
pass
|
36
|
+
|
37
|
+
|
38
|
+
class TeamNotFoundError(SiteBayError):
|
39
|
+
"""Raised when requested team is not found"""
|
40
|
+
pass
|
41
|
+
|
42
|
+
|
43
|
+
class ConfigurationError(SiteBayError):
|
44
|
+
"""Raised when configuration is invalid"""
|
45
|
+
pass
|
sitebay_mcp/resources.py
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
"""
|
2
|
+
SiteBay MCP Resources
|
3
|
+
|
4
|
+
Provides readable resources for site configurations, logs, and metrics.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import json
|
8
|
+
from typing import Any, Dict, List
|
9
|
+
from fastmcp.server import Context
|
10
|
+
from .client import SiteBayClient
|
11
|
+
from .exceptions import SiteBayError
|
12
|
+
|
13
|
+
|
14
|
+
async def get_site_config_resource(ctx: Context, site_fqdn: str) -> str:
|
15
|
+
"""
|
16
|
+
Get site configuration as a resource.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
ctx: FastMCP context
|
20
|
+
site_fqdn: Site domain name
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
JSON formatted site configuration
|
24
|
+
"""
|
25
|
+
try:
|
26
|
+
ctx.logger.info(f"Fetching configuration resource for: {site_fqdn}")
|
27
|
+
|
28
|
+
from .server import initialize_client
|
29
|
+
client = await initialize_client()
|
30
|
+
|
31
|
+
site = await client.get_site(site_fqdn)
|
32
|
+
|
33
|
+
# Format as readable configuration
|
34
|
+
config = {
|
35
|
+
"site_info": {
|
36
|
+
"domain": site.get("fqdn"),
|
37
|
+
"title": site.get("wp_title"),
|
38
|
+
"status": site.get("status"),
|
39
|
+
"region": site.get("region_name"),
|
40
|
+
"created": site.get("created_at"),
|
41
|
+
"updated": site.get("updated_at")
|
42
|
+
},
|
43
|
+
"technical_specs": {
|
44
|
+
"php_version": site.get("php_version"),
|
45
|
+
"mysql_version": site.get("mysql_version"),
|
46
|
+
"wordpress_version": site.get("wp_version"),
|
47
|
+
"git_enabled": site.get("git_enabled", False)
|
48
|
+
},
|
49
|
+
"urls": {
|
50
|
+
"site_url": site.get("site_url"),
|
51
|
+
"admin_url": site.get("admin_url")
|
52
|
+
},
|
53
|
+
"features": {
|
54
|
+
"staging_available": bool(site.get("staging_site")),
|
55
|
+
"git_integration": site.get("git_enabled", False),
|
56
|
+
"backup_enabled": True # SiteBay always has backups
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
return json.dumps(config, indent=2)
|
61
|
+
|
62
|
+
except SiteBayError as e:
|
63
|
+
ctx.logger.error(f"Error fetching config for {site_fqdn}: {str(e)}")
|
64
|
+
return f"Error: {str(e)}"
|
65
|
+
|
66
|
+
|
67
|
+
async def get_site_events_resource(ctx: Context, site_fqdn: str, limit: int = 50) -> str:
|
68
|
+
"""
|
69
|
+
Get site events/logs as a resource.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
ctx: FastMCP context
|
73
|
+
site_fqdn: Site domain name
|
74
|
+
limit: Maximum number of events to fetch
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
JSON formatted site events
|
78
|
+
"""
|
79
|
+
try:
|
80
|
+
ctx.logger.info(f"Fetching events resource for: {site_fqdn}")
|
81
|
+
|
82
|
+
from .server import initialize_client
|
83
|
+
client = await initialize_client()
|
84
|
+
|
85
|
+
events = await client.get_site_events(site_fqdn)
|
86
|
+
|
87
|
+
# Limit and format events
|
88
|
+
limited_events = events[:limit]
|
89
|
+
|
90
|
+
formatted_events = {
|
91
|
+
"site": site_fqdn,
|
92
|
+
"total_events": len(events),
|
93
|
+
"showing": len(limited_events),
|
94
|
+
"events": [
|
95
|
+
{
|
96
|
+
"timestamp": event.get("created_at"),
|
97
|
+
"type": event.get("event_type"),
|
98
|
+
"status": event.get("status"),
|
99
|
+
"description": event.get("description"),
|
100
|
+
"metadata": event.get("metadata", {})
|
101
|
+
}
|
102
|
+
for event in limited_events
|
103
|
+
]
|
104
|
+
}
|
105
|
+
|
106
|
+
return json.dumps(formatted_events, indent=2)
|
107
|
+
|
108
|
+
except SiteBayError as e:
|
109
|
+
ctx.logger.error(f"Error fetching events for {site_fqdn}: {str(e)}")
|
110
|
+
return f"Error: {str(e)}"
|
111
|
+
|
112
|
+
|
113
|
+
async def get_account_summary_resource(ctx: Context) -> str:
|
114
|
+
"""
|
115
|
+
Get account summary as a resource.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
ctx: FastMCP context
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
JSON formatted account summary
|
122
|
+
"""
|
123
|
+
try:
|
124
|
+
ctx.logger.info("Fetching account summary resource")
|
125
|
+
|
126
|
+
from .server import initialize_client
|
127
|
+
client = await initialize_client()
|
128
|
+
|
129
|
+
# Get sites and teams in parallel
|
130
|
+
sites = await client.list_sites()
|
131
|
+
teams = await client.list_teams()
|
132
|
+
regions = await client.list_regions()
|
133
|
+
templates = await client.list_templates()
|
134
|
+
|
135
|
+
summary = {
|
136
|
+
"account_overview": {
|
137
|
+
"total_sites": len(sites),
|
138
|
+
"total_teams": len(teams),
|
139
|
+
"available_regions": len(regions),
|
140
|
+
"available_templates": len(templates)
|
141
|
+
},
|
142
|
+
"sites_by_status": {},
|
143
|
+
"sites_by_region": {},
|
144
|
+
"recent_sites": []
|
145
|
+
}
|
146
|
+
|
147
|
+
# Analyze sites
|
148
|
+
for site in sites:
|
149
|
+
status = site.get("status", "unknown")
|
150
|
+
summary["sites_by_status"][status] = summary["sites_by_status"].get(status, 0) + 1
|
151
|
+
|
152
|
+
region = site.get("region_name", "unknown")
|
153
|
+
summary["sites_by_region"][region] = summary["sites_by_region"].get(region, 0) + 1
|
154
|
+
|
155
|
+
# Get 5 most recent sites
|
156
|
+
sorted_sites = sorted(sites, key=lambda x: x.get("created_at", ""), reverse=True)
|
157
|
+
summary["recent_sites"] = [
|
158
|
+
{
|
159
|
+
"domain": site.get("fqdn"),
|
160
|
+
"status": site.get("status"),
|
161
|
+
"created": site.get("created_at"),
|
162
|
+
"region": site.get("region_name")
|
163
|
+
}
|
164
|
+
for site in sorted_sites[:5]
|
165
|
+
]
|
166
|
+
|
167
|
+
return json.dumps(summary, indent=2)
|
168
|
+
|
169
|
+
except SiteBayError as e:
|
170
|
+
ctx.logger.error(f"Error fetching account summary: {str(e)}")
|
171
|
+
return f"Error: {str(e)}"
|