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/__init__.py +2 -2
- sitebay_mcp/client.py +33 -48
- sitebay_mcp/resources.py +23 -76
- sitebay_mcp/server.py +268 -351
- sitebay_mcp/tools/operations.py +44 -151
- sitebay_mcp/tools/sites.py +102 -107
- {sitebay_mcp-0.1.1751286041.dist-info → sitebay_mcp-0.1.1757498234.dist-info}/METADATA +70 -34
- sitebay_mcp-0.1.1757498234.dist-info/RECORD +14 -0
- sitebay_mcp-0.1.1751286041.dist-info/RECORD +0 -14
- {sitebay_mcp-0.1.1751286041.dist-info → sitebay_mcp-0.1.1757498234.dist-info}/WHEEL +0 -0
- {sitebay_mcp-0.1.1751286041.dist-info → sitebay_mcp-0.1.1757498234.dist-info}/entry_points.txt +0 -0
- {sitebay_mcp-0.1.1751286041.dist-info → sitebay_mcp-0.1.1757498234.dist-info}/licenses/LICENSE +0 -0
sitebay_mcp/server.py
CHANGED
@@ -5,8 +5,11 @@ Main server implementation that provides MCP tools for SiteBay WordPress hosting
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
import asyncio
|
8
|
+
import os
|
8
9
|
import sys
|
10
|
+
import argparse
|
9
11
|
from typing import Any, Optional
|
12
|
+
from pydantic import UUID4
|
10
13
|
|
11
14
|
from fastmcp import FastMCP
|
12
15
|
from fastmcp.server import Context
|
@@ -33,9 +36,12 @@ async def initialize_client() -> SiteBayClient:
|
|
33
36
|
try:
|
34
37
|
auth = SiteBayAuth()
|
35
38
|
sitebay_client = SiteBayClient(auth)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
# Test the connection using a lightweight, public endpoint
|
40
|
+
try:
|
41
|
+
await sitebay_client.list_ready_made_sites()
|
42
|
+
except Exception:
|
43
|
+
# Non-fatal for initialization; auth/transport issues will surface later
|
44
|
+
pass
|
39
45
|
|
40
46
|
except Exception as e:
|
41
47
|
raise ConfigurationError(f"Failed to initialize SiteBay client: {str(e)}")
|
@@ -45,12 +51,12 @@ async def initialize_client() -> SiteBayClient:
|
|
45
51
|
|
46
52
|
# Site Management Tools
|
47
53
|
@mcp.tool
|
48
|
-
async def sitebay_list_sites(ctx: Context, team_id: Optional[
|
54
|
+
async def sitebay_list_sites(ctx: Context, team_id: Optional[UUID4] = None) -> str:
|
49
55
|
"""
|
50
56
|
List all WordPress sites for the authenticated user.
|
51
57
|
|
52
58
|
Args:
|
53
|
-
team_id: Optional team ID to filter sites by team
|
59
|
+
team_id: Optional team ID (UUID4) to filter sites by team
|
54
60
|
|
55
61
|
Returns:
|
56
62
|
Formatted string with site details including domain, status, region, and versions
|
@@ -61,7 +67,8 @@ async def sitebay_list_sites(ctx: Context, team_id: Optional[str] = None) -> str
|
|
61
67
|
await ctx.info(f"Filtering by team ID: {team_id}")
|
62
68
|
|
63
69
|
client = await initialize_client()
|
64
|
-
|
70
|
+
team_id_str = str(team_id) if team_id is not None else None
|
71
|
+
result = await sites.sitebay_list_sites(client, team_id_str)
|
65
72
|
|
66
73
|
await ctx.info("Successfully retrieved site list")
|
67
74
|
return result
|
@@ -108,27 +115,33 @@ async def sitebay_get_site(ctx: Context, fqdn: str) -> str:
|
|
108
115
|
@mcp.tool
|
109
116
|
async def sitebay_create_site(
|
110
117
|
ctx: Context,
|
118
|
+
team_id: str,
|
111
119
|
fqdn: str,
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
120
|
+
wordpress_blog_name: str,
|
121
|
+
wordpress_first_name: str,
|
122
|
+
wordpress_last_name: str,
|
123
|
+
wordpress_email: str,
|
124
|
+
wordpress_username: str,
|
125
|
+
wordpress_password: str,
|
126
|
+
git_url: Optional[str] = None,
|
127
|
+
ready_made_site_name: Optional[str] = None,
|
128
|
+
is_free: Optional[bool] = None,
|
119
129
|
) -> str:
|
120
130
|
"""
|
121
|
-
Create a new WordPress site.
|
131
|
+
Create a new WordPress site (SiteLiveCreate schema).
|
122
132
|
|
123
133
|
Args:
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
134
|
+
team_id: Team UUID that owns the site
|
135
|
+
fqdn: The fully qualified domain name for the new site (e.g., "www.example.org")
|
136
|
+
wordpress_blog_name: Blog/site title
|
137
|
+
wordpress_first_name: Admin first name
|
138
|
+
wordpress_last_name: Admin last name
|
139
|
+
wordpress_email: Admin email address
|
140
|
+
wordpress_username: Admin username
|
141
|
+
wordpress_password: Admin password (strong)
|
142
|
+
git_url: Optional Git repository URL
|
143
|
+
ready_made_site_name: Optional ready-made site name
|
144
|
+
is_free: Optional flag for free plan
|
132
145
|
|
133
146
|
Returns:
|
134
147
|
Success message with new site details and access information
|
@@ -136,22 +149,29 @@ async def sitebay_create_site(
|
|
136
149
|
try:
|
137
150
|
await ctx.info(f"Starting site creation for: {fqdn}")
|
138
151
|
|
139
|
-
# Progress reporting
|
140
|
-
|
141
152
|
client = await initialize_client()
|
142
153
|
|
143
|
-
|
144
154
|
# Basic validation
|
145
155
|
if not fqdn or '.' not in fqdn:
|
146
156
|
raise ValueError("Invalid domain name provided")
|
147
|
-
|
157
|
+
if not team_id:
|
158
|
+
raise ValueError("team_id is required")
|
148
159
|
|
149
160
|
result = await sites.sitebay_create_site(
|
150
|
-
client,
|
151
|
-
|
161
|
+
client,
|
162
|
+
team_id,
|
163
|
+
fqdn,
|
164
|
+
wordpress_blog_name,
|
165
|
+
wordpress_first_name,
|
166
|
+
wordpress_last_name,
|
167
|
+
wordpress_email,
|
168
|
+
wordpress_username,
|
169
|
+
wordpress_password,
|
170
|
+
git_url,
|
171
|
+
ready_made_site_name,
|
172
|
+
is_free,
|
152
173
|
)
|
153
174
|
|
154
|
-
|
155
175
|
await ctx.info(f"Successfully created site: {fqdn}")
|
156
176
|
return result
|
157
177
|
|
@@ -160,15 +180,11 @@ async def sitebay_create_site(
|
|
160
180
|
return f"❌ Validation Error: {str(e)}"
|
161
181
|
except ValidationError as e:
|
162
182
|
await ctx.error(f"SiteBay validation error creating site {fqdn}: {str(e)}")
|
163
|
-
|
164
|
-
# Provide detailed feedback for the agent with field-specific errors
|
165
183
|
error_msg = f"❌ Validation Error - Please check your input:\n{str(e)}\n"
|
166
|
-
|
167
184
|
if hasattr(e, 'field_errors') and e.field_errors:
|
168
185
|
error_msg += "\nSpecific field errors:\n"
|
169
186
|
for field, msg in e.field_errors.items():
|
170
187
|
error_msg += f" • {field}: {msg}\n"
|
171
|
-
|
172
188
|
error_msg += "\nPlease adjust your parameters and try again."
|
173
189
|
return error_msg
|
174
190
|
except SiteBayError as e:
|
@@ -182,51 +198,64 @@ async def sitebay_create_site(
|
|
182
198
|
@mcp.tool
|
183
199
|
async def sitebay_update_site(
|
184
200
|
fqdn: str,
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
201
|
+
cf_dev_mode_enabled: Optional[bool] = None,
|
202
|
+
new_fqdn: Optional[str] = None,
|
203
|
+
git_url: Optional[str] = None,
|
204
|
+
http_auth_enabled: Optional[bool] = None,
|
205
|
+
team_id: Optional[str] = None,
|
206
|
+
is_free: Optional[bool] = None,
|
190
207
|
) -> str:
|
191
208
|
"""
|
192
|
-
Update an existing
|
209
|
+
Update an existing SiteBay site configuration.
|
193
210
|
|
194
211
|
Args:
|
195
|
-
fqdn:
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
212
|
+
fqdn: Site domain to update
|
213
|
+
cf_dev_mode_enabled: Enable/disable Cloudflare dev mode
|
214
|
+
new_fqdn: Change the site domain
|
215
|
+
git_url: Set repository URL for deployments
|
216
|
+
http_auth_enabled: Enable/disable HTTP basic auth
|
217
|
+
team_id: Move site to a different team
|
218
|
+
is_free: Toggle free plan flag
|
201
219
|
|
202
220
|
Returns:
|
203
221
|
Confirmation message with updated settings
|
204
222
|
"""
|
205
223
|
client = await initialize_client()
|
206
224
|
return await sites.sitebay_update_site(
|
207
|
-
client,
|
225
|
+
client,
|
226
|
+
fqdn,
|
227
|
+
cf_dev_mode_enabled=cf_dev_mode_enabled,
|
228
|
+
new_fqdn=new_fqdn,
|
229
|
+
git_url=git_url,
|
230
|
+
http_auth_enabled=http_auth_enabled,
|
231
|
+
team_id=team_id,
|
232
|
+
is_free=is_free,
|
208
233
|
)
|
209
234
|
|
210
235
|
|
211
236
|
@mcp.tool
|
212
|
-
async def sitebay_delete_site(fqdn: str
|
237
|
+
async def sitebay_delete_site(fqdn: str) -> str:
|
213
238
|
"""
|
214
239
|
Delete a WordPress site permanently. This action cannot be undone.
|
215
240
|
|
216
241
|
Args:
|
217
242
|
fqdn: The fully qualified domain name of the site to delete
|
218
|
-
confirm: Must be True to actually delete the site (safety check)
|
219
243
|
|
220
244
|
Returns:
|
221
|
-
Confirmation message
|
245
|
+
Confirmation message
|
222
246
|
"""
|
223
247
|
client = await initialize_client()
|
224
|
-
return await sites.sitebay_delete_site(client, fqdn
|
248
|
+
return await sites.sitebay_delete_site(client, fqdn)
|
225
249
|
|
226
250
|
|
227
251
|
# Site Operations Tools
|
228
252
|
@mcp.tool
|
229
|
-
async def sitebay_site_shell_command(
|
253
|
+
async def sitebay_site_shell_command(
|
254
|
+
fqdn: str,
|
255
|
+
command: str,
|
256
|
+
cwd: Optional[str] = None,
|
257
|
+
auto_track_dir: Optional[bool] = None,
|
258
|
+
) -> str:
|
230
259
|
"""
|
231
260
|
Execute a shell command on a WordPress site. Supports WP-CLI commands, system commands, etc.
|
232
261
|
|
@@ -238,11 +267,17 @@ async def sitebay_site_shell_command(fqdn: str, command: str) -> str:
|
|
238
267
|
Command output and execution details
|
239
268
|
"""
|
240
269
|
client = await initialize_client()
|
241
|
-
return await operations.sitebay_site_shell_command(
|
270
|
+
return await operations.sitebay_site_shell_command(
|
271
|
+
client, fqdn, command, cwd=cwd, auto_track_dir=auto_track_dir
|
272
|
+
)
|
242
273
|
|
243
274
|
|
244
275
|
@mcp.tool
|
245
|
-
async def sitebay_site_edit_file(
|
276
|
+
async def sitebay_site_edit_file(
|
277
|
+
fqdn: str,
|
278
|
+
file_path: str,
|
279
|
+
file_edit_using_search_replace_blocks: str,
|
280
|
+
) -> str:
|
246
281
|
"""
|
247
282
|
Edit a file in the site's wp-content directory.
|
248
283
|
|
@@ -255,137 +290,53 @@ async def sitebay_site_edit_file(fqdn: str, file_path: str, content: str) -> str
|
|
255
290
|
Success confirmation with file details
|
256
291
|
"""
|
257
292
|
client = await initialize_client()
|
258
|
-
return await operations.sitebay_site_edit_file(
|
259
|
-
|
260
|
-
|
261
|
-
@mcp.tool
|
262
|
-
async def sitebay_site_get_events(
|
263
|
-
fqdn: str,
|
264
|
-
after_datetime: Optional[str] = None,
|
265
|
-
limit: int = 20
|
266
|
-
) -> str:
|
267
|
-
"""
|
268
|
-
Get recent events for a site (deployments, updates, restores, etc.).
|
269
|
-
|
270
|
-
Args:
|
271
|
-
fqdn: The fully qualified domain name of the site
|
272
|
-
after_datetime: Optional ISO datetime to filter events after (e.g., "2024-01-01T00:00:00Z")
|
273
|
-
limit: Maximum number of events to return (default: 20)
|
274
|
-
|
275
|
-
Returns:
|
276
|
-
Formatted list of recent site events with timestamps and details
|
277
|
-
"""
|
278
|
-
client = await initialize_client()
|
279
|
-
return await operations.sitebay_site_get_events(client, fqdn, after_datetime, limit)
|
293
|
+
return await operations.sitebay_site_edit_file(
|
294
|
+
client, fqdn, file_path, file_edit_using_search_replace_blocks
|
295
|
+
)
|
280
296
|
|
281
297
|
|
282
|
-
|
283
|
-
async def sitebay_site_external_path_list(fqdn: str) -> str:
|
284
|
-
"""
|
285
|
-
List external path configurations for a site (URL proxying/routing).
|
286
|
-
|
287
|
-
Args:
|
288
|
-
fqdn: The fully qualified domain name of the site
|
289
|
-
|
290
|
-
Returns:
|
291
|
-
List of configured external paths with their target URLs and status
|
292
|
-
"""
|
293
|
-
client = await initialize_client()
|
294
|
-
return await operations.sitebay_site_external_path_list(client, fqdn)
|
298
|
+
# Site events tool removed (not present in schema)
|
295
299
|
|
296
300
|
|
297
|
-
|
298
|
-
async def sitebay_site_external_path_create(
|
299
|
-
fqdn: str,
|
300
|
-
path: str,
|
301
|
-
target_url: str,
|
302
|
-
description: Optional[str] = None
|
303
|
-
) -> str:
|
304
|
-
"""
|
305
|
-
Create an external path configuration to proxy requests to external URLs.
|
306
|
-
|
307
|
-
Args:
|
308
|
-
fqdn: The fully qualified domain name of the site
|
309
|
-
path: The path on your site (e.g., "/api", "/external")
|
310
|
-
target_url: The external URL to proxy requests to
|
311
|
-
description: Optional description for this path configuration
|
312
|
-
|
313
|
-
Returns:
|
314
|
-
Success message with external path details
|
315
|
-
"""
|
316
|
-
client = await initialize_client()
|
317
|
-
return await operations.sitebay_site_external_path_create(
|
318
|
-
client, fqdn, path, target_url, description
|
319
|
-
)
|
301
|
+
# External path tools removed (no longer supported)
|
320
302
|
|
321
303
|
|
322
304
|
# Helper Tools
|
323
|
-
@mcp.tool
|
324
|
-
async def sitebay_list_regions() -> str:
|
325
|
-
"""
|
326
|
-
List all available hosting regions for site deployment.
|
327
|
-
|
328
|
-
Returns:
|
329
|
-
List of available regions with their details
|
330
|
-
"""
|
331
|
-
try:
|
332
|
-
client = await initialize_client()
|
333
|
-
regions = await client.list_regions()
|
334
|
-
|
335
|
-
if not regions:
|
336
|
-
return "No regions available."
|
337
|
-
|
338
|
-
result = f"**Available Hosting Regions** ({len(regions)} regions):\n\n"
|
339
|
-
|
340
|
-
for region in regions:
|
341
|
-
result += f"• **{region.get('name', 'Unknown')}**\n"
|
342
|
-
result += f" - Location: {region.get('location', 'Unknown')}\n"
|
343
|
-
result += f" - Status: {region.get('status', 'Unknown')}\n"
|
344
|
-
|
345
|
-
if region.get('description'):
|
346
|
-
result += f" - Description: {region.get('description')}\n"
|
347
|
-
|
348
|
-
result += "\n"
|
349
|
-
|
350
|
-
return result
|
351
|
-
|
352
|
-
except SiteBayError as e:
|
353
|
-
return f"Error listing regions: {str(e)}"
|
354
305
|
|
355
306
|
|
356
307
|
@mcp.tool
|
357
|
-
async def
|
308
|
+
async def sitebay_list_ready_made_sites() -> str:
|
358
309
|
"""
|
359
|
-
List
|
310
|
+
List available ready-made sites for quick launches.
|
360
311
|
|
361
312
|
Returns:
|
362
|
-
List of
|
313
|
+
List of ready-made sites with descriptions
|
363
314
|
"""
|
364
315
|
try:
|
365
316
|
client = await initialize_client()
|
366
|
-
|
317
|
+
items = await client.list_ready_made_sites()
|
367
318
|
|
368
|
-
if not
|
369
|
-
return "No
|
319
|
+
if not items:
|
320
|
+
return "No ready-made sites available."
|
370
321
|
|
371
|
-
result = f"**Available
|
322
|
+
result = f"**Available Ready-made Sites** ({len(items)}):\n\n"
|
372
323
|
|
373
|
-
for
|
374
|
-
result += f"• **{
|
375
|
-
result += f" - ID: {
|
324
|
+
for item in items:
|
325
|
+
result += f"• **{item.get('name', 'Unknown')}**\n"
|
326
|
+
result += f" - ID: {item.get('id', 'Unknown')}\n"
|
376
327
|
|
377
|
-
if
|
378
|
-
result += f" - Description: {
|
328
|
+
if item.get('description'):
|
329
|
+
result += f" - Description: {item.get('description')}\n"
|
379
330
|
|
380
|
-
if
|
381
|
-
result += f" - Category: {
|
331
|
+
if item.get('category'):
|
332
|
+
result += f" - Category: {item.get('category')}\n"
|
382
333
|
|
383
334
|
result += "\n"
|
384
335
|
|
385
336
|
return result
|
386
337
|
|
387
338
|
except SiteBayError as e:
|
388
|
-
return f"Error listing
|
339
|
+
return f"Error listing ready-made sites: {str(e)}"
|
389
340
|
|
390
341
|
|
391
342
|
@mcp.tool
|
@@ -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" -
|
414
|
-
result += f" -
|
415
|
-
|
416
|
-
|
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
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
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
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
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 {
|
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
|
-
|
459
|
-
"
|
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
|
-
|
480
|
-
|
481
|
-
|
482
|
-
method: str = "
|
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
|
-
|
490
|
-
|
491
|
-
|
492
|
-
method: HTTP method (
|
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 {
|
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
|
-
|
504
|
-
"
|
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
|
-
|
526
|
-
|
527
|
-
|
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
|
-
|
534
|
-
|
535
|
-
|
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 {
|
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
|
-
|
546
|
-
"
|
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
|
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 =
|
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:
|
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
|
-
|
712
|
-
|
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
|
-
|
720
|
-
|
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
|
-
|
731
|
-
|
732
|
-
|
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
|
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"• **
|
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" -
|
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
|
-
|
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,
|
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
|
-
|
885
|
-
|
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()
|