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
@@ -0,0 +1,285 @@
|
|
1
|
+
"""
|
2
|
+
Site operations tools for SiteBay MCP Server
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Optional, List, Dict, Any
|
6
|
+
from ..client import SiteBayClient
|
7
|
+
from ..exceptions import SiteBayError
|
8
|
+
|
9
|
+
|
10
|
+
async def sitebay_site_shell_command(
|
11
|
+
client: SiteBayClient,
|
12
|
+
fqdn: str,
|
13
|
+
command: str
|
14
|
+
) -> str:
|
15
|
+
"""
|
16
|
+
Execute a shell command on a WordPress site (including WP-CLI commands).
|
17
|
+
|
18
|
+
Args:
|
19
|
+
fqdn: The fully qualified domain name of the site
|
20
|
+
command: The shell command to execute (e.g., "wp plugin list")
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
Command output or error message
|
24
|
+
"""
|
25
|
+
try:
|
26
|
+
result = await client.execute_shell_command(fqdn, command)
|
27
|
+
|
28
|
+
# Handle different response formats
|
29
|
+
if isinstance(result, dict):
|
30
|
+
if 'output' in result:
|
31
|
+
output = result['output']
|
32
|
+
elif 'result' in result:
|
33
|
+
output = result['result']
|
34
|
+
else:
|
35
|
+
output = str(result)
|
36
|
+
else:
|
37
|
+
output = str(result)
|
38
|
+
|
39
|
+
response = f"**Command executed on {fqdn}:**\n"
|
40
|
+
response += f"```bash\n{command}\n```\n\n"
|
41
|
+
response += f"**Output:**\n```\n{output}\n```"
|
42
|
+
|
43
|
+
return response
|
44
|
+
|
45
|
+
except SiteBayError as e:
|
46
|
+
return f"Error executing command on {fqdn}: {str(e)}"
|
47
|
+
|
48
|
+
|
49
|
+
async def sitebay_site_edit_file(
|
50
|
+
client: SiteBayClient,
|
51
|
+
fqdn: str,
|
52
|
+
file_path: str,
|
53
|
+
content: str
|
54
|
+
) -> str:
|
55
|
+
"""
|
56
|
+
Edit a file in the site's wp-content directory.
|
57
|
+
|
58
|
+
Args:
|
59
|
+
fqdn: The fully qualified domain name of the site
|
60
|
+
file_path: Path to the file relative to wp-content (e.g., "themes/mytheme/style.css")
|
61
|
+
content: New content for the file
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
Success message or error
|
65
|
+
"""
|
66
|
+
try:
|
67
|
+
result = await client.edit_file(fqdn, file_path, content)
|
68
|
+
|
69
|
+
response = f"✅ **File Updated Successfully**\n\n"
|
70
|
+
response += f"• **Site**: {fqdn}\n"
|
71
|
+
response += f"• **File**: wp-content/{file_path}\n"
|
72
|
+
response += f"• **Content Length**: {len(content)} characters\n"
|
73
|
+
|
74
|
+
if isinstance(result, str) and result:
|
75
|
+
response += f"\n**Server Response:**\n```\n{result}\n```"
|
76
|
+
|
77
|
+
return response
|
78
|
+
|
79
|
+
except SiteBayError as e:
|
80
|
+
return f"Error editing file on {fqdn}: {str(e)}"
|
81
|
+
|
82
|
+
|
83
|
+
async def sitebay_site_get_events(
|
84
|
+
client: SiteBayClient,
|
85
|
+
fqdn: str,
|
86
|
+
after_datetime: Optional[str] = None,
|
87
|
+
limit: int = 20
|
88
|
+
) -> str:
|
89
|
+
"""
|
90
|
+
Get recent events for a site (deployments, updates, restores, etc.).
|
91
|
+
|
92
|
+
Args:
|
93
|
+
fqdn: The fully qualified domain name of the site
|
94
|
+
after_datetime: Optional datetime to filter events after (ISO format)
|
95
|
+
limit: Maximum number of events to return (default: 20)
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
Formatted list of site events
|
99
|
+
"""
|
100
|
+
try:
|
101
|
+
events = await client.get_site_events(fqdn, after_datetime)
|
102
|
+
|
103
|
+
if not events:
|
104
|
+
return f"No events found for {fqdn}."
|
105
|
+
|
106
|
+
# Limit the results
|
107
|
+
events = events[:limit]
|
108
|
+
|
109
|
+
result = f"**Recent Events for {fqdn}** (showing {len(events)} events):\n\n"
|
110
|
+
|
111
|
+
for event in events:
|
112
|
+
result += f"• **{event.get('event_type', 'Unknown Event')}**\n"
|
113
|
+
result += f" - Time: {event.get('created_at', 'Unknown')}\n"
|
114
|
+
result += f" - Status: {event.get('status', 'Unknown')}\n"
|
115
|
+
|
116
|
+
if event.get('description'):
|
117
|
+
result += f" - Description: {event.get('description')}\n"
|
118
|
+
|
119
|
+
if event.get('metadata'):
|
120
|
+
metadata = event.get('metadata')
|
121
|
+
for key, value in metadata.items():
|
122
|
+
result += f" - {key.title()}: {value}\n"
|
123
|
+
|
124
|
+
result += "\n"
|
125
|
+
|
126
|
+
return result
|
127
|
+
|
128
|
+
except SiteBayError as e:
|
129
|
+
return f"Error getting events for {fqdn}: {str(e)}"
|
130
|
+
|
131
|
+
|
132
|
+
async def sitebay_site_external_path_list(
|
133
|
+
client: SiteBayClient,
|
134
|
+
fqdn: str
|
135
|
+
) -> str:
|
136
|
+
"""
|
137
|
+
List external path configurations for a site.
|
138
|
+
|
139
|
+
Args:
|
140
|
+
fqdn: The fully qualified domain name of the site
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
List of external path configurations
|
144
|
+
"""
|
145
|
+
try:
|
146
|
+
paths = await client.list_external_paths(fqdn)
|
147
|
+
|
148
|
+
if not paths:
|
149
|
+
return f"No external paths configured for {fqdn}."
|
150
|
+
|
151
|
+
result = f"**External Paths for {fqdn}**:\n\n"
|
152
|
+
|
153
|
+
for path in paths:
|
154
|
+
result += f"• **Path**: {path.get('path', 'Unknown')}\n"
|
155
|
+
result += f" - Target URL: {path.get('target_url', 'Unknown')}\n"
|
156
|
+
result += f" - Status: {path.get('status', 'Unknown')}\n"
|
157
|
+
result += f" - Created: {path.get('created_at', 'Unknown')}\n"
|
158
|
+
result += f" - ID: {path.get('id', 'Unknown')}\n"
|
159
|
+
result += "\n"
|
160
|
+
|
161
|
+
return result
|
162
|
+
|
163
|
+
except SiteBayError as e:
|
164
|
+
return f"Error listing external paths for {fqdn}: {str(e)}"
|
165
|
+
|
166
|
+
|
167
|
+
async def sitebay_site_external_path_create(
|
168
|
+
client: SiteBayClient,
|
169
|
+
fqdn: str,
|
170
|
+
path: str,
|
171
|
+
target_url: str,
|
172
|
+
description: Optional[str] = None
|
173
|
+
) -> str:
|
174
|
+
"""
|
175
|
+
Create an external path configuration for a site.
|
176
|
+
|
177
|
+
Args:
|
178
|
+
fqdn: The fully qualified domain name of the site
|
179
|
+
path: The path on your site (e.g., "/api")
|
180
|
+
target_url: The external URL to proxy to
|
181
|
+
description: Optional description for the path
|
182
|
+
|
183
|
+
Returns:
|
184
|
+
Success message with path details
|
185
|
+
"""
|
186
|
+
try:
|
187
|
+
path_data = {
|
188
|
+
"path": path,
|
189
|
+
"target_url": target_url,
|
190
|
+
}
|
191
|
+
|
192
|
+
if description:
|
193
|
+
path_data["description"] = description
|
194
|
+
|
195
|
+
external_path = await client.create_external_path(fqdn, path_data)
|
196
|
+
|
197
|
+
result = f"✅ **External Path Created Successfully**\n\n"
|
198
|
+
result += f"• **Site**: {fqdn}\n"
|
199
|
+
result += f"• **Path**: {external_path.get('path')}\n"
|
200
|
+
result += f"• **Target URL**: {external_path.get('target_url')}\n"
|
201
|
+
result += f"• **Status**: {external_path.get('status')}\n"
|
202
|
+
result += f"• **ID**: {external_path.get('id')}\n"
|
203
|
+
|
204
|
+
if description:
|
205
|
+
result += f"• **Description**: {description}\n"
|
206
|
+
|
207
|
+
result += f"\n🔗 Your site path {fqdn}{path} now proxies to {target_url}"
|
208
|
+
|
209
|
+
return result
|
210
|
+
|
211
|
+
except SiteBayError as e:
|
212
|
+
return f"Error creating external path for {fqdn}: {str(e)}"
|
213
|
+
|
214
|
+
|
215
|
+
async def sitebay_site_external_path_update(
|
216
|
+
client: SiteBayClient,
|
217
|
+
fqdn: str,
|
218
|
+
path_id: str,
|
219
|
+
path: Optional[str] = None,
|
220
|
+
target_url: Optional[str] = None,
|
221
|
+
description: Optional[str] = None
|
222
|
+
) -> str:
|
223
|
+
"""
|
224
|
+
Update an external path configuration.
|
225
|
+
|
226
|
+
Args:
|
227
|
+
fqdn: The fully qualified domain name of the site
|
228
|
+
path_id: The ID of the external path to update
|
229
|
+
path: New path value (optional)
|
230
|
+
target_url: New target URL (optional)
|
231
|
+
description: New description (optional)
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
Update confirmation message
|
235
|
+
"""
|
236
|
+
try:
|
237
|
+
path_data = {}
|
238
|
+
|
239
|
+
if path:
|
240
|
+
path_data["path"] = path
|
241
|
+
if target_url:
|
242
|
+
path_data["target_url"] = target_url
|
243
|
+
if description:
|
244
|
+
path_data["description"] = description
|
245
|
+
|
246
|
+
if not path_data:
|
247
|
+
return "No updates specified. Please provide at least one field to update."
|
248
|
+
|
249
|
+
external_path = await client.update_external_path(fqdn, path_id, path_data)
|
250
|
+
|
251
|
+
result = f"✅ **External Path Updated Successfully**\n\n"
|
252
|
+
result += f"• **Site**: {fqdn}\n"
|
253
|
+
result += f"• **Path**: {external_path.get('path')}\n"
|
254
|
+
result += f"• **Target URL**: {external_path.get('target_url')}\n"
|
255
|
+
result += f"• **Status**: {external_path.get('status')}\n"
|
256
|
+
result += f"• **ID**: {external_path.get('id')}\n"
|
257
|
+
|
258
|
+
return result
|
259
|
+
|
260
|
+
except SiteBayError as e:
|
261
|
+
return f"Error updating external path for {fqdn}: {str(e)}"
|
262
|
+
|
263
|
+
|
264
|
+
async def sitebay_site_external_path_delete(
|
265
|
+
client: SiteBayClient,
|
266
|
+
fqdn: str,
|
267
|
+
path_id: str
|
268
|
+
) -> str:
|
269
|
+
"""
|
270
|
+
Delete an external path configuration.
|
271
|
+
|
272
|
+
Args:
|
273
|
+
fqdn: The fully qualified domain name of the site
|
274
|
+
path_id: The ID of the external path to delete
|
275
|
+
|
276
|
+
Returns:
|
277
|
+
Deletion confirmation message
|
278
|
+
"""
|
279
|
+
try:
|
280
|
+
await client.delete_external_path(fqdn, path_id)
|
281
|
+
|
282
|
+
return f"✅ **External Path Deleted Successfully**\n\nExternal path {path_id} has been removed from {fqdn}."
|
283
|
+
|
284
|
+
except SiteBayError as e:
|
285
|
+
return f"Error deleting external path for {fqdn}: {str(e)}"
|
@@ -0,0 +1,248 @@
|
|
1
|
+
"""
|
2
|
+
Site management tools for SiteBay MCP Server
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Optional, Dict, Any, List
|
6
|
+
from ..client import SiteBayClient
|
7
|
+
from ..exceptions import SiteBayError
|
8
|
+
|
9
|
+
|
10
|
+
async def sitebay_list_sites(
|
11
|
+
client: SiteBayClient,
|
12
|
+
team_id: Optional[str] = None
|
13
|
+
) -> str:
|
14
|
+
"""
|
15
|
+
List all WordPress sites for the authenticated user.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
team_id: Optional team ID to filter sites by team
|
19
|
+
|
20
|
+
Returns:
|
21
|
+
Formatted string with site details
|
22
|
+
"""
|
23
|
+
try:
|
24
|
+
sites = await client.list_sites(team_id=team_id)
|
25
|
+
|
26
|
+
if not sites:
|
27
|
+
return "No sites found for your account."
|
28
|
+
|
29
|
+
result = f"Found {len(sites)} site(s):\n\n"
|
30
|
+
|
31
|
+
for site in sites:
|
32
|
+
result += f"• **{site.get('fqdn', 'Unknown')}**\n"
|
33
|
+
result += f" - Status: {site.get('status', 'Unknown')}\n"
|
34
|
+
result += f" - Region: {site.get('region_name', 'Unknown')}\n"
|
35
|
+
result += f" - WordPress Version: {site.get('wp_version', 'Unknown')}\n"
|
36
|
+
result += f" - PHP Version: {site.get('php_version', 'Unknown')}\n"
|
37
|
+
result += f" - Created: {site.get('created_at', 'Unknown')}\n"
|
38
|
+
if site.get('staging_site'):
|
39
|
+
result += f" - Has Staging Site: Yes\n"
|
40
|
+
result += "\n"
|
41
|
+
|
42
|
+
return result
|
43
|
+
|
44
|
+
except SiteBayError as e:
|
45
|
+
return f"Error listing sites: {str(e)}"
|
46
|
+
|
47
|
+
|
48
|
+
async def sitebay_get_site(
|
49
|
+
client: SiteBayClient,
|
50
|
+
fqdn: str
|
51
|
+
) -> str:
|
52
|
+
"""
|
53
|
+
Get detailed information about a specific WordPress site.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
fqdn: The fully qualified domain name of the site
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
Formatted string with detailed site information
|
60
|
+
"""
|
61
|
+
try:
|
62
|
+
site = await client.get_site(fqdn)
|
63
|
+
|
64
|
+
result = f"**Site Details for {fqdn}**\n\n"
|
65
|
+
result += f"• **Status**: {site.get('status', 'Unknown')}\n"
|
66
|
+
result += f"• **Region**: {site.get('region_name', 'Unknown')}\n"
|
67
|
+
result += f"• **WordPress Version**: {site.get('wp_version', 'Unknown')}\n"
|
68
|
+
result += f"• **PHP Version**: {site.get('php_version', 'Unknown')}\n"
|
69
|
+
result += f"• **MySQL Version**: {site.get('mysql_version', 'Unknown')}\n"
|
70
|
+
result += f"• **Site URL**: {site.get('site_url', 'Unknown')}\n"
|
71
|
+
result += f"• **Admin URL**: {site.get('admin_url', 'Unknown')}\n"
|
72
|
+
result += f"• **Created**: {site.get('created_at', 'Unknown')}\n"
|
73
|
+
result += f"• **Updated**: {site.get('updated_at', 'Unknown')}\n"
|
74
|
+
|
75
|
+
if site.get('staging_site'):
|
76
|
+
result += f"• **Staging Site**: Available\n"
|
77
|
+
else:
|
78
|
+
result += f"• **Staging Site**: Not created\n"
|
79
|
+
|
80
|
+
if site.get('git_enabled'):
|
81
|
+
result += f"• **Git Integration**: Enabled\n"
|
82
|
+
if site.get('git_repo'):
|
83
|
+
result += f"• **Git Repository**: {site.get('git_repo')}\n"
|
84
|
+
|
85
|
+
return result
|
86
|
+
|
87
|
+
except SiteBayError as e:
|
88
|
+
return f"Error getting site details: {str(e)}"
|
89
|
+
|
90
|
+
|
91
|
+
async def sitebay_create_site(
|
92
|
+
client: SiteBayClient,
|
93
|
+
fqdn: str,
|
94
|
+
wp_title: str,
|
95
|
+
wp_username: str,
|
96
|
+
wp_password: str,
|
97
|
+
wp_email: str,
|
98
|
+
region_name: Optional[str] = None,
|
99
|
+
template_id: Optional[str] = None,
|
100
|
+
team_id: Optional[str] = None
|
101
|
+
) -> str:
|
102
|
+
"""
|
103
|
+
Create a new WordPress site.
|
104
|
+
|
105
|
+
Args:
|
106
|
+
fqdn: The fully qualified domain name for the new site
|
107
|
+
wp_title: WordPress site title
|
108
|
+
wp_username: WordPress admin username
|
109
|
+
wp_password: WordPress admin password
|
110
|
+
wp_email: WordPress admin email
|
111
|
+
region_name: Optional region name (uses default if not specified)
|
112
|
+
template_id: Optional template ID to use for site creation
|
113
|
+
team_id: Optional team ID to create site under
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
Formatted string with new site details
|
117
|
+
"""
|
118
|
+
try:
|
119
|
+
site_data = {
|
120
|
+
"fqdn": fqdn,
|
121
|
+
"wp_title": wp_title,
|
122
|
+
"wp_username": wp_username,
|
123
|
+
"wp_password": wp_password,
|
124
|
+
"wp_email": wp_email,
|
125
|
+
}
|
126
|
+
|
127
|
+
if region_name:
|
128
|
+
site_data["region_name"] = region_name
|
129
|
+
if template_id:
|
130
|
+
site_data["template_id"] = template_id
|
131
|
+
if team_id:
|
132
|
+
site_data["team_id"] = team_id
|
133
|
+
|
134
|
+
site = await client.create_site(site_data)
|
135
|
+
|
136
|
+
result = f"✅ **Site Created Successfully!**\n\n"
|
137
|
+
result += f"• **Domain**: {site.get('fqdn')}\n"
|
138
|
+
result += f"• **Status**: {site.get('status')}\n"
|
139
|
+
result += f"• **Region**: {site.get('region_name')}\n"
|
140
|
+
result += f"• **Site URL**: {site.get('site_url')}\n"
|
141
|
+
result += f"• **Admin URL**: {site.get('admin_url')}\n"
|
142
|
+
result += f"• **WordPress Admin**: {wp_username}\n"
|
143
|
+
result += f"• **WordPress Email**: {wp_email}\n"
|
144
|
+
result += f"\n🚀 Your WordPress site is being deployed and will be ready shortly!"
|
145
|
+
|
146
|
+
return result
|
147
|
+
|
148
|
+
except SiteBayError as e:
|
149
|
+
return f"Error creating site: {str(e)}"
|
150
|
+
|
151
|
+
|
152
|
+
async def sitebay_update_site(
|
153
|
+
client: SiteBayClient,
|
154
|
+
fqdn: str,
|
155
|
+
wp_title: Optional[str] = None,
|
156
|
+
wp_username: Optional[str] = None,
|
157
|
+
wp_password: Optional[str] = None,
|
158
|
+
wp_email: Optional[str] = None,
|
159
|
+
php_version: Optional[str] = None
|
160
|
+
) -> str:
|
161
|
+
"""
|
162
|
+
Update an existing WordPress site configuration.
|
163
|
+
|
164
|
+
Args:
|
165
|
+
fqdn: The fully qualified domain name of the site to update
|
166
|
+
wp_title: New WordPress site title
|
167
|
+
wp_username: New WordPress admin username
|
168
|
+
wp_password: New WordPress admin password
|
169
|
+
wp_email: New WordPress admin email
|
170
|
+
php_version: New PHP version (e.g., "8.1", "8.2")
|
171
|
+
|
172
|
+
Returns:
|
173
|
+
Formatted string with update confirmation
|
174
|
+
"""
|
175
|
+
try:
|
176
|
+
site_data = {}
|
177
|
+
|
178
|
+
if wp_title:
|
179
|
+
site_data["wp_title"] = wp_title
|
180
|
+
if wp_username:
|
181
|
+
site_data["wp_username"] = wp_username
|
182
|
+
if wp_password:
|
183
|
+
site_data["wp_password"] = wp_password
|
184
|
+
if wp_email:
|
185
|
+
site_data["wp_email"] = wp_email
|
186
|
+
if php_version:
|
187
|
+
site_data["php_version"] = php_version
|
188
|
+
|
189
|
+
if not site_data:
|
190
|
+
return "No updates specified. Please provide at least one field to update."
|
191
|
+
|
192
|
+
site = await client.update_site(fqdn, site_data)
|
193
|
+
|
194
|
+
result = f"✅ **Site Updated Successfully!**\n\n"
|
195
|
+
result += f"• **Domain**: {site.get('fqdn')}\n"
|
196
|
+
result += f"• **Status**: {site.get('status')}\n"
|
197
|
+
|
198
|
+
if wp_title:
|
199
|
+
result += f"• **Title**: Updated to '{wp_title}'\n"
|
200
|
+
if wp_username:
|
201
|
+
result += f"• **Admin Username**: Updated to '{wp_username}'\n"
|
202
|
+
if wp_password:
|
203
|
+
result += f"• **Admin Password**: Updated\n"
|
204
|
+
if wp_email:
|
205
|
+
result += f"• **Admin Email**: Updated to '{wp_email}'\n"
|
206
|
+
if php_version:
|
207
|
+
result += f"• **PHP Version**: Updated to {php_version}\n"
|
208
|
+
|
209
|
+
return result
|
210
|
+
|
211
|
+
except SiteBayError as e:
|
212
|
+
return f"Error updating site: {str(e)}"
|
213
|
+
|
214
|
+
|
215
|
+
async def sitebay_delete_site(
|
216
|
+
client: SiteBayClient,
|
217
|
+
fqdn: str,
|
218
|
+
confirm: bool = False
|
219
|
+
) -> str:
|
220
|
+
"""
|
221
|
+
Delete a WordPress site permanently.
|
222
|
+
|
223
|
+
Args:
|
224
|
+
fqdn: The fully qualified domain name of the site to delete
|
225
|
+
confirm: Must be True to actually delete the site (safety check)
|
226
|
+
|
227
|
+
Returns:
|
228
|
+
Confirmation message or error
|
229
|
+
"""
|
230
|
+
if not confirm:
|
231
|
+
return (
|
232
|
+
f"⚠️ **CONFIRMATION REQUIRED**\n\n"
|
233
|
+
f"You are about to permanently delete the site: **{fqdn}**\n\n"
|
234
|
+
f"This action will:\n"
|
235
|
+
f"• Delete all website files and content\n"
|
236
|
+
f"• Delete the database and all data\n"
|
237
|
+
f"• Remove any staging sites\n"
|
238
|
+
f"• Cannot be undone\n\n"
|
239
|
+
f"To proceed with deletion, call this function again with confirm=True"
|
240
|
+
)
|
241
|
+
|
242
|
+
try:
|
243
|
+
await client.delete_site(fqdn)
|
244
|
+
|
245
|
+
return f"✅ **Site Deleted Successfully**\n\nThe site {fqdn} has been permanently deleted."
|
246
|
+
|
247
|
+
except SiteBayError as e:
|
248
|
+
return f"Error deleting site: {str(e)}"
|