sitebay-mcp 0.1.1751285665__py3-none-any.whl → 0.1.1757492007__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 +2 -2
- sitebay_mcp/client.py +34 -48
- sitebay_mcp/resources.py +6 -50
- sitebay_mcp/server.py +263 -339
- sitebay_mcp/tools/operations.py +45 -129
- sitebay_mcp/tools/sites.py +94 -87
- {sitebay_mcp-0.1.1751285665.dist-info → sitebay_mcp-0.1.1757492007.dist-info}/METADATA +67 -31
- sitebay_mcp-0.1.1757492007.dist-info/RECORD +14 -0
- sitebay_mcp-0.1.1751285665.dist-info/RECORD +0 -14
- {sitebay_mcp-0.1.1751285665.dist-info → sitebay_mcp-0.1.1757492007.dist-info}/WHEEL +0 -0
- {sitebay_mcp-0.1.1751285665.dist-info → sitebay_mcp-0.1.1757492007.dist-info}/entry_points.txt +0 -0
- {sitebay_mcp-0.1.1751285665.dist-info → sitebay_mcp-0.1.1757492007.dist-info}/licenses/LICENSE +0 -0
sitebay_mcp/server.py
CHANGED
@@ -5,8 +5,11 @@ Main server implementation that provides MCP tools for SiteBay WordPress hosting
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
import asyncio
|
8
|
+
import os
|
8
9
|
import sys
|
10
|
+
import argparse
|
9
11
|
from typing import Any, Optional
|
12
|
+
from pydantic import UUID4
|
10
13
|
|
11
14
|
from fastmcp import FastMCP
|
12
15
|
from fastmcp.server import Context
|
@@ -33,9 +36,12 @@ async def initialize_client() -> SiteBayClient:
|
|
33
36
|
try:
|
34
37
|
auth = SiteBayAuth()
|
35
38
|
sitebay_client = SiteBayClient(auth)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
# Test the connection using a lightweight, public endpoint
|
40
|
+
try:
|
41
|
+
await sitebay_client.list_ready_made_sites()
|
42
|
+
except Exception:
|
43
|
+
# Non-fatal for initialization; auth/transport issues will surface later
|
44
|
+
pass
|
39
45
|
|
40
46
|
except Exception as e:
|
41
47
|
raise ConfigurationError(f"Failed to initialize SiteBay client: {str(e)}")
|
@@ -45,12 +51,12 @@ async def initialize_client() -> SiteBayClient:
|
|
45
51
|
|
46
52
|
# Site Management Tools
|
47
53
|
@mcp.tool
|
48
|
-
async def sitebay_list_sites(ctx: Context, team_id: Optional[
|
54
|
+
async def sitebay_list_sites(ctx: Context, team_id: Optional[UUID4] = None) -> str:
|
49
55
|
"""
|
50
56
|
List all WordPress sites for the authenticated user.
|
51
57
|
|
52
58
|
Args:
|
53
|
-
team_id: Optional team ID to filter sites by team
|
59
|
+
team_id: Optional team ID (UUID4) to filter sites by team
|
54
60
|
|
55
61
|
Returns:
|
56
62
|
Formatted string with site details including domain, status, region, and versions
|
@@ -61,7 +67,8 @@ async def sitebay_list_sites(ctx: Context, team_id: Optional[str] = None) -> str
|
|
61
67
|
await ctx.info(f"Filtering by team ID: {team_id}")
|
62
68
|
|
63
69
|
client = await initialize_client()
|
64
|
-
|
70
|
+
team_id_str = str(team_id) if team_id is not None else None
|
71
|
+
result = await sites.sitebay_list_sites(client, team_id_str)
|
65
72
|
|
66
73
|
await ctx.info("Successfully retrieved site list")
|
67
74
|
return result
|
@@ -108,27 +115,33 @@ async def sitebay_get_site(ctx: Context, fqdn: str) -> str:
|
|
108
115
|
@mcp.tool
|
109
116
|
async def sitebay_create_site(
|
110
117
|
ctx: Context,
|
118
|
+
team_id: str,
|
111
119
|
fqdn: str,
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
120
|
+
wordpress_blog_name: str,
|
121
|
+
wordpress_first_name: str,
|
122
|
+
wordpress_last_name: str,
|
123
|
+
wordpress_email: str,
|
124
|
+
wordpress_username: str,
|
125
|
+
wordpress_password: str,
|
126
|
+
git_url: Optional[str] = None,
|
127
|
+
ready_made_site_name: Optional[str] = None,
|
128
|
+
is_free: Optional[bool] = None,
|
119
129
|
) -> str:
|
120
130
|
"""
|
121
|
-
Create a new WordPress site.
|
131
|
+
Create a new WordPress site (SiteLiveCreate schema).
|
122
132
|
|
123
133
|
Args:
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
134
|
+
team_id: Team UUID that owns the site
|
135
|
+
fqdn: The fully qualified domain name for the new site (e.g., "www.example.org")
|
136
|
+
wordpress_blog_name: Blog/site title
|
137
|
+
wordpress_first_name: Admin first name
|
138
|
+
wordpress_last_name: Admin last name
|
139
|
+
wordpress_email: Admin email address
|
140
|
+
wordpress_username: Admin username
|
141
|
+
wordpress_password: Admin password (strong)
|
142
|
+
git_url: Optional Git repository URL
|
143
|
+
ready_made_site_name: Optional ready-made site name
|
144
|
+
is_free: Optional flag for free plan
|
132
145
|
|
133
146
|
Returns:
|
134
147
|
Success message with new site details and access information
|
@@ -136,22 +149,29 @@ async def sitebay_create_site(
|
|
136
149
|
try:
|
137
150
|
await ctx.info(f"Starting site creation for: {fqdn}")
|
138
151
|
|
139
|
-
# Progress reporting
|
140
|
-
|
141
152
|
client = await initialize_client()
|
142
153
|
|
143
|
-
|
144
154
|
# Basic validation
|
145
155
|
if not fqdn or '.' not in fqdn:
|
146
156
|
raise ValueError("Invalid domain name provided")
|
147
|
-
|
157
|
+
if not team_id:
|
158
|
+
raise ValueError("team_id is required")
|
148
159
|
|
149
160
|
result = await sites.sitebay_create_site(
|
150
|
-
client,
|
151
|
-
|
161
|
+
client,
|
162
|
+
team_id,
|
163
|
+
fqdn,
|
164
|
+
wordpress_blog_name,
|
165
|
+
wordpress_first_name,
|
166
|
+
wordpress_last_name,
|
167
|
+
wordpress_email,
|
168
|
+
wordpress_username,
|
169
|
+
wordpress_password,
|
170
|
+
git_url,
|
171
|
+
ready_made_site_name,
|
172
|
+
is_free,
|
152
173
|
)
|
153
174
|
|
154
|
-
|
155
175
|
await ctx.info(f"Successfully created site: {fqdn}")
|
156
176
|
return result
|
157
177
|
|
@@ -160,15 +180,11 @@ async def sitebay_create_site(
|
|
160
180
|
return f"❌ Validation Error: {str(e)}"
|
161
181
|
except ValidationError as e:
|
162
182
|
await ctx.error(f"SiteBay validation error creating site {fqdn}: {str(e)}")
|
163
|
-
|
164
|
-
# Provide detailed feedback for the agent with field-specific errors
|
165
183
|
error_msg = f"❌ Validation Error - Please check your input:\n{str(e)}\n"
|
166
|
-
|
167
184
|
if hasattr(e, 'field_errors') and e.field_errors:
|
168
185
|
error_msg += "\nSpecific field errors:\n"
|
169
186
|
for field, msg in e.field_errors.items():
|
170
187
|
error_msg += f" • {field}: {msg}\n"
|
171
|
-
|
172
188
|
error_msg += "\nPlease adjust your parameters and try again."
|
173
189
|
return error_msg
|
174
190
|
except SiteBayError as e:
|
@@ -182,51 +198,64 @@ async def sitebay_create_site(
|
|
182
198
|
@mcp.tool
|
183
199
|
async def sitebay_update_site(
|
184
200
|
fqdn: str,
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
201
|
+
cf_dev_mode_enabled: Optional[bool] = None,
|
202
|
+
new_fqdn: Optional[str] = None,
|
203
|
+
git_url: Optional[str] = None,
|
204
|
+
http_auth_enabled: Optional[bool] = None,
|
205
|
+
team_id: Optional[str] = None,
|
206
|
+
is_free: Optional[bool] = None,
|
190
207
|
) -> str:
|
191
208
|
"""
|
192
|
-
Update an existing
|
209
|
+
Update an existing SiteBay site configuration.
|
193
210
|
|
194
211
|
Args:
|
195
|
-
fqdn:
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
212
|
+
fqdn: Site domain to update
|
213
|
+
cf_dev_mode_enabled: Enable/disable Cloudflare dev mode
|
214
|
+
new_fqdn: Change the site domain
|
215
|
+
git_url: Set repository URL for deployments
|
216
|
+
http_auth_enabled: Enable/disable HTTP basic auth
|
217
|
+
team_id: Move site to a different team
|
218
|
+
is_free: Toggle free plan flag
|
201
219
|
|
202
220
|
Returns:
|
203
221
|
Confirmation message with updated settings
|
204
222
|
"""
|
205
223
|
client = await initialize_client()
|
206
224
|
return await sites.sitebay_update_site(
|
207
|
-
client,
|
225
|
+
client,
|
226
|
+
fqdn,
|
227
|
+
cf_dev_mode_enabled=cf_dev_mode_enabled,
|
228
|
+
new_fqdn=new_fqdn,
|
229
|
+
git_url=git_url,
|
230
|
+
http_auth_enabled=http_auth_enabled,
|
231
|
+
team_id=team_id,
|
232
|
+
is_free=is_free,
|
208
233
|
)
|
209
234
|
|
210
235
|
|
211
236
|
@mcp.tool
|
212
|
-
async def sitebay_delete_site(fqdn: str
|
237
|
+
async def sitebay_delete_site(fqdn: str) -> str:
|
213
238
|
"""
|
214
239
|
Delete a WordPress site permanently. This action cannot be undone.
|
215
240
|
|
216
241
|
Args:
|
217
242
|
fqdn: The fully qualified domain name of the site to delete
|
218
|
-
confirm: Must be True to actually delete the site (safety check)
|
219
243
|
|
220
244
|
Returns:
|
221
|
-
Confirmation message
|
245
|
+
Confirmation message
|
222
246
|
"""
|
223
247
|
client = await initialize_client()
|
224
|
-
return await sites.sitebay_delete_site(client, fqdn
|
248
|
+
return await sites.sitebay_delete_site(client, fqdn)
|
225
249
|
|
226
250
|
|
227
251
|
# Site Operations Tools
|
228
252
|
@mcp.tool
|
229
|
-
async def sitebay_site_shell_command(
|
253
|
+
async def sitebay_site_shell_command(
|
254
|
+
fqdn: str,
|
255
|
+
command: str,
|
256
|
+
cwd: Optional[str] = None,
|
257
|
+
auto_track_dir: Optional[bool] = None,
|
258
|
+
) -> str:
|
230
259
|
"""
|
231
260
|
Execute a shell command on a WordPress site. Supports WP-CLI commands, system commands, etc.
|
232
261
|
|
@@ -238,11 +267,17 @@ async def sitebay_site_shell_command(fqdn: str, command: str) -> str:
|
|
238
267
|
Command output and execution details
|
239
268
|
"""
|
240
269
|
client = await initialize_client()
|
241
|
-
return await operations.sitebay_site_shell_command(
|
270
|
+
return await operations.sitebay_site_shell_command(
|
271
|
+
client, fqdn, command, cwd=cwd, auto_track_dir=auto_track_dir
|
272
|
+
)
|
242
273
|
|
243
274
|
|
244
275
|
@mcp.tool
|
245
|
-
async def sitebay_site_edit_file(
|
276
|
+
async def sitebay_site_edit_file(
|
277
|
+
fqdn: str,
|
278
|
+
file_path: str,
|
279
|
+
file_edit_using_search_replace_blocks: str,
|
280
|
+
) -> str:
|
246
281
|
"""
|
247
282
|
Edit a file in the site's wp-content directory.
|
248
283
|
|
@@ -255,137 +290,53 @@ async def sitebay_site_edit_file(fqdn: str, file_path: str, content: str) -> str
|
|
255
290
|
Success confirmation with file details
|
256
291
|
"""
|
257
292
|
client = await initialize_client()
|
258
|
-
return await operations.sitebay_site_edit_file(
|
259
|
-
|
260
|
-
|
261
|
-
@mcp.tool
|
262
|
-
async def sitebay_site_get_events(
|
263
|
-
fqdn: str,
|
264
|
-
after_datetime: Optional[str] = None,
|
265
|
-
limit: int = 20
|
266
|
-
) -> str:
|
267
|
-
"""
|
268
|
-
Get recent events for a site (deployments, updates, restores, etc.).
|
269
|
-
|
270
|
-
Args:
|
271
|
-
fqdn: The fully qualified domain name of the site
|
272
|
-
after_datetime: Optional ISO datetime to filter events after (e.g., "2024-01-01T00:00:00Z")
|
273
|
-
limit: Maximum number of events to return (default: 20)
|
274
|
-
|
275
|
-
Returns:
|
276
|
-
Formatted list of recent site events with timestamps and details
|
277
|
-
"""
|
278
|
-
client = await initialize_client()
|
279
|
-
return await operations.sitebay_site_get_events(client, fqdn, after_datetime, limit)
|
293
|
+
return await operations.sitebay_site_edit_file(
|
294
|
+
client, fqdn, file_path, file_edit_using_search_replace_blocks
|
295
|
+
)
|
280
296
|
|
281
297
|
|
282
|
-
|
283
|
-
async def sitebay_site_external_path_list(fqdn: str) -> str:
|
284
|
-
"""
|
285
|
-
List external path configurations for a site (URL proxying/routing).
|
286
|
-
|
287
|
-
Args:
|
288
|
-
fqdn: The fully qualified domain name of the site
|
289
|
-
|
290
|
-
Returns:
|
291
|
-
List of configured external paths with their target URLs and status
|
292
|
-
"""
|
293
|
-
client = await initialize_client()
|
294
|
-
return await operations.sitebay_site_external_path_list(client, fqdn)
|
298
|
+
# Site events tool removed (not present in schema)
|
295
299
|
|
296
300
|
|
297
|
-
|
298
|
-
async def sitebay_site_external_path_create(
|
299
|
-
fqdn: str,
|
300
|
-
path: str,
|
301
|
-
target_url: str,
|
302
|
-
description: Optional[str] = None
|
303
|
-
) -> str:
|
304
|
-
"""
|
305
|
-
Create an external path configuration to proxy requests to external URLs.
|
306
|
-
|
307
|
-
Args:
|
308
|
-
fqdn: The fully qualified domain name of the site
|
309
|
-
path: The path on your site (e.g., "/api", "/external")
|
310
|
-
target_url: The external URL to proxy requests to
|
311
|
-
description: Optional description for this path configuration
|
312
|
-
|
313
|
-
Returns:
|
314
|
-
Success message with external path details
|
315
|
-
"""
|
316
|
-
client = await initialize_client()
|
317
|
-
return await operations.sitebay_site_external_path_create(
|
318
|
-
client, fqdn, path, target_url, description
|
319
|
-
)
|
301
|
+
# External path tools removed (no longer supported)
|
320
302
|
|
321
303
|
|
322
304
|
# Helper Tools
|
323
|
-
@mcp.tool
|
324
|
-
async def sitebay_list_regions() -> str:
|
325
|
-
"""
|
326
|
-
List all available hosting regions for site deployment.
|
327
|
-
|
328
|
-
Returns:
|
329
|
-
List of available regions with their details
|
330
|
-
"""
|
331
|
-
try:
|
332
|
-
client = await initialize_client()
|
333
|
-
regions = await client.list_regions()
|
334
|
-
|
335
|
-
if not regions:
|
336
|
-
return "No regions available."
|
337
|
-
|
338
|
-
result = f"**Available Hosting Regions** ({len(regions)} regions):\n\n"
|
339
|
-
|
340
|
-
for region in regions:
|
341
|
-
result += f"• **{region.get('name', 'Unknown')}**\n"
|
342
|
-
result += f" - Location: {region.get('location', 'Unknown')}\n"
|
343
|
-
result += f" - Status: {region.get('status', 'Unknown')}\n"
|
344
|
-
|
345
|
-
if region.get('description'):
|
346
|
-
result += f" - Description: {region.get('description')}\n"
|
347
|
-
|
348
|
-
result += "\n"
|
349
|
-
|
350
|
-
return result
|
351
|
-
|
352
|
-
except SiteBayError as e:
|
353
|
-
return f"Error listing regions: {str(e)}"
|
354
305
|
|
355
306
|
|
356
307
|
@mcp.tool
|
357
|
-
async def
|
308
|
+
async def sitebay_list_ready_made_sites() -> str:
|
358
309
|
"""
|
359
|
-
List
|
310
|
+
List available ready-made sites for quick launches.
|
360
311
|
|
361
312
|
Returns:
|
362
|
-
List of
|
313
|
+
List of ready-made sites with descriptions
|
363
314
|
"""
|
364
315
|
try:
|
365
316
|
client = await initialize_client()
|
366
|
-
|
317
|
+
items = await client.list_ready_made_sites()
|
367
318
|
|
368
|
-
if not
|
369
|
-
return "No
|
319
|
+
if not items:
|
320
|
+
return "No ready-made sites available."
|
370
321
|
|
371
|
-
result = f"**Available
|
322
|
+
result = f"**Available Ready-made Sites** ({len(items)}):\n\n"
|
372
323
|
|
373
|
-
for
|
374
|
-
result += f"• **{
|
375
|
-
result += f" - ID: {
|
324
|
+
for item in items:
|
325
|
+
result += f"• **{item.get('name', 'Unknown')}**\n"
|
326
|
+
result += f" - ID: {item.get('id', 'Unknown')}\n"
|
376
327
|
|
377
|
-
if
|
378
|
-
result += f" - Description: {
|
328
|
+
if item.get('description'):
|
329
|
+
result += f" - Description: {item.get('description')}\n"
|
379
330
|
|
380
|
-
if
|
381
|
-
result += f" - Category: {
|
331
|
+
if item.get('category'):
|
332
|
+
result += f" - Category: {item.get('category')}\n"
|
382
333
|
|
383
334
|
result += "\n"
|
384
335
|
|
385
336
|
return result
|
386
337
|
|
387
338
|
except SiteBayError as e:
|
388
|
-
return f"Error listing
|
339
|
+
return f"Error listing ready-made sites: {str(e)}"
|
389
340
|
|
390
341
|
|
391
342
|
@mcp.tool
|
@@ -433,34 +384,32 @@ async def sitebay_list_teams(ctx: Context) -> str:
|
|
433
384
|
@mcp.tool
|
434
385
|
async def sitebay_wordpress_proxy(
|
435
386
|
ctx: Context,
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
387
|
+
fqdn: str,
|
388
|
+
path: Optional[str] = None,
|
389
|
+
query_params_json: Optional[str] = None,
|
390
|
+
method: str = "get",
|
440
391
|
) -> str:
|
441
392
|
"""
|
442
393
|
Proxy requests to a WordPress site's REST API.
|
443
394
|
|
444
395
|
Args:
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
396
|
+
fqdn: The site domain
|
397
|
+
path: WordPress API path (e.g., "/wp-json/wp/v2/posts")
|
398
|
+
query_params_json: Optional JSON string for payload or query params
|
399
|
+
method: HTTP method (get, post, put, delete)
|
449
400
|
|
450
401
|
Returns:
|
451
402
|
WordPress API response
|
452
403
|
"""
|
453
404
|
try:
|
454
|
-
await ctx.info(f"WordPress proxy request to {
|
405
|
+
await ctx.info(f"WordPress proxy request to {fqdn}{path or ''}")
|
455
406
|
|
456
407
|
client = await initialize_client()
|
457
|
-
proxy_data: dict[str, Any] = {
|
458
|
-
|
459
|
-
"
|
460
|
-
|
461
|
-
|
462
|
-
if data:
|
463
|
-
proxy_data["data"] = data
|
408
|
+
proxy_data: dict[str, Any] = {"fqdn": fqdn, "method": method}
|
409
|
+
if path is not None:
|
410
|
+
proxy_data["path"] = path
|
411
|
+
if query_params_json is not None:
|
412
|
+
proxy_data["query_params_json"] = query_params_json
|
464
413
|
|
465
414
|
result = await client.wordpress_proxy(proxy_data)
|
466
415
|
return f"✅ WordPress API Response:\n```json\n{result}\n```"
|
@@ -476,37 +425,32 @@ async def sitebay_wordpress_proxy(
|
|
476
425
|
@mcp.tool
|
477
426
|
async def sitebay_shopify_proxy(
|
478
427
|
ctx: Context,
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
method: str = "
|
483
|
-
data: Optional[dict] = None
|
428
|
+
shop_name: str,
|
429
|
+
path: Optional[str] = None,
|
430
|
+
query_params_json: Optional[str] = None,
|
431
|
+
method: str = "get",
|
484
432
|
) -> str:
|
485
433
|
"""
|
486
434
|
Proxy requests to a Shopify Admin API.
|
487
435
|
|
488
436
|
Args:
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
method: HTTP method (
|
493
|
-
data: Optional data for POST/PUT requests
|
437
|
+
shop_name: Shopify shop name
|
438
|
+
path: Shopify API path (e.g., "/admin/api/2024-04/products.json")
|
439
|
+
query_params_json: Optional JSON string for payload or query params
|
440
|
+
method: HTTP method (get, post, put, delete)
|
494
441
|
|
495
442
|
Returns:
|
496
443
|
Shopify API response
|
497
444
|
"""
|
498
445
|
try:
|
499
|
-
await ctx.info(f"Shopify proxy request to {
|
446
|
+
await ctx.info(f"Shopify proxy request to {shop_name}{path or ''}")
|
500
447
|
|
501
448
|
client = await initialize_client()
|
502
|
-
proxy_data: dict[str, Any] = {
|
503
|
-
|
504
|
-
"
|
505
|
-
|
506
|
-
"
|
507
|
-
}
|
508
|
-
if data:
|
509
|
-
proxy_data["data"] = data
|
449
|
+
proxy_data: dict[str, Any] = {"shop_name": shop_name, "method": method}
|
450
|
+
if path is not None:
|
451
|
+
proxy_data["path"] = path
|
452
|
+
if query_params_json is not None:
|
453
|
+
proxy_data["query_params_json"] = query_params_json
|
510
454
|
|
511
455
|
result = await client.shopify_proxy(proxy_data)
|
512
456
|
return f"✅ Shopify API Response:\n```json\n{result}\n```"
|
@@ -522,31 +466,28 @@ async def sitebay_shopify_proxy(
|
|
522
466
|
@mcp.tool
|
523
467
|
async def sitebay_posthog_proxy(
|
524
468
|
ctx: Context,
|
525
|
-
|
526
|
-
|
527
|
-
|
469
|
+
path: str,
|
470
|
+
query_params_json: Optional[str] = None,
|
471
|
+
method: str = "get",
|
528
472
|
) -> str:
|
529
473
|
"""
|
530
474
|
Proxy POST requests to PostHog analytics API.
|
531
475
|
|
532
476
|
Args:
|
533
|
-
|
534
|
-
|
535
|
-
|
477
|
+
path: PostHog API path
|
478
|
+
query_params_json: Optional JSON string for payload or query params
|
479
|
+
method: HTTP method (get, post, put, delete)
|
536
480
|
|
537
481
|
Returns:
|
538
482
|
PostHog API response
|
539
483
|
"""
|
540
484
|
try:
|
541
|
-
await ctx.info(f"PostHog proxy request to {
|
485
|
+
await ctx.info(f"PostHog proxy request to {path}")
|
542
486
|
|
543
487
|
client = await initialize_client()
|
544
|
-
proxy_data: dict[str, Any] = {
|
545
|
-
|
546
|
-
"
|
547
|
-
}
|
548
|
-
if api_key:
|
549
|
-
proxy_data["api_key"] = api_key
|
488
|
+
proxy_data: dict[str, Any] = {"path": path, "method": method}
|
489
|
+
if query_params_json is not None:
|
490
|
+
proxy_data["query_params_json"] = query_params_json
|
550
491
|
|
551
492
|
result = await client.posthog_proxy(proxy_data)
|
552
493
|
return f"✅ PostHog API Response:\n```json\n{result}\n```"
|
@@ -559,102 +500,7 @@ async def sitebay_posthog_proxy(
|
|
559
500
|
return f"❌ Unexpected error: {str(e)}"
|
560
501
|
|
561
502
|
|
562
|
-
# Staging
|
563
|
-
@mcp.tool
|
564
|
-
async def sitebay_staging_create(
|
565
|
-
ctx: Context,
|
566
|
-
fqdn: str,
|
567
|
-
staging_subdomain: Optional[str] = None
|
568
|
-
) -> str:
|
569
|
-
"""
|
570
|
-
Create a staging site for testing changes.
|
571
|
-
|
572
|
-
Args:
|
573
|
-
fqdn: The live site domain
|
574
|
-
staging_subdomain: Optional custom staging subdomain
|
575
|
-
|
576
|
-
Returns:
|
577
|
-
Staging site creation confirmation
|
578
|
-
"""
|
579
|
-
try:
|
580
|
-
await ctx.info(f"Creating staging site for {fqdn}")
|
581
|
-
|
582
|
-
|
583
|
-
client = await initialize_client()
|
584
|
-
staging_data = {}
|
585
|
-
if staging_subdomain:
|
586
|
-
staging_data["staging_subdomain"] = staging_subdomain
|
587
|
-
|
588
|
-
result = await client.create_staging_site(fqdn, staging_data)
|
589
|
-
|
590
|
-
|
591
|
-
await ctx.info(f"Successfully created staging site for {fqdn}")
|
592
|
-
return f"✅ **Staging Site Created**\n\nStaging environment for {fqdn} is now available for testing changes safely."
|
593
|
-
|
594
|
-
except SiteBayError as e:
|
595
|
-
await ctx.error(f"Error creating staging site: {str(e)}")
|
596
|
-
return f"❌ Staging Creation Error: {str(e)}"
|
597
|
-
except Exception as e:
|
598
|
-
await ctx.error(f"Unexpected staging error: {str(e)}")
|
599
|
-
return f"❌ Unexpected error: {str(e)}"
|
600
|
-
|
601
|
-
|
602
|
-
@mcp.tool
|
603
|
-
async def sitebay_staging_delete(ctx: Context, fqdn: str) -> str:
|
604
|
-
"""
|
605
|
-
Delete the staging site.
|
606
|
-
|
607
|
-
Args:
|
608
|
-
fqdn: The live site domain
|
609
|
-
|
610
|
-
Returns:
|
611
|
-
Staging deletion confirmation
|
612
|
-
"""
|
613
|
-
try:
|
614
|
-
await ctx.info(f"Deleting staging site for {fqdn}")
|
615
|
-
|
616
|
-
client = await initialize_client()
|
617
|
-
await client.delete_staging_site(fqdn)
|
618
|
-
|
619
|
-
await ctx.info(f"Successfully deleted staging site for {fqdn}")
|
620
|
-
return f"✅ **Staging Site Deleted**\n\nThe staging environment for {fqdn} has been removed."
|
621
|
-
|
622
|
-
except SiteBayError as e:
|
623
|
-
await ctx.error(f"Error deleting staging site: {str(e)}")
|
624
|
-
return f"❌ Staging Deletion Error: {str(e)}"
|
625
|
-
except Exception as e:
|
626
|
-
await ctx.error(f"Unexpected staging error: {str(e)}")
|
627
|
-
return f"❌ Unexpected error: {str(e)}"
|
628
|
-
|
629
|
-
|
630
|
-
@mcp.tool
|
631
|
-
async def sitebay_staging_commit(ctx: Context, fqdn: str) -> str:
|
632
|
-
"""
|
633
|
-
Commit staging changes to live site (sync staging to live).
|
634
|
-
|
635
|
-
Args:
|
636
|
-
fqdn: The live site domain
|
637
|
-
|
638
|
-
Returns:
|
639
|
-
Staging commit confirmation
|
640
|
-
"""
|
641
|
-
try:
|
642
|
-
await ctx.info(f"Committing staging changes for {fqdn}")
|
643
|
-
|
644
|
-
|
645
|
-
client = await initialize_client()
|
646
|
-
result = await client.commit_staging_site(fqdn)
|
647
|
-
|
648
|
-
|
649
|
-
await ctx.info(f"Successfully committed staging for {fqdn}")
|
650
|
-
return f"✅ **Staging Committed to Live**\n\nChanges from staging have been synchronized to the live site {fqdn}."
|
651
|
-
|
652
|
-
except SiteBayError as e:
|
653
|
-
await ctx.error(f"Error committing staging: {str(e)}")
|
654
|
-
return f"❌ Staging Commit Error: {str(e)}"
|
655
|
-
except Exception as e:
|
656
|
-
await ctx.error(f"Unexpected staging error: {str(e)}")
|
657
|
-
return f"❌ Unexpected error: {str(e)}"
|
503
|
+
# Staging tools removed (no longer supported)
|
658
504
|
|
659
505
|
|
660
506
|
# Backup/Restore Tools
|
@@ -708,37 +554,57 @@ async def sitebay_backup_list_commits(
|
|
708
554
|
async def sitebay_backup_restore(
|
709
555
|
ctx: Context,
|
710
556
|
fqdn: str,
|
711
|
-
|
712
|
-
|
557
|
+
restore_point: Optional[str] = None,
|
558
|
+
for_stage_site: Optional[bool] = None,
|
559
|
+
restore_db: Optional[bool] = None,
|
560
|
+
restore_wp_content: Optional[bool] = None,
|
561
|
+
delete_extra_files: Optional[bool] = None,
|
562
|
+
dolt_restore_hash: Optional[str] = None,
|
563
|
+
is_dry_run: Optional[bool] = None,
|
713
564
|
) -> str:
|
714
565
|
"""
|
715
566
|
Restore a site to a previous point in time.
|
716
567
|
|
717
|
-
Args:
|
568
|
+
Args (PITRestoreCreate schema):
|
718
569
|
fqdn: The site domain
|
719
|
-
|
720
|
-
|
570
|
+
restore_point: ISO datetime string (or omit for latest)
|
571
|
+
for_stage_site: Whether to restore the stage site
|
572
|
+
restore_db: Restore database (default true)
|
573
|
+
restore_wp_content: Restore wp-content (default true)
|
574
|
+
delete_extra_files: Delete extra files from target (default false)
|
575
|
+
dolt_restore_hash: Optional Dolt hash to restore DB
|
576
|
+
is_dry_run: Simulate restore without applying changes
|
721
577
|
|
722
578
|
Returns:
|
723
579
|
Restore operation confirmation
|
724
580
|
"""
|
725
581
|
try:
|
726
582
|
await ctx.info(f"Starting point-in-time restore for {fqdn}")
|
727
|
-
|
728
|
-
|
729
583
|
client = await initialize_client()
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
584
|
+
|
585
|
+
restore_data: dict[str, Any] = {}
|
586
|
+
if restore_point is not None:
|
587
|
+
restore_data["restore_point"] = restore_point
|
588
|
+
if for_stage_site is not None:
|
589
|
+
restore_data["for_stage_site"] = for_stage_site
|
590
|
+
if restore_db is not None:
|
591
|
+
restore_data["restore_db"] = restore_db
|
592
|
+
if restore_wp_content is not None:
|
593
|
+
restore_data["restore_wp_content"] = restore_wp_content
|
594
|
+
if delete_extra_files is not None:
|
595
|
+
restore_data["delete_extra_files"] = delete_extra_files
|
596
|
+
if dolt_restore_hash is not None:
|
597
|
+
restore_data["dolt_restore_hash"] = dolt_restore_hash
|
598
|
+
if is_dry_run is not None:
|
599
|
+
restore_data["is_dry_run"] = is_dry_run
|
600
|
+
|
736
601
|
result = await client.create_restore(fqdn, restore_data)
|
737
|
-
|
738
|
-
|
602
|
+
|
739
603
|
await ctx.info(f"Successfully initiated restore for {fqdn}")
|
740
|
-
return
|
741
|
-
|
604
|
+
return (
|
605
|
+
"✅ **Point-in-Time Restore Initiated**\n\n"
|
606
|
+
f"Restore operation for {fqdn} has been started."
|
607
|
+
)
|
742
608
|
except SiteBayError as e:
|
743
609
|
await ctx.error(f"Error starting restore: {str(e)}")
|
744
610
|
return f"❌ Restore Error: {str(e)}"
|
@@ -841,19 +707,7 @@ async def site_config_resource(ctx: Context, site_fqdn: str) -> str:
|
|
841
707
|
return await resources.get_site_config_resource(ctx, site_fqdn)
|
842
708
|
|
843
709
|
|
844
|
-
|
845
|
-
async def site_events_resource(ctx: Context, site_fqdn: str, limit: int = 50) -> str:
|
846
|
-
"""
|
847
|
-
Get site events and logs as a readable resource.
|
848
|
-
|
849
|
-
Args:
|
850
|
-
site_fqdn: The fully qualified domain name of the site
|
851
|
-
limit: Maximum number of events to return (default: 50)
|
852
|
-
|
853
|
-
Returns:
|
854
|
-
JSON formatted site events and deployment logs
|
855
|
-
"""
|
856
|
-
return await resources.get_site_events_resource(ctx, site_fqdn, limit)
|
710
|
+
# Site events resource removed (not present in schema)
|
857
711
|
|
858
712
|
|
859
713
|
@mcp.resource("sitebay://account/summary")
|
@@ -862,7 +716,7 @@ async def account_summary_resource(ctx: Context) -> str:
|
|
862
716
|
Get account summary as a readable resource.
|
863
717
|
|
864
718
|
Returns:
|
865
|
-
JSON formatted account overview including site counts,
|
719
|
+
JSON formatted account overview including site counts, ready-made catalog size, and recent activity
|
866
720
|
"""
|
867
721
|
return await resources.get_account_summary_resource(ctx)
|
868
722
|
|
@@ -874,16 +728,86 @@ async def cleanup():
|
|
874
728
|
await sitebay_client.close()
|
875
729
|
|
876
730
|
|
731
|
+
def _run_stdio():
|
732
|
+
"""Run the MCP server over STDIO (default)."""
|
733
|
+
mcp.run()
|
734
|
+
|
735
|
+
|
736
|
+
def _run_http(host: str, port: int):
|
737
|
+
"""Run the MCP server over HTTP (streamable)."""
|
738
|
+
# FastMCP >= 2.9 provides HTTP transport via run_http
|
739
|
+
if not hasattr(mcp, "run_http"):
|
740
|
+
raise RuntimeError(
|
741
|
+
"FastMCP does not support HTTP transport in this environment. "
|
742
|
+
"Please upgrade fastmcp to >= 2.9."
|
743
|
+
)
|
744
|
+
print(f"Starting SiteBay MCP HTTP server on http://{host}:{port}")
|
745
|
+
mcp.run_http(host=host, port=port)
|
746
|
+
|
747
|
+
|
877
748
|
def main():
|
878
|
-
"""Main entry point for the MCP server
|
749
|
+
"""Main entry point for the MCP server.
|
750
|
+
|
751
|
+
Supports both STDIO (default) and HTTP transport. Use one of:
|
752
|
+
- stdio (default): `sitebay-mcp`
|
753
|
+
- http: `sitebay-mcp --http --port 7823 --host 0.0.0.0`
|
754
|
+
|
755
|
+
Environment variables (used if flags not provided):
|
756
|
+
- MCP_TRANSPORT=stdio|http
|
757
|
+
- MCP_HTTP_HOST (default: 127.0.0.1)
|
758
|
+
- MCP_HTTP_PORT or PORT (default: 7823)
|
759
|
+
"""
|
760
|
+
parser = argparse.ArgumentParser(prog="sitebay-mcp", add_help=True)
|
761
|
+
parser.add_argument(
|
762
|
+
"--http",
|
763
|
+
action="store_true",
|
764
|
+
help="Run the MCP server using HTTP transport (streamable)",
|
765
|
+
)
|
766
|
+
parser.add_argument(
|
767
|
+
"--transport",
|
768
|
+
choices=["stdio", "http"],
|
769
|
+
help="Transport mode (overrides --http)",
|
770
|
+
)
|
771
|
+
parser.add_argument(
|
772
|
+
"--host",
|
773
|
+
default=None,
|
774
|
+
help="HTTP host to bind (default: 127.0.0.1)",
|
775
|
+
)
|
776
|
+
parser.add_argument(
|
777
|
+
"--port",
|
778
|
+
type=int,
|
779
|
+
default=None,
|
780
|
+
help="HTTP port to bind (default: 7823)",
|
781
|
+
)
|
782
|
+
|
783
|
+
args = parser.parse_args()
|
784
|
+
|
785
|
+
# Resolve transport
|
786
|
+
env_transport = os.getenv("MCP_TRANSPORT")
|
787
|
+
transport = (
|
788
|
+
args.transport
|
789
|
+
if args.transport
|
790
|
+
else ("http" if args.http else (env_transport or "stdio"))
|
791
|
+
)
|
792
|
+
|
793
|
+
# Set up cleanup
|
879
794
|
try:
|
880
|
-
# Set up cleanup
|
881
795
|
import atexit
|
882
796
|
atexit.register(lambda: asyncio.run(cleanup()))
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
797
|
+
except Exception:
|
798
|
+
pass
|
799
|
+
|
800
|
+
try:
|
801
|
+
if transport == "http":
|
802
|
+
host = args.host or os.getenv("MCP_HTTP_HOST") or "127.0.0.1"
|
803
|
+
port = (
|
804
|
+
args.port
|
805
|
+
or int(os.getenv("MCP_HTTP_PORT") or os.getenv("PORT") or 7823)
|
806
|
+
)
|
807
|
+
_run_http(host, port)
|
808
|
+
else:
|
809
|
+
_run_stdio()
|
810
|
+
|
887
811
|
except KeyboardInterrupt:
|
888
812
|
print("\nShutting down SiteBay MCP Server...")
|
889
813
|
asyncio.run(cleanup())
|
@@ -894,4 +818,4 @@ def main():
|
|
894
818
|
|
895
819
|
|
896
820
|
if __name__ == "__main__":
|
897
|
-
main()
|
821
|
+
main()
|