sitebay-mcp 0.1.1751179164__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sitebay_mcp/server.py ADDED
@@ -0,0 +1,909 @@
1
+ """
2
+ SiteBay MCP Server
3
+
4
+ Main server implementation that provides MCP tools for SiteBay WordPress hosting platform.
5
+ """
6
+
7
+ import asyncio
8
+ import sys
9
+ from typing import Optional
10
+
11
+ from fastmcp import FastMCP
12
+ from fastmcp.server import Context
13
+ from .auth import SiteBayAuth
14
+ from .client import SiteBayClient
15
+ from .exceptions import ConfigurationError, SiteBayError, ValidationError
16
+ from .tools import sites, operations
17
+ from . import resources
18
+
19
+
20
+ # Create the MCP server instance
21
+ mcp = FastMCP("SiteBay WordPress Hosting")
22
+
23
+
24
+ # Global client instance (will be initialized on startup)
25
+ sitebay_client: Optional[SiteBayClient] = None
26
+
27
+
28
+ async def initialize_client() -> SiteBayClient:
29
+ """Initialize the SiteBay client with authentication"""
30
+ global sitebay_client
31
+
32
+ if sitebay_client is None:
33
+ try:
34
+ auth = SiteBayAuth()
35
+ sitebay_client = SiteBayClient(auth)
36
+
37
+ # Test the connection by trying to list regions (public endpoint)
38
+ await sitebay_client.list_regions()
39
+
40
+ except Exception as e:
41
+ raise ConfigurationError(f"Failed to initialize SiteBay client: {str(e)}")
42
+
43
+ return sitebay_client
44
+
45
+
46
+ # Site Management Tools
47
+ @mcp.tool
48
+ async def sitebay_list_sites(ctx: Context, team_id: Optional[str] = None) -> str:
49
+ """
50
+ List all WordPress sites for the authenticated user.
51
+
52
+ Args:
53
+ team_id: Optional team ID to filter sites by team
54
+
55
+ Returns:
56
+ Formatted string with site details including domain, status, region, and versions
57
+ """
58
+ try:
59
+ await ctx.info("Fetching WordPress sites from SiteBay")
60
+ if team_id:
61
+ await ctx.info(f"Filtering by team ID: {team_id}")
62
+
63
+ client = await initialize_client()
64
+ result = await sites.sitebay_list_sites(client, team_id)
65
+
66
+ await ctx.info("Successfully retrieved site list")
67
+ return result
68
+
69
+ except SiteBayError as e:
70
+ await ctx.error(f"SiteBay API error: {str(e)}")
71
+ return f"❌ SiteBay Error: {str(e)}"
72
+ except Exception as e:
73
+ await ctx.error(f"Unexpected error listing sites: {str(e)}")
74
+ return f"❌ Unexpected error: {str(e)}"
75
+
76
+
77
+ @mcp.tool
78
+ async def sitebay_get_site(ctx: Context, fqdn: str) -> str:
79
+ """
80
+ Get detailed information about a specific WordPress site.
81
+
82
+ Args:
83
+ fqdn: The fully qualified domain name of the site (e.g., "example.com")
84
+
85
+ Returns:
86
+ Detailed site information including status, versions, URLs, and configuration
87
+ """
88
+ try:
89
+ ctx.logger.info(f"Fetching details for site: {fqdn}")
90
+
91
+ client = await initialize_client()
92
+ result = await sites.sitebay_get_site(client, fqdn)
93
+
94
+ ctx.logger.info(f"Successfully retrieved details for {fqdn}")
95
+ return result
96
+
97
+ except SiteBayError as e:
98
+ ctx.logger.error(f"SiteBay API error for {fqdn}: {str(e)}")
99
+ return f"❌ SiteBay Error: {str(e)}"
100
+ except Exception as e:
101
+ ctx.logger.error(f"Unexpected error getting site {fqdn}: {str(e)}")
102
+ return f"❌ Unexpected error: {str(e)}"
103
+
104
+
105
+ @mcp.tool
106
+ async def sitebay_create_site(
107
+ ctx: Context,
108
+ fqdn: str,
109
+ wp_title: str,
110
+ wp_username: str,
111
+ wp_password: str,
112
+ wp_email: str,
113
+ region_name: Optional[str] = None,
114
+ template_id: Optional[str] = None,
115
+ team_id: Optional[str] = None
116
+ ) -> str:
117
+ """
118
+ Create a new WordPress site.
119
+
120
+ Args:
121
+ fqdn: The fully qualified domain name for the new site (e.g., "myblog.example.com")
122
+ wp_title: WordPress site title
123
+ wp_username: WordPress admin username
124
+ wp_password: WordPress admin password (should be strong)
125
+ wp_email: WordPress admin email address
126
+ region_name: Optional region name for hosting (uses default if not specified)
127
+ template_id: Optional template ID to use for site creation
128
+ team_id: Optional team ID to create site under
129
+
130
+ Returns:
131
+ Success message with new site details and access information
132
+ """
133
+ try:
134
+ ctx.logger.info(f"Starting site creation for: {fqdn}")
135
+
136
+ # Progress reporting
137
+ progress = ctx.create_progress_token("site-creation")
138
+ await ctx.report_progress(progress, "Initializing site creation...", 0.1)
139
+
140
+ client = await initialize_client()
141
+
142
+ await ctx.report_progress(progress, "Validating site configuration...", 0.2)
143
+
144
+ # Basic validation
145
+ if not fqdn or '.' not in fqdn:
146
+ raise ValueError("Invalid domain name provided")
147
+
148
+ await ctx.report_progress(progress, "Submitting site creation request to SiteBay...", 0.4)
149
+
150
+ result = await sites.sitebay_create_site(
151
+ client, fqdn, wp_title, wp_username, wp_password, wp_email,
152
+ region_name, template_id, team_id
153
+ )
154
+
155
+ await ctx.report_progress(progress, "Site creation submitted successfully!", 1.0)
156
+
157
+ ctx.logger.info(f"Successfully created site: {fqdn}")
158
+ return result
159
+
160
+ except ValueError as e:
161
+ ctx.logger.error(f"Validation error creating site {fqdn}: {str(e)}")
162
+ return f"❌ Validation Error: {str(e)}"
163
+ except ValidationError as e:
164
+ ctx.logger.error(f"SiteBay validation error creating site {fqdn}: {str(e)}")
165
+
166
+ # Provide detailed feedback for the agent with field-specific errors
167
+ error_msg = f"❌ Validation Error - Please check your input:\n{str(e)}\n"
168
+
169
+ if hasattr(e, 'field_errors') and e.field_errors:
170
+ error_msg += "\nSpecific field errors:\n"
171
+ for field, msg in e.field_errors.items():
172
+ error_msg += f" • {field}: {msg}\n"
173
+
174
+ error_msg += "\nPlease adjust your parameters and try again."
175
+ return error_msg
176
+ except SiteBayError as e:
177
+ ctx.logger.error(f"SiteBay API error creating site {fqdn}: {str(e)}")
178
+ return f"❌ SiteBay Error: {str(e)}"
179
+ except Exception as e:
180
+ ctx.logger.error(f"Unexpected error creating site {fqdn}: {str(e)}")
181
+ return f"❌ Unexpected error: {str(e)}"
182
+
183
+
184
+ @mcp.tool
185
+ async def sitebay_update_site(
186
+ fqdn: str,
187
+ wp_title: Optional[str] = None,
188
+ wp_username: Optional[str] = None,
189
+ wp_password: Optional[str] = None,
190
+ wp_email: Optional[str] = None,
191
+ php_version: Optional[str] = None
192
+ ) -> str:
193
+ """
194
+ Update an existing WordPress site configuration.
195
+
196
+ Args:
197
+ fqdn: The fully qualified domain name of the site to update
198
+ wp_title: New WordPress site title
199
+ wp_username: New WordPress admin username
200
+ wp_password: New WordPress admin password
201
+ wp_email: New WordPress admin email
202
+ php_version: New PHP version (e.g., "8.1", "8.2", "8.3")
203
+
204
+ Returns:
205
+ Confirmation message with updated settings
206
+ """
207
+ client = await initialize_client()
208
+ return await sites.sitebay_update_site(
209
+ client, fqdn, wp_title, wp_username, wp_password, wp_email, php_version
210
+ )
211
+
212
+
213
+ @mcp.tool
214
+ async def sitebay_delete_site(fqdn: str, confirm: bool = False) -> str:
215
+ """
216
+ Delete a WordPress site permanently. This action cannot be undone.
217
+
218
+ Args:
219
+ fqdn: The fully qualified domain name of the site to delete
220
+ confirm: Must be True to actually delete the site (safety check)
221
+
222
+ Returns:
223
+ Confirmation message or deletion requirements if confirm=False
224
+ """
225
+ client = await initialize_client()
226
+ return await sites.sitebay_delete_site(client, fqdn, confirm)
227
+
228
+
229
+ # Site Operations Tools
230
+ @mcp.tool
231
+ async def sitebay_site_shell_command(fqdn: str, command: str) -> str:
232
+ """
233
+ Execute a shell command on a WordPress site. Supports WP-CLI commands, system commands, etc.
234
+
235
+ Args:
236
+ fqdn: The fully qualified domain name of the site
237
+ command: The shell command to execute (e.g., "wp plugin list", "ls -la", "wp search-replace")
238
+
239
+ Returns:
240
+ Command output and execution details
241
+ """
242
+ client = await initialize_client()
243
+ return await operations.sitebay_site_shell_command(client, fqdn, command)
244
+
245
+
246
+ @mcp.tool
247
+ async def sitebay_site_edit_file(fqdn: str, file_path: str, content: str) -> str:
248
+ """
249
+ Edit a file in the site's wp-content directory.
250
+
251
+ Args:
252
+ fqdn: The fully qualified domain name of the site
253
+ file_path: Path to the file relative to wp-content (e.g., "themes/mytheme/style.css")
254
+ content: New content for the file
255
+
256
+ Returns:
257
+ Success confirmation with file details
258
+ """
259
+ client = await initialize_client()
260
+ return await operations.sitebay_site_edit_file(client, fqdn, file_path, content)
261
+
262
+
263
+ @mcp.tool
264
+ async def sitebay_site_get_events(
265
+ fqdn: str,
266
+ after_datetime: Optional[str] = None,
267
+ limit: int = 20
268
+ ) -> str:
269
+ """
270
+ Get recent events for a site (deployments, updates, restores, etc.).
271
+
272
+ Args:
273
+ fqdn: The fully qualified domain name of the site
274
+ after_datetime: Optional ISO datetime to filter events after (e.g., "2024-01-01T00:00:00Z")
275
+ limit: Maximum number of events to return (default: 20)
276
+
277
+ Returns:
278
+ Formatted list of recent site events with timestamps and details
279
+ """
280
+ client = await initialize_client()
281
+ return await operations.sitebay_site_get_events(client, fqdn, after_datetime, limit)
282
+
283
+
284
+ @mcp.tool
285
+ async def sitebay_site_external_path_list(fqdn: str) -> str:
286
+ """
287
+ List external path configurations for a site (URL proxying/routing).
288
+
289
+ Args:
290
+ fqdn: The fully qualified domain name of the site
291
+
292
+ Returns:
293
+ List of configured external paths with their target URLs and status
294
+ """
295
+ client = await initialize_client()
296
+ return await operations.sitebay_site_external_path_list(client, fqdn)
297
+
298
+
299
+ @mcp.tool
300
+ async def sitebay_site_external_path_create(
301
+ fqdn: str,
302
+ path: str,
303
+ target_url: str,
304
+ description: Optional[str] = None
305
+ ) -> str:
306
+ """
307
+ Create an external path configuration to proxy requests to external URLs.
308
+
309
+ Args:
310
+ fqdn: The fully qualified domain name of the site
311
+ path: The path on your site (e.g., "/api", "/external")
312
+ target_url: The external URL to proxy requests to
313
+ description: Optional description for this path configuration
314
+
315
+ Returns:
316
+ Success message with external path details
317
+ """
318
+ client = await initialize_client()
319
+ return await operations.sitebay_site_external_path_create(
320
+ client, fqdn, path, target_url, description
321
+ )
322
+
323
+
324
+ # Helper Tools
325
+ @mcp.tool
326
+ async def sitebay_list_regions() -> str:
327
+ """
328
+ List all available hosting regions for site deployment.
329
+
330
+ Returns:
331
+ List of available regions with their details
332
+ """
333
+ try:
334
+ client = await initialize_client()
335
+ regions = await client.list_regions()
336
+
337
+ if not regions:
338
+ return "No regions available."
339
+
340
+ result = f"**Available Hosting Regions** ({len(regions)} regions):\n\n"
341
+
342
+ for region in regions:
343
+ result += f"• **{region.get('name', 'Unknown')}**\n"
344
+ result += f" - Location: {region.get('location', 'Unknown')}\n"
345
+ result += f" - Status: {region.get('status', 'Unknown')}\n"
346
+
347
+ if region.get('description'):
348
+ result += f" - Description: {region.get('description')}\n"
349
+
350
+ result += "\n"
351
+
352
+ return result
353
+
354
+ except SiteBayError as e:
355
+ return f"Error listing regions: {str(e)}"
356
+
357
+
358
+ @mcp.tool
359
+ async def sitebay_list_templates() -> str:
360
+ """
361
+ List all available site templates for quick site creation.
362
+
363
+ Returns:
364
+ List of available templates with descriptions
365
+ """
366
+ try:
367
+ client = await initialize_client()
368
+ templates = await client.list_templates()
369
+
370
+ if not templates:
371
+ return "No templates available."
372
+
373
+ result = f"**Available Site Templates** ({len(templates)} templates):\n\n"
374
+
375
+ for template in templates:
376
+ result += f"• **{template.get('name', 'Unknown')}**\n"
377
+ result += f" - ID: {template.get('id', 'Unknown')}\n"
378
+
379
+ if template.get('description'):
380
+ result += f" - Description: {template.get('description')}\n"
381
+
382
+ if template.get('category'):
383
+ result += f" - Category: {template.get('category')}\n"
384
+
385
+ result += "\n"
386
+
387
+ return result
388
+
389
+ except SiteBayError as e:
390
+ return f"Error listing templates: {str(e)}"
391
+
392
+
393
+ @mcp.tool
394
+ async def sitebay_list_teams(ctx: Context) -> str:
395
+ """
396
+ List all teams for the authenticated user.
397
+
398
+ Returns:
399
+ Formatted list of teams with their details and member information
400
+ """
401
+ try:
402
+ ctx.logger.info("Fetching teams from SiteBay")
403
+
404
+ client = await initialize_client()
405
+ teams = await client.list_teams()
406
+
407
+ if not teams:
408
+ return "No teams found for your account."
409
+
410
+ result = f"**Your Teams** ({len(teams)} teams):\n\n"
411
+
412
+ for team in teams:
413
+ result += f"• **{team.get('name', 'Unknown')}**\n"
414
+ result += f" - ID: {team.get('id', 'Unknown')}\n"
415
+ result += f" - Role: {team.get('role', 'Unknown')}\n"
416
+ result += f" - Created: {team.get('created_at', 'Unknown')}\n"
417
+
418
+ if team.get('description'):
419
+ result += f" - Description: {team.get('description')}\n"
420
+
421
+ result += "\n"
422
+
423
+ ctx.logger.info("Successfully retrieved teams list")
424
+ return result
425
+
426
+ except SiteBayError as e:
427
+ ctx.logger.error(f"SiteBay API error: {str(e)}")
428
+ return f"❌ SiteBay Error: {str(e)}"
429
+ except Exception as e:
430
+ ctx.logger.error(f"Unexpected error listing teams: {str(e)}")
431
+ return f"❌ Unexpected error: {str(e)}"
432
+
433
+
434
+ # Proxy Tools
435
+ @mcp.tool
436
+ async def sitebay_wordpress_proxy(
437
+ ctx: Context,
438
+ site_fqdn: str,
439
+ endpoint: str,
440
+ method: str = "GET",
441
+ data: dict = None
442
+ ) -> str:
443
+ """
444
+ Proxy requests to a WordPress site's REST API.
445
+
446
+ Args:
447
+ site_fqdn: The site domain
448
+ endpoint: WordPress API endpoint (e.g., "/wp/v2/posts")
449
+ method: HTTP method (GET, POST, PUT, DELETE)
450
+ data: Optional data for POST/PUT requests
451
+
452
+ Returns:
453
+ WordPress API response
454
+ """
455
+ try:
456
+ ctx.logger.info(f"WordPress proxy request to {site_fqdn}{endpoint}")
457
+
458
+ client = await initialize_client()
459
+ proxy_data = {
460
+ "site_fqdn": site_fqdn,
461
+ "endpoint": endpoint,
462
+ "method": method
463
+ }
464
+ if data:
465
+ proxy_data["data"] = data
466
+
467
+ result = await client.wordpress_proxy(proxy_data)
468
+ return f"✅ WordPress API Response:\n```json\n{result}\n```"
469
+
470
+ except SiteBayError as e:
471
+ ctx.logger.error(f"WordPress proxy error: {str(e)}")
472
+ return f"❌ WordPress Proxy Error: {str(e)}"
473
+ except Exception as e:
474
+ ctx.logger.error(f"Unexpected proxy error: {str(e)}")
475
+ return f"❌ Unexpected error: {str(e)}"
476
+
477
+
478
+ @mcp.tool
479
+ async def sitebay_shopify_proxy(
480
+ ctx: Context,
481
+ shop_domain: str,
482
+ endpoint: str,
483
+ access_token: str,
484
+ method: str = "GET",
485
+ data: dict = None
486
+ ) -> str:
487
+ """
488
+ Proxy requests to a Shopify Admin API.
489
+
490
+ Args:
491
+ shop_domain: Shopify shop domain (e.g., "myshop.myshopify.com")
492
+ endpoint: Shopify API endpoint (e.g., "/admin/api/2023-10/products.json")
493
+ access_token: Shopify access token
494
+ method: HTTP method (GET, POST, PUT, DELETE)
495
+ data: Optional data for POST/PUT requests
496
+
497
+ Returns:
498
+ Shopify API response
499
+ """
500
+ try:
501
+ ctx.logger.info(f"Shopify proxy request to {shop_domain}{endpoint}")
502
+
503
+ client = await initialize_client()
504
+ proxy_data = {
505
+ "shop_domain": shop_domain,
506
+ "endpoint": endpoint,
507
+ "access_token": access_token,
508
+ "method": method
509
+ }
510
+ if data:
511
+ proxy_data["data"] = data
512
+
513
+ result = await client.shopify_proxy(proxy_data)
514
+ return f"✅ Shopify API Response:\n```json\n{result}\n```"
515
+
516
+ except SiteBayError as e:
517
+ ctx.logger.error(f"Shopify proxy error: {str(e)}")
518
+ return f"❌ Shopify Proxy Error: {str(e)}"
519
+ except Exception as e:
520
+ ctx.logger.error(f"Unexpected proxy error: {str(e)}")
521
+ return f"❌ Unexpected error: {str(e)}"
522
+
523
+
524
+ @mcp.tool
525
+ async def sitebay_posthog_proxy(
526
+ ctx: Context,
527
+ endpoint: str,
528
+ data: dict,
529
+ api_key: Optional[str] = None
530
+ ) -> str:
531
+ """
532
+ Proxy POST requests to PostHog analytics API.
533
+
534
+ Args:
535
+ endpoint: PostHog API endpoint
536
+ data: Data to send to PostHog
537
+ api_key: Optional PostHog API key
538
+
539
+ Returns:
540
+ PostHog API response
541
+ """
542
+ try:
543
+ ctx.logger.info(f"PostHog proxy request to {endpoint}")
544
+
545
+ client = await initialize_client()
546
+ proxy_data = {
547
+ "endpoint": endpoint,
548
+ "data": data
549
+ }
550
+ if api_key:
551
+ proxy_data["api_key"] = api_key
552
+
553
+ result = await client.posthog_proxy(proxy_data)
554
+ return f"✅ PostHog API Response:\n```json\n{result}\n```"
555
+
556
+ except SiteBayError as e:
557
+ ctx.logger.error(f"PostHog proxy error: {str(e)}")
558
+ return f"❌ PostHog Proxy Error: {str(e)}"
559
+ except Exception as e:
560
+ ctx.logger.error(f"Unexpected proxy error: {str(e)}")
561
+ return f"❌ Unexpected error: {str(e)}"
562
+
563
+
564
+ # Staging Tools
565
+ @mcp.tool
566
+ async def sitebay_staging_create(
567
+ ctx: Context,
568
+ fqdn: str,
569
+ staging_subdomain: Optional[str] = None
570
+ ) -> str:
571
+ """
572
+ Create a staging site for testing changes.
573
+
574
+ Args:
575
+ fqdn: The live site domain
576
+ staging_subdomain: Optional custom staging subdomain
577
+
578
+ Returns:
579
+ Staging site creation confirmation
580
+ """
581
+ try:
582
+ ctx.logger.info(f"Creating staging site for {fqdn}")
583
+
584
+ progress = ctx.create_progress_token("staging-creation")
585
+ await ctx.report_progress(progress, "Creating staging environment...", 0.3)
586
+
587
+ client = await initialize_client()
588
+ staging_data = {}
589
+ if staging_subdomain:
590
+ staging_data["staging_subdomain"] = staging_subdomain
591
+
592
+ result = await client.create_staging_site(fqdn, staging_data)
593
+
594
+ await ctx.report_progress(progress, "Staging site created successfully!", 1.0)
595
+
596
+ ctx.logger.info(f"Successfully created staging site for {fqdn}")
597
+ return f"✅ **Staging Site Created**\n\nStaging environment for {fqdn} is now available for testing changes safely."
598
+
599
+ except SiteBayError as e:
600
+ ctx.logger.error(f"Error creating staging site: {str(e)}")
601
+ return f"❌ Staging Creation Error: {str(e)}"
602
+ except Exception as e:
603
+ ctx.logger.error(f"Unexpected staging error: {str(e)}")
604
+ return f"❌ Unexpected error: {str(e)}"
605
+
606
+
607
+ @mcp.tool
608
+ async def sitebay_staging_delete(ctx: Context, fqdn: str) -> str:
609
+ """
610
+ Delete the staging site.
611
+
612
+ Args:
613
+ fqdn: The live site domain
614
+
615
+ Returns:
616
+ Staging deletion confirmation
617
+ """
618
+ try:
619
+ ctx.logger.info(f"Deleting staging site for {fqdn}")
620
+
621
+ client = await initialize_client()
622
+ await client.delete_staging_site(fqdn)
623
+
624
+ ctx.logger.info(f"Successfully deleted staging site for {fqdn}")
625
+ return f"✅ **Staging Site Deleted**\n\nThe staging environment for {fqdn} has been removed."
626
+
627
+ except SiteBayError as e:
628
+ ctx.logger.error(f"Error deleting staging site: {str(e)}")
629
+ return f"❌ Staging Deletion Error: {str(e)}"
630
+ except Exception as e:
631
+ ctx.logger.error(f"Unexpected staging error: {str(e)}")
632
+ return f"❌ Unexpected error: {str(e)}"
633
+
634
+
635
+ @mcp.tool
636
+ async def sitebay_staging_commit(ctx: Context, fqdn: str) -> str:
637
+ """
638
+ Commit staging changes to live site (sync staging to live).
639
+
640
+ Args:
641
+ fqdn: The live site domain
642
+
643
+ Returns:
644
+ Staging commit confirmation
645
+ """
646
+ try:
647
+ ctx.logger.info(f"Committing staging changes for {fqdn}")
648
+
649
+ progress = ctx.create_progress_token("staging-commit")
650
+ await ctx.report_progress(progress, "Syncing staging to live...", 0.5)
651
+
652
+ client = await initialize_client()
653
+ result = await client.commit_staging_site(fqdn)
654
+
655
+ await ctx.report_progress(progress, "Staging committed to live successfully!", 1.0)
656
+
657
+ ctx.logger.info(f"Successfully committed staging for {fqdn}")
658
+ return f"✅ **Staging Committed to Live**\n\nChanges from staging have been synchronized to the live site {fqdn}."
659
+
660
+ except SiteBayError as e:
661
+ ctx.logger.error(f"Error committing staging: {str(e)}")
662
+ return f"❌ Staging Commit Error: {str(e)}"
663
+ except Exception as e:
664
+ ctx.logger.error(f"Unexpected staging error: {str(e)}")
665
+ return f"❌ Unexpected error: {str(e)}"
666
+
667
+
668
+ # Backup/Restore Tools
669
+ @mcp.tool
670
+ async def sitebay_backup_list_commits(
671
+ ctx: Context,
672
+ fqdn: str,
673
+ number_to_fetch: int = 10
674
+ ) -> str:
675
+ """
676
+ List available backup commits for point-in-time restore.
677
+
678
+ Args:
679
+ fqdn: The site domain
680
+ number_to_fetch: Number of backup entries to fetch (default: 10)
681
+
682
+ Returns:
683
+ List of available backup commits
684
+ """
685
+ try:
686
+ ctx.logger.info(f"Fetching backup commits for {fqdn}")
687
+
688
+ client = await initialize_client()
689
+ commits = await client.get_backup_commits(fqdn, number_to_fetch)
690
+
691
+ if not commits:
692
+ return f"No backup commits found for {fqdn}."
693
+
694
+ result = f"**Available Backup Commits for {fqdn}** ({len(commits)} entries):\n\n"
695
+
696
+ for commit in commits:
697
+ result += f"• **{commit.get('created_at', 'Unknown time')}**\n"
698
+ result += f" - ID: {commit.get('id', 'Unknown')}\n"
699
+ result += f" - Commit Hash: {commit.get('commit_hash', 'Unknown')}\n"
700
+ result += f" - Tables Saved: {len(commit.get('tables_saved', []))} tables\n"
701
+ result += f" - Status: {'Completed' if commit.get('finished_at') else 'In Progress'}\n"
702
+ result += "\n"
703
+
704
+ ctx.logger.info(f"Successfully retrieved backup commits for {fqdn}")
705
+ return result
706
+
707
+ except SiteBayError as e:
708
+ ctx.logger.error(f"Error fetching backup commits: {str(e)}")
709
+ return f"❌ Backup Error: {str(e)}"
710
+ except Exception as e:
711
+ ctx.logger.error(f"Unexpected backup error: {str(e)}")
712
+ return f"❌ Unexpected error: {str(e)}"
713
+
714
+
715
+ @mcp.tool
716
+ async def sitebay_backup_restore(
717
+ ctx: Context,
718
+ fqdn: str,
719
+ commit_id: str,
720
+ restore_type: str = "full"
721
+ ) -> str:
722
+ """
723
+ Restore a site to a previous point in time.
724
+
725
+ Args:
726
+ fqdn: The site domain
727
+ commit_id: The backup commit ID to restore from
728
+ restore_type: Type of restore ("full", "database", "files")
729
+
730
+ Returns:
731
+ Restore operation confirmation
732
+ """
733
+ try:
734
+ ctx.logger.info(f"Starting point-in-time restore for {fqdn}")
735
+
736
+ progress = ctx.create_progress_token("backup-restore")
737
+ await ctx.report_progress(progress, "Initializing restore operation...", 0.1)
738
+
739
+ client = await initialize_client()
740
+ restore_data = {
741
+ "commit_id": commit_id,
742
+ "restore_type": restore_type
743
+ }
744
+
745
+ await ctx.report_progress(progress, "Submitting restore request...", 0.3)
746
+
747
+ result = await client.create_restore(fqdn, restore_data)
748
+
749
+ await ctx.report_progress(progress, "Restore operation initiated!", 1.0)
750
+
751
+ ctx.logger.info(f"Successfully initiated restore for {fqdn}")
752
+ 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."
753
+
754
+ except SiteBayError as e:
755
+ ctx.logger.error(f"Error starting restore: {str(e)}")
756
+ return f"❌ Restore Error: {str(e)}"
757
+ except Exception as e:
758
+ ctx.logger.error(f"Unexpected restore error: {str(e)}")
759
+ return f"❌ Unexpected error: {str(e)}"
760
+
761
+
762
+ # Account Tools
763
+ @mcp.tool
764
+ async def sitebay_account_affiliates(ctx: Context) -> str:
765
+ """
766
+ Get affiliate referral information.
767
+
768
+ Returns:
769
+ List of users who signed up using your affiliate links
770
+ """
771
+ try:
772
+ ctx.logger.info("Fetching affiliate referrals")
773
+
774
+ client = await initialize_client()
775
+ affiliates = await client.get_affiliate_referrals()
776
+
777
+ if not affiliates:
778
+ return "No affiliate referrals found."
779
+
780
+ result = f"**Your Affiliate Referrals** ({len(affiliates)} referrals):\n\n"
781
+
782
+ for affiliate in affiliates:
783
+ result += f"• **User**: {affiliate.get('email', 'Unknown')}\n"
784
+ result += f" - Signed up: {affiliate.get('created_at', 'Unknown')}\n"
785
+ result += f" - Status: {affiliate.get('status', 'Unknown')}\n"
786
+ result += "\n"
787
+
788
+ ctx.logger.info("Successfully retrieved affiliate referrals")
789
+ return result
790
+
791
+ except SiteBayError as e:
792
+ ctx.logger.error(f"Error fetching affiliates: {str(e)}")
793
+ return f"❌ Affiliate Error: {str(e)}"
794
+ except Exception as e:
795
+ ctx.logger.error(f"Unexpected affiliate error: {str(e)}")
796
+ return f"❌ Unexpected error: {str(e)}"
797
+
798
+
799
+ @mcp.tool
800
+ async def sitebay_account_create_checkout(
801
+ ctx: Context,
802
+ plan_name: str = "starter",
803
+ interval: str = "month",
804
+ team_id: Optional[str] = None
805
+ ) -> str:
806
+ """
807
+ Create a Stripe checkout session for team billing.
808
+
809
+ Args:
810
+ plan_name: Plan type ("starter", "pro", "enterprise")
811
+ interval: Billing interval ("month", "year")
812
+ team_id: Optional team ID to purchase for
813
+
814
+ Returns:
815
+ Stripe checkout URL
816
+ """
817
+ try:
818
+ ctx.logger.info(f"Creating checkout session for {plan_name} plan")
819
+
820
+ client = await initialize_client()
821
+ checkout_data = {
822
+ "plan_name": plan_name,
823
+ "interval": interval
824
+ }
825
+ if team_id:
826
+ checkout_data["for_team_id"] = team_id
827
+
828
+ result = await client.create_checkout_session(checkout_data)
829
+
830
+ ctx.logger.info("Successfully created checkout session")
831
+ return f"✅ **Checkout Session Created**\n\nPlan: {plan_name} ({interval}ly)\nCheckout URL: {result.get('url', 'URL not provided')}"
832
+
833
+ except SiteBayError as e:
834
+ ctx.logger.error(f"Error creating checkout: {str(e)}")
835
+ return f"❌ Checkout Error: {str(e)}"
836
+ except Exception as e:
837
+ ctx.logger.error(f"Unexpected checkout error: {str(e)}")
838
+ return f"❌ Unexpected error: {str(e)}"
839
+
840
+
841
+ # Resources
842
+ @mcp.resource("sitebay://site/{site_fqdn}/config")
843
+ async def site_config_resource(ctx: Context, site_fqdn: str) -> str:
844
+ """
845
+ Get site configuration as a readable resource.
846
+
847
+ Args:
848
+ site_fqdn: The fully qualified domain name of the site
849
+
850
+ Returns:
851
+ JSON formatted site configuration including technical specs, URLs, and features
852
+ """
853
+ return await resources.get_site_config_resource(ctx, site_fqdn)
854
+
855
+
856
+ @mcp.resource("sitebay://site/{site_fqdn}/events")
857
+ async def site_events_resource(ctx: Context, site_fqdn: str, limit: int = 50) -> str:
858
+ """
859
+ Get site events and logs as a readable resource.
860
+
861
+ Args:
862
+ site_fqdn: The fully qualified domain name of the site
863
+ limit: Maximum number of events to return (default: 50)
864
+
865
+ Returns:
866
+ JSON formatted site events and deployment logs
867
+ """
868
+ return await resources.get_site_events_resource(ctx, site_fqdn, limit)
869
+
870
+
871
+ @mcp.resource("sitebay://account/summary")
872
+ async def account_summary_resource(ctx: Context) -> str:
873
+ """
874
+ Get account summary as a readable resource.
875
+
876
+ Returns:
877
+ JSON formatted account overview including site counts, regions, and recent activity
878
+ """
879
+ return await resources.get_account_summary_resource(ctx)
880
+
881
+
882
+ async def cleanup():
883
+ """Cleanup function to close the client connection"""
884
+ global sitebay_client
885
+ if sitebay_client:
886
+ await sitebay_client.close()
887
+
888
+
889
+ def main():
890
+ """Main entry point for the MCP server"""
891
+ try:
892
+ # Set up cleanup
893
+ import atexit
894
+ atexit.register(lambda: asyncio.run(cleanup()))
895
+
896
+ # Run the FastMCP server
897
+ mcp.run()
898
+
899
+ except KeyboardInterrupt:
900
+ print("\nShutting down SiteBay MCP Server...")
901
+ asyncio.run(cleanup())
902
+ sys.exit(0)
903
+ except Exception as e:
904
+ print(f"Error starting SiteBay MCP Server: {e}")
905
+ sys.exit(1)
906
+
907
+
908
+ if __name__ == "__main__":
909
+ main()