sitebay-mcp 0.1.1751179164__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sitebay_mcp/__init__.py +15 -0
- sitebay_mcp/auth.py +54 -0
- sitebay_mcp/client.py +359 -0
- sitebay_mcp/exceptions.py +45 -0
- sitebay_mcp/resources.py +171 -0
- sitebay_mcp/server.py +909 -0
- sitebay_mcp/tools/__init__.py +5 -0
- sitebay_mcp/tools/operations.py +285 -0
- sitebay_mcp/tools/sites.py +248 -0
- sitebay_mcp-0.1.1751179164.dist-info/METADATA +271 -0
- sitebay_mcp-0.1.1751179164.dist-info/RECORD +14 -0
- sitebay_mcp-0.1.1751179164.dist-info/WHEEL +4 -0
- sitebay_mcp-0.1.1751179164.dist-info/entry_points.txt +2 -0
- sitebay_mcp-0.1.1751179164.dist-info/licenses/LICENSE +21 -0
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()
|