Skip to main content
Glama
server.py26.2 kB
""" SiteBay MCP Server Main server implementation that provides MCP tools for SiteBay WordPress hosting platform. """ import asyncio import os import sys import argparse from typing import Any, Optional from pydantic import UUID4 import fastmcp from fastmcp import FastMCP from fastmcp.server import Context from .auth import SiteBayAuth from .client import SiteBayClient from .exceptions import ConfigurationError, SiteBayError, ValidationError from .tools import sites, operations from . import resources # Create the MCP server instance mcp: FastMCP = FastMCP("SiteBay WordPress Hosting") # Global client instance (will be initialized on startup) sitebay_client: Optional[SiteBayClient] = None async def initialize_client() -> SiteBayClient: """Initialize the SiteBay client with authentication""" global sitebay_client if sitebay_client is None: try: auth = SiteBayAuth() sitebay_client = SiteBayClient(auth) # Avoid upfront network calls during initialization so startup stays fast. except Exception as e: raise ConfigurationError(f"Failed to initialize SiteBay client: {str(e)}") return sitebay_client # Site Management Tools @mcp.tool async def sitebay_list_sites(ctx: Context, team_id: Optional[UUID4] = None) -> str: """ List all WordPress sites for the authenticated user. Args: team_id: Optional team ID (UUID4) to filter sites by team Returns: Formatted string with site details including domain, status, region, and versions """ try: await ctx.info("Fetching WordPress sites from SiteBay") if team_id: await ctx.info(f"Filtering by team ID: {team_id}") client = await initialize_client() team_id_str = str(team_id) if team_id is not None else None result = await sites.sitebay_list_sites(client, team_id_str) await ctx.info("Successfully retrieved site list") return result except SiteBayError as e: await ctx.error(f"SiteBay API error: {str(e)}") return f"❌ SiteBay Error: {str(e)}" except ValueError as e: await ctx.error(f"Validation error listing sites: {str(e)}") return f"❌ Validation Error: {str(e)}" except Exception as e: await ctx.error(f"Unexpected error listing sites: {str(e)}") return f"❌ Unexpected error: {str(e)}" @mcp.tool async def sitebay_get_site(ctx: Context, fqdn: str) -> str: """ Get detailed information about a specific WordPress site. Args: fqdn: The fully qualified domain name of the site (e.g., "www.example.com") Returns: Detailed site information including status, versions, URLs, and configuration """ try: await ctx.info(f"Fetching details for site: {fqdn}") client = await initialize_client() result = await sites.sitebay_get_site(client, fqdn) await ctx.info(f"Successfully retrieved details for {fqdn}") return result except SiteBayError as e: await ctx.error(f"SiteBay API error for {fqdn}: {str(e)}") return f"❌ SiteBay Error: {str(e)}" except Exception as e: await ctx.error(f"Unexpected error getting site {fqdn}: {str(e)}") return f"❌ Unexpected error: {str(e)}" @mcp.tool async def sitebay_create_site( ctx: Context, team_id: str, fqdn: str, wordpress_blog_name: str, wordpress_first_name: str, wordpress_last_name: str, wordpress_email: str, wordpress_username: str, wordpress_password: str, git_url: Optional[str] = None, ready_made_site_name: Optional[str] = None, is_free: Optional[bool] = None, ) -> str: """ Create a new WordPress site (SiteLiveCreate schema). Args: team_id: Team UUID that owns the site fqdn: The fully qualified domain name for the new site (e.g., "www.example.org") wordpress_blog_name: Blog/site title wordpress_first_name: Admin first name wordpress_last_name: Admin last name wordpress_email: Admin email address wordpress_username: Admin username wordpress_password: Admin password (strong) git_url: Optional Git repository URL ready_made_site_name: Optional ready-made site name is_free: Optional flag for free plan Returns: Success message with new site details and access information """ try: await ctx.info(f"Starting site creation for: {fqdn}") client = await initialize_client() # Basic validation if not fqdn or '.' not in fqdn: raise ValueError("Invalid domain name provided") if not team_id: raise ValueError("team_id is required") result = await sites.sitebay_create_site( client, team_id, fqdn, wordpress_blog_name, wordpress_first_name, wordpress_last_name, wordpress_email, wordpress_username, wordpress_password, git_url, ready_made_site_name, is_free, ) await ctx.info(f"Successfully created site: {fqdn}") return result except ValueError as e: await ctx.error(f"Validation error creating site {fqdn}: {str(e)}") return f"❌ Validation Error: {str(e)}" except ValidationError as e: await ctx.error(f"SiteBay validation error creating site {fqdn}: {str(e)}") error_msg = f"❌ Validation Error - Please check your input:\n{str(e)}\n" if hasattr(e, 'field_errors') and e.field_errors: error_msg += "\nSpecific field errors:\n" for field, msg in e.field_errors.items(): error_msg += f" • {field}: {msg}\n" error_msg += "\nPlease adjust your parameters and try again." return error_msg except SiteBayError as e: await ctx.error(f"SiteBay API error creating site {fqdn}: {str(e)}") return f"❌ SiteBay Error: {str(e)}" except Exception as e: await ctx.error(f"Unexpected error creating site {fqdn}: {str(e)}") return f"❌ Unexpected error: {str(e)}" @mcp.tool async def sitebay_update_site( fqdn: str, cf_dev_mode_enabled: Optional[bool] = None, new_fqdn: Optional[str] = None, git_url: Optional[str] = None, http_auth_enabled: Optional[bool] = None, team_id: Optional[str] = None, is_free: Optional[bool] = None, ) -> str: """ Update an existing SiteBay site configuration. Args: fqdn: Site domain to update cf_dev_mode_enabled: Enable/disable Cloudflare dev mode new_fqdn: Change the site domain git_url: Set repository URL for deployments http_auth_enabled: Enable/disable HTTP basic auth team_id: Move site to a different team is_free: Toggle free plan flag Returns: Confirmation message with updated settings """ client = await initialize_client() return await sites.sitebay_update_site( client, fqdn, cf_dev_mode_enabled=cf_dev_mode_enabled, new_fqdn=new_fqdn, git_url=git_url, http_auth_enabled=http_auth_enabled, team_id=team_id, is_free=is_free, ) @mcp.tool async def sitebay_delete_site(fqdn: str) -> str: """ Delete a WordPress site permanently. This action cannot be undone. Args: fqdn: The fully qualified domain name of the site to delete Returns: Confirmation message """ client = await initialize_client() return await sites.sitebay_delete_site(client, fqdn) # Site Operations Tools @mcp.tool async def sitebay_site_shell_command( fqdn: str, command: str, cwd: Optional[str] = None, auto_track_dir: Optional[bool] = None, ) -> str: """ Execute a shell command on a WordPress site. Supports WP-CLI commands, system commands, etc. Args: fqdn: The fully qualified domain name of the site command: The shell command to execute (e.g., "wp plugin list", "ls -la", "wp search-replace") Returns: Command output and execution details """ client = await initialize_client() return await operations.sitebay_site_shell_command( client, fqdn, command, cwd=cwd, auto_track_dir=auto_track_dir ) @mcp.tool async def sitebay_site_edit_file( fqdn: str, file_path: str, file_edit_using_search_replace_blocks: str, ) -> str: """ Edit a file in the site's wp-content directory. Args: fqdn: The fully qualified domain name of the site file_path: Path to the file in wp-content (e.g., "wp-content/themes/mytheme/style.css") content: New content for the file Returns: Success confirmation with file details """ client = await initialize_client() return await operations.sitebay_site_edit_file( client, fqdn, file_path, file_edit_using_search_replace_blocks ) # Site events tool removed (not present in schema) # External path tools removed (no longer supported) # Helper Tools @mcp.tool async def sitebay_list_ready_made_sites() -> str: """ List available ready-made sites for quick launches. Returns: List of ready-made sites with descriptions """ try: client = await initialize_client() items = await client.list_ready_made_sites() if not items: return "No ready-made sites available." result = f"**Available Ready-made Sites** ({len(items)}):\n\n" for item in items: result += f"• **{item.get('name', 'Unknown')}**\n" result += f" - ID: {item.get('id', 'Unknown')}\n" if item.get('description'): result += f" - Description: {item.get('description')}\n" if item.get('category'): result += f" - Category: {item.get('category')}\n" result += "\n" return result except SiteBayError as e: return f"Error listing ready-made sites: {str(e)}" @mcp.tool async def sitebay_list_teams(ctx: Context) -> str: """ List all teams for the authenticated user. Returns: Formatted list of teams with their details and member information """ try: await ctx.info("Fetching teams from SiteBay") client = await initialize_client() teams = await client.list_teams() if not teams: return "No teams found for your account." result = f"**Your Teams** ({len(teams)} teams):\n\n" for team in teams: result += f"• **{team.get('name', 'Unknown')}**\n" result += f" - ID: {team.get('id', 'Unknown')}\n" result += f" - Plan: {team.get('plan_type_name', 'Unknown')}\n" result += f" - Active: {team.get('is_active', 'Unknown')}\n" result += f" - Default: {team.get('is_default', 'Unknown')}\n" result += f" - Created: {team.get('created_at', 'Unknown')}\n\n" await ctx.info("Successfully retrieved teams list") return result except SiteBayError as e: await ctx.error(f"SiteBay API error: {str(e)}") return f"❌ SiteBay Error: {str(e)}" except Exception as e: await ctx.error(f"Unexpected error listing teams: {str(e)}") return f"❌ Unexpected error: {str(e)}" # Proxy Tools @mcp.tool async def sitebay_wordpress_proxy( ctx: Context, fqdn: str, path: str = "/wp-json/wp/v2/", query_params_json: str = "", method: str = "get", ) -> str: """ Proxy requests to a WordPress site's REST API. Args: fqdn: The site domain path: WordPress API path (e.g., "/wp-json/wp/v2/posts") query_params_json: Optional JSON string for payload or query params method: HTTP method (get, post, put, delete) Returns: WordPress API response """ try: await ctx.info(f"WordPress proxy request to {fqdn}{path or ''}") client = await initialize_client() proxy_data: dict[str, Any] = {"fqdn": fqdn, "method": method, "path": path} if query_params_json: proxy_data["query_params_json"] = query_params_json result = await client.wordpress_proxy(proxy_data) return f"✅ WordPress API Response:\n```json\n{result}\n```" except SiteBayError as e: await ctx.error(f"WordPress proxy error: {str(e)}") return f"❌ WordPress Proxy Error: {str(e)}" except Exception as e: await ctx.error(f"Unexpected proxy error: {str(e)}") return f"❌ Unexpected error: {str(e)}" @mcp.tool async def sitebay_shopify_proxy( ctx: Context, shop_name: str, path: str = "/admin/api/2024-04", query_params_json: str = "", method: str = "get", ) -> str: """ Proxy requests to a Shopify Admin API. Args: shop_name: Shopify shop name path: Shopify API path (e.g., "/admin/api/2024-04/products.json") query_params_json: Optional JSON string for payload or query params method: HTTP method (get, post, put, delete) Returns: Shopify API response """ try: await ctx.info(f"Shopify proxy request to {shop_name}{path or ''}") client = await initialize_client() proxy_data: dict[str, Any] = {"shop_name": shop_name, "method": method, "path": path} if query_params_json: proxy_data["query_params_json"] = query_params_json result = await client.shopify_proxy(proxy_data) return f"✅ Shopify API Response:\n```json\n{result}\n```" except SiteBayError as e: await ctx.error(f"Shopify proxy error: {str(e)}") return f"❌ Shopify Proxy Error: {str(e)}" except Exception as e: await ctx.error(f"Unexpected proxy error: {str(e)}") return f"❌ Unexpected error: {str(e)}" @mcp.tool async def sitebay_posthog_proxy( ctx: Context, path: str, query_params_json: str = "", method: str = "get", ) -> str: """ Proxy POST requests to PostHog analytics API. Args: path: PostHog API path query_params_json: Optional JSON string for payload or query params method: HTTP method (get, post, put, delete) Returns: PostHog API response """ try: await ctx.info(f"PostHog proxy request to {path}") client = await initialize_client() proxy_data: dict[str, Any] = {"path": path, "method": method} if query_params_json: proxy_data["query_params_json"] = query_params_json result = await client.posthog_proxy(proxy_data) return f"✅ PostHog API Response:\n```json\n{result}\n```" except SiteBayError as e: await ctx.error(f"PostHog proxy error: {str(e)}") return f"❌ PostHog Proxy Error: {str(e)}" except Exception as e: await ctx.error(f"Unexpected proxy error: {str(e)}") return f"❌ Unexpected error: {str(e)}" # Staging tools removed (no longer supported) # Backup/Restore Tools @mcp.tool async def sitebay_backup_list_commits( ctx: Context, fqdn: str, number_to_fetch: int = 1 ) -> str: """ List available backup commits for point-in-time restore. Args: fqdn: The site domain number_to_fetch: Number of backup entries to fetch (default: 1) Returns: List of available backup commits """ try: await ctx.info(f"Fetching backup commits for {fqdn}") client = await initialize_client() commits = await client.get_backup_commits(fqdn, number_to_fetch) if not commits: return f"No backup commits found for {fqdn}." result = f"**Available Backup Commits for {fqdn}** ({len(commits)} entries):\n\n" for commit in commits: result += f"• **{commit.get('created_at', 'Unknown time')}**\n" result += f" - ID: {commit.get('id', 'Unknown')}\n" result += f" - Commit Hash: {commit.get('commit_hash', 'Unknown')}\n" result += f" - Tables Saved: {len(commit.get('tables_saved', []))} tables\n" result += f" - Status: {'Completed' if commit.get('finished_at') else 'In Progress'}\n" result += "\n" await ctx.info(f"Successfully retrieved backup commits for {fqdn}") return result except SiteBayError as e: await ctx.error(f"Error fetching backup commits: {str(e)}") return f"❌ Backup Error: {str(e)}" except Exception as e: await ctx.error(f"Unexpected backup error: {str(e)}") return f"❌ Unexpected error: {str(e)}" @mcp.tool async def sitebay_backup_restore( ctx: Context, fqdn: str, restore_point: Optional[str] = None, for_stage_site: Optional[bool] = None, restore_db: Optional[bool] = None, restore_wp_content: Optional[bool] = None, delete_extra_files: Optional[bool] = None, dolt_restore_hash: Optional[str] = None, is_dry_run: Optional[bool] = None, ) -> str: """ Restore a site to a previous point in time. Args (PITRestoreCreate schema): fqdn: The site domain restore_point: ISO datetime string (or omit for latest) for_stage_site: Whether to restore the stage site restore_db: Restore database (default true) restore_wp_content: Restore wp-content (default true) delete_extra_files: Delete extra files from target (default false) dolt_restore_hash: Optional Dolt hash to restore DB is_dry_run: Simulate restore without applying changes Returns: Restore operation confirmation """ try: await ctx.info(f"Starting point-in-time restore for {fqdn}") client = await initialize_client() restore_data: dict[str, Any] = {} if restore_point is not None: restore_data["restore_point"] = restore_point if for_stage_site is not None: restore_data["for_stage_site"] = for_stage_site if restore_db is not None: restore_data["restore_db"] = restore_db if restore_wp_content is not None: restore_data["restore_wp_content"] = restore_wp_content if delete_extra_files is not None: restore_data["delete_extra_files"] = delete_extra_files if dolt_restore_hash is not None: restore_data["dolt_restore_hash"] = dolt_restore_hash if is_dry_run is not None: restore_data["is_dry_run"] = is_dry_run result = await client.create_restore(fqdn, restore_data) await ctx.info(f"Successfully initiated restore for {fqdn}") return ( "✅ **Point-in-Time Restore Initiated**\n\n" f"Restore operation for {fqdn} has been started." ) except SiteBayError as e: await ctx.error(f"Error starting restore: {str(e)}") return f"❌ Restore Error: {str(e)}" except Exception as e: await ctx.error(f"Unexpected restore error: {str(e)}") return f"❌ Unexpected error: {str(e)}" # Account Tools @mcp.tool async def sitebay_account_affiliates(ctx: Context) -> str: """ Get affiliate referral information. Returns: List of users who signed up using your affiliate links """ try: await ctx.info("Fetching affiliate referrals") client = await initialize_client() affiliates = await client.get_affiliate_referrals() if not affiliates: return "No affiliate referrals found." result = f"**Your Affiliate Referrals** ({len(affiliates)} referrals):\n\n" for affiliate in affiliates: result += f"• **Email**: {affiliate.get('email', 'Unknown')}\n" result += f" - Name: {affiliate.get('full_name', 'Unknown')}\n" result += f" - Signed up: {affiliate.get('created_at', 'Unknown')}\n" result += f" - Active: {affiliate.get('is_active', 'Unknown')}\n\n" await ctx.info("Successfully retrieved affiliate referrals") return result except SiteBayError as e: await ctx.error(f"Error fetching affiliates: {str(e)}") return f"❌ Affiliate Error: {str(e)}" except Exception as e: await ctx.error(f"Unexpected affiliate error: {str(e)}") return f"❌ Unexpected error: {str(e)}" @mcp.tool async def sitebay_account_create_checkout( ctx: Context, plan_name: str = "starter", interval: str = "month", team_id: Optional[str] = None ) -> str: """ Create a Stripe checkout session for team billing. Args: plan_name: Plan type ("starter", "business", "micro") interval: Billing interval ("month", "year") team_id: Optional team ID to purchase for Returns: Stripe checkout URL """ try: await ctx.info(f"Creating checkout session for {plan_name} plan") client = await initialize_client() checkout_data = { "plan_name": plan_name, "interval": interval } if team_id: checkout_data["for_team_id"] = team_id result = await client.create_checkout_session(checkout_data) await ctx.info("Successfully created checkout session") return f"✅ **Checkout Session Created**\n\nPlan: {plan_name} ({interval}ly)\nCheckout URL: {result.get('url', 'URL not provided')}" except SiteBayError as e: await ctx.error(f"Error creating checkout: {str(e)}") return f"❌ Checkout Error: {str(e)}" except Exception as e: await ctx.error(f"Unexpected checkout error: {str(e)}") return f"❌ Unexpected error: {str(e)}" # Resources @mcp.resource("sitebay://site/{site_fqdn}/config") async def site_config_resource(ctx: Context, site_fqdn: str) -> str: """ Get site configuration as a readable resource. Args: site_fqdn: The fully qualified domain name of the site Returns: JSON formatted site configuration including technical specs, URLs, and features """ return await resources.get_site_config_resource(ctx, site_fqdn) # Site events resource removed (not present in schema) @mcp.resource("sitebay://account/summary") async def account_summary_resource(ctx: Context) -> str: """ Get account summary as a readable resource. Returns: JSON formatted account overview including site counts, ready-made catalog size, and recent activity """ return await resources.get_account_summary_resource(ctx) async def cleanup(): """Cleanup function to close the client connection""" global sitebay_client if sitebay_client: await sitebay_client.close() def _run_stdio(): """Run the MCP server over STDIO (default).""" mcp.run() def _run_http(host: str, port: int): """Run the MCP server over HTTP (streamable).""" server_url = f"http://{host}:{port}{fastmcp.settings.streamable_http_path}" if hasattr(mcp, "run_http_async"): print(f"Starting SiteBay MCP HTTP server on {server_url}") asyncio.run( mcp.run_http_async(host=host, port=port, transport="streamable-http") ) elif hasattr(mcp, "run_http"): print(f"Starting SiteBay MCP HTTP server on {server_url}") mcp.run_http(host=host, port=port) else: raise RuntimeError( "FastMCP does not support HTTP transport in this environment. " "Please upgrade fastmcp to >= 2.9." ) def main(): """Main entry point for the MCP server. Supports both STDIO (default) and HTTP transport. Use one of: - stdio (default): `sitebay-mcp` - http: `sitebay-mcp --http --port 7823 --host 0.0.0.0` Environment variables (used if flags not provided): - MCP_TRANSPORT=stdio|http - MCP_HTTP_HOST (default: 127.0.0.1) - MCP_HTTP_PORT or PORT (default: 7823) """ parser = argparse.ArgumentParser(prog="sitebay-mcp", add_help=True) parser.add_argument( "--http", action="store_true", help="Run the MCP server using HTTP transport (streamable)", ) parser.add_argument( "--transport", choices=["stdio", "http"], help="Transport mode (overrides --http)", ) parser.add_argument( "--host", default=None, help="HTTP host to bind (default: 127.0.0.1)", ) parser.add_argument( "--port", type=int, default=None, help="HTTP port to bind (default: 7823)", ) args = parser.parse_args() # Resolve transport env_transport = os.getenv("MCP_TRANSPORT") transport = ( args.transport if args.transport else ("http" if args.http else (env_transport or "stdio")) ) # Set up cleanup try: import atexit atexit.register(lambda: asyncio.run(cleanup())) except Exception: pass try: if transport == "http": host = args.host or os.getenv("MCP_HTTP_HOST") or "127.0.0.1" port = ( args.port or int(os.getenv("MCP_HTTP_PORT") or os.getenv("PORT") or 7823) ) _run_http(host, port) else: _run_stdio() except KeyboardInterrupt: print("\nShutting down SiteBay MCP Server...") asyncio.run(cleanup()) sys.exit(0) except Exception as e: print(f"Error starting SiteBay MCP Server: {e}") sys.exit(1) if __name__ == "__main__": main()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sitebay/sitebay-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server