sitebay-mcp 0.1.1751286041__py3-none-any.whl → 0.1.1757498234__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
@@ -410,13 +361,10 @@ async def sitebay_list_teams(ctx: Context) -> str:
410
361
  for team in teams:
411
362
  result += f"• **{team.get('name', 'Unknown')}**\n"
412
363
  result += f" - ID: {team.get('id', 'Unknown')}\n"
413
- result += f" - Role: {team.get('role', 'Unknown')}\n"
414
- result += f" - Created: {team.get('created_at', 'Unknown')}\n"
415
-
416
- if team.get('description'):
417
- result += f" - Description: {team.get('description')}\n"
418
-
419
- result += "\n"
364
+ result += f" - Plan: {team.get('plan_type_name', 'Unknown')}\n"
365
+ result += f" - Active: {team.get('is_active', 'Unknown')}\n"
366
+ result += f" - Default: {team.get('is_default', 'Unknown')}\n"
367
+ result += f" - Created: {team.get('created_at', 'Unknown')}\n\n"
420
368
 
421
369
  await ctx.info("Successfully retrieved teams list")
422
370
  return result
@@ -433,34 +381,30 @@ async def sitebay_list_teams(ctx: Context) -> str:
433
381
  @mcp.tool
434
382
  async def sitebay_wordpress_proxy(
435
383
  ctx: Context,
436
- site_fqdn: str,
437
- endpoint: str,
438
- method: str = "GET",
439
- data: Optional[dict] = None
384
+ fqdn: str,
385
+ path: str = "/wp-json/wp/v2/",
386
+ query_params_json: str = "",
387
+ method: str = "get",
440
388
  ) -> str:
441
389
  """
442
390
  Proxy requests to a WordPress site's REST API.
443
391
 
444
392
  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
393
+ fqdn: The site domain
394
+ path: WordPress API path (e.g., "/wp-json/wp/v2/posts")
395
+ query_params_json: Optional JSON string for payload or query params
396
+ method: HTTP method (get, post, put, delete)
449
397
 
450
398
  Returns:
451
399
  WordPress API response
452
400
  """
453
401
  try:
454
- await ctx.info(f"WordPress proxy request to {site_fqdn}{endpoint}")
402
+ await ctx.info(f"WordPress proxy request to {fqdn}{path or ''}")
455
403
 
456
404
  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
405
+ proxy_data: dict[str, Any] = {"fqdn": fqdn, "method": method, "path": path}
406
+ if query_params_json:
407
+ proxy_data["query_params_json"] = query_params_json
464
408
 
465
409
  result = await client.wordpress_proxy(proxy_data)
466
410
  return f"✅ WordPress API Response:\n```json\n{result}\n```"
@@ -476,37 +420,30 @@ async def sitebay_wordpress_proxy(
476
420
  @mcp.tool
477
421
  async def sitebay_shopify_proxy(
478
422
  ctx: Context,
479
- shop_domain: str,
480
- endpoint: str,
481
- access_token: str,
482
- method: str = "GET",
483
- data: Optional[dict] = None
423
+ shop_name: str,
424
+ path: str = "/admin/api/2024-04",
425
+ query_params_json: str = "",
426
+ method: str = "get",
484
427
  ) -> str:
485
428
  """
486
429
  Proxy requests to a Shopify Admin API.
487
430
 
488
431
  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
432
+ shop_name: Shopify shop name
433
+ path: Shopify API path (e.g., "/admin/api/2024-04/products.json")
434
+ query_params_json: Optional JSON string for payload or query params
435
+ method: HTTP method (get, post, put, delete)
494
436
 
495
437
  Returns:
496
438
  Shopify API response
497
439
  """
498
440
  try:
499
- await ctx.info(f"Shopify proxy request to {shop_domain}{endpoint}")
441
+ await ctx.info(f"Shopify proxy request to {shop_name}{path or ''}")
500
442
 
501
443
  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
444
+ proxy_data: dict[str, Any] = {"shop_name": shop_name, "method": method, "path": path}
445
+ if query_params_json:
446
+ proxy_data["query_params_json"] = query_params_json
510
447
 
511
448
  result = await client.shopify_proxy(proxy_data)
512
449
  return f"✅ Shopify API Response:\n```json\n{result}\n```"
@@ -522,31 +459,28 @@ async def sitebay_shopify_proxy(
522
459
  @mcp.tool
523
460
  async def sitebay_posthog_proxy(
524
461
  ctx: Context,
525
- endpoint: str,
526
- data: dict,
527
- api_key: Optional[str] = None
462
+ path: str,
463
+ query_params_json: str = "",
464
+ method: str = "get",
528
465
  ) -> str:
529
466
  """
530
467
  Proxy POST requests to PostHog analytics API.
531
468
 
532
469
  Args:
533
- endpoint: PostHog API endpoint
534
- data: Data to send to PostHog
535
- api_key: Optional PostHog API key
470
+ path: PostHog API path
471
+ query_params_json: Optional JSON string for payload or query params
472
+ method: HTTP method (get, post, put, delete)
536
473
 
537
474
  Returns:
538
475
  PostHog API response
539
476
  """
540
477
  try:
541
- await ctx.info(f"PostHog proxy request to {endpoint}")
478
+ await ctx.info(f"PostHog proxy request to {path}")
542
479
 
543
480
  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
481
+ proxy_data: dict[str, Any] = {"path": path, "method": method}
482
+ if query_params_json:
483
+ proxy_data["query_params_json"] = query_params_json
550
484
 
551
485
  result = await client.posthog_proxy(proxy_data)
552
486
  return f"✅ PostHog API Response:\n```json\n{result}\n```"
@@ -559,102 +493,7 @@ async def sitebay_posthog_proxy(
559
493
  return f"❌ Unexpected error: {str(e)}"
560
494
 
561
495
 
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)}"
496
+ # Staging tools removed (no longer supported)
658
497
 
659
498
 
660
499
  # Backup/Restore Tools
@@ -662,14 +501,14 @@ async def sitebay_staging_commit(ctx: Context, fqdn: str) -> str:
662
501
  async def sitebay_backup_list_commits(
663
502
  ctx: Context,
664
503
  fqdn: str,
665
- number_to_fetch: int = 10
504
+ number_to_fetch: int = 1
666
505
  ) -> str:
667
506
  """
668
507
  List available backup commits for point-in-time restore.
669
508
 
670
509
  Args:
671
510
  fqdn: The site domain
672
- number_to_fetch: Number of backup entries to fetch (default: 10)
511
+ number_to_fetch: Number of backup entries to fetch (default: 1)
673
512
 
674
513
  Returns:
675
514
  List of available backup commits
@@ -708,37 +547,57 @@ async def sitebay_backup_list_commits(
708
547
  async def sitebay_backup_restore(
709
548
  ctx: Context,
710
549
  fqdn: str,
711
- commit_id: str,
712
- restore_type: str = "full"
550
+ restore_point: Optional[str] = None,
551
+ for_stage_site: Optional[bool] = None,
552
+ restore_db: Optional[bool] = None,
553
+ restore_wp_content: Optional[bool] = None,
554
+ delete_extra_files: Optional[bool] = None,
555
+ dolt_restore_hash: Optional[str] = None,
556
+ is_dry_run: Optional[bool] = None,
713
557
  ) -> str:
714
558
  """
715
559
  Restore a site to a previous point in time.
716
560
 
717
- Args:
561
+ Args (PITRestoreCreate schema):
718
562
  fqdn: The site domain
719
- commit_id: The backup commit ID to restore from
720
- restore_type: Type of restore ("full", "database", "files")
563
+ restore_point: ISO datetime string (or omit for latest)
564
+ for_stage_site: Whether to restore the stage site
565
+ restore_db: Restore database (default true)
566
+ restore_wp_content: Restore wp-content (default true)
567
+ delete_extra_files: Delete extra files from target (default false)
568
+ dolt_restore_hash: Optional Dolt hash to restore DB
569
+ is_dry_run: Simulate restore without applying changes
721
570
 
722
571
  Returns:
723
572
  Restore operation confirmation
724
573
  """
725
574
  try:
726
575
  await ctx.info(f"Starting point-in-time restore for {fqdn}")
727
-
728
-
729
576
  client = await initialize_client()
730
- restore_data = {
731
- "commit_id": commit_id,
732
- "restore_type": restore_type
733
- }
734
-
735
-
577
+
578
+ restore_data: dict[str, Any] = {}
579
+ if restore_point is not None:
580
+ restore_data["restore_point"] = restore_point
581
+ if for_stage_site is not None:
582
+ restore_data["for_stage_site"] = for_stage_site
583
+ if restore_db is not None:
584
+ restore_data["restore_db"] = restore_db
585
+ if restore_wp_content is not None:
586
+ restore_data["restore_wp_content"] = restore_wp_content
587
+ if delete_extra_files is not None:
588
+ restore_data["delete_extra_files"] = delete_extra_files
589
+ if dolt_restore_hash is not None:
590
+ restore_data["dolt_restore_hash"] = dolt_restore_hash
591
+ if is_dry_run is not None:
592
+ restore_data["is_dry_run"] = is_dry_run
593
+
736
594
  result = await client.create_restore(fqdn, restore_data)
737
-
738
-
595
+
739
596
  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
-
597
+ return (
598
+ "✅ **Point-in-Time Restore Initiated**\n\n"
599
+ f"Restore operation for {fqdn} has been started."
600
+ )
742
601
  except SiteBayError as e:
743
602
  await ctx.error(f"Error starting restore: {str(e)}")
744
603
  return f"❌ Restore Error: {str(e)}"
@@ -768,10 +627,10 @@ async def sitebay_account_affiliates(ctx: Context) -> str:
768
627
  result = f"**Your Affiliate Referrals** ({len(affiliates)} referrals):\n\n"
769
628
 
770
629
  for affiliate in affiliates:
771
- result += f"• **User**: {affiliate.get('email', 'Unknown')}\n"
630
+ result += f"• **Email**: {affiliate.get('email', 'Unknown')}\n"
631
+ result += f" - Name: {affiliate.get('full_name', 'Unknown')}\n"
772
632
  result += f" - Signed up: {affiliate.get('created_at', 'Unknown')}\n"
773
- result += f" - Status: {affiliate.get('status', 'Unknown')}\n"
774
- result += "\n"
633
+ result += f" - Active: {affiliate.get('is_active', 'Unknown')}\n\n"
775
634
 
776
635
  await ctx.info("Successfully retrieved affiliate referrals")
777
636
  return result
@@ -841,19 +700,7 @@ async def site_config_resource(ctx: Context, site_fqdn: str) -> str:
841
700
  return await resources.get_site_config_resource(ctx, site_fqdn)
842
701
 
843
702
 
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)
703
+ # Site events resource removed (not present in schema)
857
704
 
858
705
 
859
706
  @mcp.resource("sitebay://account/summary")
@@ -862,7 +709,7 @@ async def account_summary_resource(ctx: Context) -> str:
862
709
  Get account summary as a readable resource.
863
710
 
864
711
  Returns:
865
- JSON formatted account overview including site counts, regions, and recent activity
712
+ JSON formatted account overview including site counts, ready-made catalog size, and recent activity
866
713
  """
867
714
  return await resources.get_account_summary_resource(ctx)
868
715
 
@@ -874,16 +721,86 @@ async def cleanup():
874
721
  await sitebay_client.close()
875
722
 
876
723
 
724
+ def _run_stdio():
725
+ """Run the MCP server over STDIO (default)."""
726
+ mcp.run()
727
+
728
+
729
+ def _run_http(host: str, port: int):
730
+ """Run the MCP server over HTTP (streamable)."""
731
+ # FastMCP >= 2.9 provides HTTP transport via run_http
732
+ if not hasattr(mcp, "run_http"):
733
+ raise RuntimeError(
734
+ "FastMCP does not support HTTP transport in this environment. "
735
+ "Please upgrade fastmcp to >= 2.9."
736
+ )
737
+ print(f"Starting SiteBay MCP HTTP server on http://{host}:{port}")
738
+ mcp.run_http(host=host, port=port)
739
+
740
+
877
741
  def main():
878
- """Main entry point for the MCP server"""
742
+ """Main entry point for the MCP server.
743
+
744
+ Supports both STDIO (default) and HTTP transport. Use one of:
745
+ - stdio (default): `sitebay-mcp`
746
+ - http: `sitebay-mcp --http --port 7823 --host 0.0.0.0`
747
+
748
+ Environment variables (used if flags not provided):
749
+ - MCP_TRANSPORT=stdio|http
750
+ - MCP_HTTP_HOST (default: 127.0.0.1)
751
+ - MCP_HTTP_PORT or PORT (default: 7823)
752
+ """
753
+ parser = argparse.ArgumentParser(prog="sitebay-mcp", add_help=True)
754
+ parser.add_argument(
755
+ "--http",
756
+ action="store_true",
757
+ help="Run the MCP server using HTTP transport (streamable)",
758
+ )
759
+ parser.add_argument(
760
+ "--transport",
761
+ choices=["stdio", "http"],
762
+ help="Transport mode (overrides --http)",
763
+ )
764
+ parser.add_argument(
765
+ "--host",
766
+ default=None,
767
+ help="HTTP host to bind (default: 127.0.0.1)",
768
+ )
769
+ parser.add_argument(
770
+ "--port",
771
+ type=int,
772
+ default=None,
773
+ help="HTTP port to bind (default: 7823)",
774
+ )
775
+
776
+ args = parser.parse_args()
777
+
778
+ # Resolve transport
779
+ env_transport = os.getenv("MCP_TRANSPORT")
780
+ transport = (
781
+ args.transport
782
+ if args.transport
783
+ else ("http" if args.http else (env_transport or "stdio"))
784
+ )
785
+
786
+ # Set up cleanup
879
787
  try:
880
- # Set up cleanup
881
788
  import atexit
882
789
  atexit.register(lambda: asyncio.run(cleanup()))
883
-
884
- # Run the FastMCP server
885
- mcp.run()
886
-
790
+ except Exception:
791
+ pass
792
+
793
+ try:
794
+ if transport == "http":
795
+ host = args.host or os.getenv("MCP_HTTP_HOST") or "127.0.0.1"
796
+ port = (
797
+ args.port
798
+ or int(os.getenv("MCP_HTTP_PORT") or os.getenv("PORT") or 7823)
799
+ )
800
+ _run_http(host, port)
801
+ else:
802
+ _run_stdio()
803
+
887
804
  except KeyboardInterrupt:
888
805
  print("\nShutting down SiteBay MCP Server...")
889
806
  asyncio.run(cleanup())
@@ -894,4 +811,4 @@ def main():
894
811
 
895
812
 
896
813
  if __name__ == "__main__":
897
- main()
814
+ main()