sitebay-mcp 0.1.1751286041__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/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
- # Test the connection by trying to list regions (public endpoint)
38
- await sitebay_client.list_regions()
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[str] = None) -> str:
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
- result = await sites.sitebay_list_sites(client, team_id)
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
- wp_title: str,
113
- wp_username: str,
114
- wp_password: str,
115
- wp_email: str,
116
- region_name: Optional[str] = None,
117
- template_id: Optional[str] = None,
118
- team_id: Optional[str] = None
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
- fqdn: The fully qualified domain name for the new site (e.g., "myblog.example.com")
125
- wp_title: WordPress site title
126
- wp_username: WordPress admin username
127
- wp_password: WordPress admin password (should be strong)
128
- wp_email: WordPress admin email address
129
- region_name: Optional region name for hosting (uses default if not specified)
130
- template_id: Optional template ID to use for site creation
131
- team_id: Optional team ID to create site under
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, fqdn, wp_title, wp_username, wp_password, wp_email,
151
- region_name, template_id, team_id
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
- wp_title: Optional[str] = None,
186
- wp_username: Optional[str] = None,
187
- wp_password: Optional[str] = None,
188
- wp_email: Optional[str] = None,
189
- php_version: Optional[str] = None
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 WordPress site configuration.
209
+ Update an existing SiteBay site configuration.
193
210
 
194
211
  Args:
195
- fqdn: The fully qualified domain name of the site to update
196
- wp_title: New WordPress site title
197
- wp_username: New WordPress admin username
198
- wp_password: New WordPress admin password
199
- wp_email: New WordPress admin email
200
- php_version: New PHP version (e.g., "8.1", "8.2", "8.3")
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, fqdn, wp_title, wp_username, wp_password, wp_email, php_version
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, confirm: bool = False) -> 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 or deletion requirements if confirm=False
245
+ Confirmation message
222
246
  """
223
247
  client = await initialize_client()
224
- return await sites.sitebay_delete_site(client, fqdn, confirm)
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(fqdn: str, command: str) -> str:
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(client, fqdn, 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(fqdn: str, file_path: str, content: str) -> str:
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(client, fqdn, file_path, content)
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
- @mcp.tool
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
- @mcp.tool
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 sitebay_list_templates() -> str:
308
+ async def sitebay_list_ready_made_sites() -> str:
358
309
  """
359
- List all available site templates for quick site creation.
310
+ List available ready-made sites for quick launches.
360
311
 
361
312
  Returns:
362
- List of available templates with descriptions
313
+ List of ready-made sites with descriptions
363
314
  """
364
315
  try:
365
316
  client = await initialize_client()
366
- templates = await client.list_templates()
317
+ items = await client.list_ready_made_sites()
367
318
 
368
- if not templates:
369
- return "No templates available."
319
+ if not items:
320
+ return "No ready-made sites available."
370
321
 
371
- result = f"**Available Site Templates** ({len(templates)} templates):\n\n"
322
+ result = f"**Available Ready-made Sites** ({len(items)}):\n\n"
372
323
 
373
- for template in templates:
374
- result += f"• **{template.get('name', 'Unknown')}**\n"
375
- result += f" - ID: {template.get('id', 'Unknown')}\n"
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 template.get('description'):
378
- result += f" - Description: {template.get('description')}\n"
328
+ if item.get('description'):
329
+ result += f" - Description: {item.get('description')}\n"
379
330
 
380
- if template.get('category'):
381
- result += f" - Category: {template.get('category')}\n"
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 templates: {str(e)}"
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
- site_fqdn: str,
437
- endpoint: str,
438
- method: str = "GET",
439
- data: Optional[dict] = None
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
- site_fqdn: The site domain
446
- endpoint: WordPress API endpoint (e.g., "/wp/v2/posts")
447
- method: HTTP method (GET, POST, PUT, DELETE)
448
- data: Optional data for POST/PUT requests
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 {site_fqdn}{endpoint}")
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
- "site_fqdn": site_fqdn,
459
- "endpoint": endpoint,
460
- "method": method
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
- shop_domain: str,
480
- endpoint: str,
481
- access_token: str,
482
- method: str = "GET",
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
- shop_domain: Shopify shop domain (e.g., "myshop.myshopify.com")
490
- endpoint: Shopify API endpoint (e.g., "/admin/api/2023-10/products.json")
491
- access_token: Shopify access token
492
- method: HTTP method (GET, POST, PUT, DELETE)
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 {shop_domain}{endpoint}")
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
- "shop_domain": shop_domain,
504
- "endpoint": endpoint,
505
- "access_token": access_token,
506
- "method": method
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
- endpoint: str,
526
- data: dict,
527
- api_key: Optional[str] = None
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
- endpoint: PostHog API endpoint
534
- data: Data to send to PostHog
535
- api_key: Optional PostHog API key
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 {endpoint}")
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
- "endpoint": endpoint,
546
- "data": data
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 Tools
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
- commit_id: str,
712
- restore_type: str = "full"
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
- commit_id: The backup commit ID to restore from
720
- restore_type: Type of restore ("full", "database", "files")
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
- restore_data = {
731
- "commit_id": commit_id,
732
- "restore_type": restore_type
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 f"✅ **Point-in-Time Restore Initiated**\n\nRestore operation for {fqdn} has been started. The site will be restored to the selected backup point."
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
- @mcp.resource("sitebay://site/{site_fqdn}/events")
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, regions, and recent activity
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
- # Run the FastMCP server
885
- mcp.run()
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()