diff --git a/README.md b/README.md index 516fef9..e62bae9 100644 --- a/README.md +++ b/README.md @@ -268,9 +268,10 @@ Some MCP Hub tools require companion WordPress plugins: | Tools | Requirement | |-------|-------------| -| SEO tools (`wordpress_get_post_seo`, etc.) | [SEO API Bridge](wordpress-plugin/seo-api-bridge/) + Rank Math or Yoast SEO | +| SEO tools (`wordpress_get_post_seo`, etc.) | [SEO API Bridge](wordpress-plugin/seo-api-bridge/) ([Download ZIP](wordpress-plugin/seo-api-bridge.zip)) + Rank Math or Yoast SEO | | WP-CLI tools (15 tools: `wp_cache_*`, `wp_db_*`, etc.) | Docker socket + `CONTAINER` env var | | WordPress Advanced database/system tools | Docker socket + `CONTAINER` env var | +| OpenPanel analytics integration | [OpenPanel](wordpress-plugin/openpanel/) ([Download ZIP](wordpress-plugin/openpanel.zip)) | | WooCommerce tools | WooCommerce plugin (separate `WOOCOMMERCE_` config) | **Docker socket** is needed for WP-CLI and WordPress Advanced system tools. Add to your docker-compose: diff --git a/core/health.py b/core/health.py index 9296049..f828f91 100644 --- a/core/health.py +++ b/core/health.py @@ -9,8 +9,7 @@ This module provides comprehensive health monitoring capabilities including: - Dependency health checks - System uptime tracking -Author: Coolify MCP Team -Version: 7.2 +Author: MCP Hub Team """ import json @@ -24,6 +23,7 @@ from typing import Any from core.audit_log import AuditLogger from core.project_manager import ProjectManager +from core.site_manager import SiteManager logger = logging.getLogger(__name__) @@ -133,6 +133,7 @@ class HealthMonitor: audit_logger: AuditLogger | None = None, metrics_retention_hours: int = 24, max_metrics_per_project: int = 1000, + site_manager: SiteManager | None = None, ): """ Initialize health monitor. @@ -142,8 +143,10 @@ class HealthMonitor: audit_logger: Optional audit logger for logging health events metrics_retention_hours: Hours to retain historical metrics max_metrics_per_project: Maximum metrics to store per project + site_manager: Optional SiteManager for comprehensive site discovery """ self.project_manager = project_manager + self.site_manager = site_manager self.audit_logger = audit_logger self.metrics_retention_hours = metrics_retention_hours self.max_metrics_per_project = max_metrics_per_project @@ -401,9 +404,17 @@ class HealthMonitor: start_time = time.time() try: - # Get plugin instance + # Get plugin instance from ProjectManager plugin = self.project_manager.projects.get(project_id) - if not plugin: + + if plugin: + # Perform health check via plugin instance + health_result = await plugin.health_check() + elif self.site_manager: + # Fallback: site exists in SiteManager but not ProjectManager + # (e.g. WooCommerce uses CONSUMER_KEY, not USERNAME/APP_PASSWORD) + health_result = await self._basic_site_health_check(project_id) + else: return ProjectHealthStatus( project_id=project_id, healthy=False, @@ -413,9 +424,6 @@ class HealthMonitor: recent_errors=["Project not found"], alerts=["CRITICAL: Project not found"], ) - - # Perform health check - health_result = await plugin.health_check() response_time_ms = (time.time() - start_time) * 1000 # Handle both dict and string (JSON) responses @@ -495,6 +503,43 @@ class HealthMonitor: alerts=[f"CRITICAL: Health check failed - {error_msg}"], ) + async def _basic_site_health_check(self, project_id: str) -> dict[str, Any]: + """Basic HTTP health check for sites not in ProjectManager.""" + import aiohttp + + if not self.site_manager: + return {"healthy": False, "message": "SiteManager not available"} + + # Parse plugin_type and site_id from full_id (e.g. "woocommerce_site1") + parts = project_id.split("_", 1) + if len(parts) < 2: + return {"healthy": False, "message": f"Invalid project_id format: {project_id}"} + + plugin_type = parts[0] + site_id = parts[1] + + try: + config = self.site_manager.get_site_config(plugin_type, site_id) + except (KeyError, ValueError): + return {"healthy": False, "message": f"Site not found: {project_id}"} + + url = config.url + if not url: + return {"healthy": False, "message": "No URL configured for site"} + + try: + async with aiohttp.ClientSession() as session: + async with session.get( + url, timeout=aiohttp.ClientTimeout(total=10), ssl=False + ) as resp: + return { + "healthy": resp.status < 500, + "status_code": resp.status, + "message": f"HTTP {resp.status} from {url}", + } + except Exception as e: + return {"healthy": False, "message": f"Connection failed: {e}"} + async def check_all_projects_health(self, include_metrics: bool = True) -> dict[str, Any]: """ Check health of all projects. @@ -507,8 +552,14 @@ class HealthMonitor: """ health_statuses = {} + # Collect all known project/site IDs from both sources + all_project_ids = set(self.project_manager.projects.keys()) + if self.site_manager: + for site_info in self.site_manager.list_all_sites(): + all_project_ids.add(site_info["full_id"]) + # Check each project - for project_id in self.project_manager.projects.keys(): + for project_id in sorted(all_project_ids): status = await self.check_project_health(project_id, include_metrics) health_statuses[project_id] = status.to_dict() @@ -673,7 +724,10 @@ def get_health_monitor() -> HealthMonitor | None: def initialize_health_monitor( - project_manager: ProjectManager, audit_logger: AuditLogger | None = None, **kwargs + project_manager: ProjectManager, + audit_logger: AuditLogger | None = None, + site_manager: SiteManager | None = None, + **kwargs, ) -> HealthMonitor: """ Initialize the global health monitor. @@ -681,11 +735,14 @@ def initialize_health_monitor( Args: project_manager: Project manager instance audit_logger: Optional audit logger + site_manager: Optional SiteManager for comprehensive site discovery **kwargs: Additional configuration options Returns: HealthMonitor instance """ global _health_monitor - _health_monitor = HealthMonitor(project_manager, audit_logger, **kwargs) + _health_monitor = HealthMonitor( + project_manager, audit_logger, site_manager=site_manager, **kwargs + ) return _health_monitor diff --git a/core/templates/base.html b/core/templates/base.html index d804513..cf370c3 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -4,6 +4,7 @@ {% block title %}MCP Hub{% endblock %} + diff --git a/core/templates/dashboard/base.html b/core/templates/dashboard/base.html index b0bceb9..406a3eb 100644 --- a/core/templates/dashboard/base.html +++ b/core/templates/dashboard/base.html @@ -4,6 +4,7 @@ {% block title %}{{ t.dashboard }}{% endblock %} - MCP Hub + diff --git a/core/templates/dashboard/login.html b/core/templates/dashboard/login.html index 0dafedb..0c75ece 100644 --- a/core/templates/dashboard/login.html +++ b/core/templates/dashboard/login.html @@ -4,6 +4,7 @@ {{ t.login_title }} - MCP Hub + diff --git a/core/templates/static/logo.svg b/core/templates/static/logo.svg new file mode 100644 index 0000000..c366a64 --- /dev/null +++ b/core/templates/static/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server.py b/server.py index 3b54376..c019357 100644 --- a/server.py +++ b/server.py @@ -1055,6 +1055,7 @@ from core import initialize_health_monitor health_monitor = initialize_health_monitor( project_manager=project_manager, audit_logger=audit_logger, + site_manager=site_manager, metrics_retention_hours=24, max_metrics_per_project=1000, ) @@ -4441,6 +4442,13 @@ def create_multi_endpoint_app(transport: str = "streamable-http"): safe_name = project_id.replace("-", "_").replace(".", "_") routes.append(Mount(mount_path, app=project_app, name=f"mcp_project_{safe_name}")) + # Static files (favicon, logo) + _static_dir = os.path.join(os.path.dirname(__file__), "core", "templates", "static") + if os.path.isdir(_static_dir): + from starlette.staticfiles import StaticFiles + + routes.append(Mount("/static", app=StaticFiles(directory=_static_dir), name="static")) + # Main admin endpoint (must be last - catches all remaining routes) routes.append(Mount("/", app=main_app, name="mcp_admin")) diff --git a/wordpress-plugin/openpanel.zip b/wordpress-plugin/openpanel.zip index 4123431..2ed3721 100644 Binary files a/wordpress-plugin/openpanel.zip and b/wordpress-plugin/openpanel.zip differ diff --git a/wordpress-plugin/openpanel/openpanel.php b/wordpress-plugin/openpanel/openpanel.php index 08610ab..e3507b9 100644 --- a/wordpress-plugin/openpanel/openpanel.php +++ b/wordpress-plugin/openpanel/openpanel.php @@ -3,11 +3,13 @@ * Plugin Name: OpenPanel * Description: Activate OpenPanel to start tracking your website. Supports both Cloud and Self-Hosted instances. * Version: 1.1.1 - * Author: OpenPanel / Airano + * Plugin URI: https://github.com/airano-ir/mcphub + * Author: OpenPanel / MCP Hub + * Author URI: https://github.com/airano-ir * License: GPLv2 or later * Requires at least: 5.8 * Requires PHP: 7.4 - * Tested up to: 6.8 + * Tested up to: 6.9 * Text Domain: openpanel */ @@ -479,7 +481,7 @@ final class OP_WP_Proxy { // Content-Type is a special header NOT prefixed with HTTP_ in PHP // This is critical for OpenPanel API which requires application/json if (!empty($_SERVER['CONTENT_TYPE'])) { - $headers['Content-Type'] = sanitize_text_field($_SERVER['CONTENT_TYPE']); + $headers['Content-Type'] = sanitize_text_field(wp_unslash($_SERVER['CONTENT_TYPE'])); } foreach ($_SERVER as $name => $value) { diff --git a/wordpress-plugin/openpanel/readme.txt b/wordpress-plugin/openpanel/readme.txt index 21f166c..be9cad1 100644 --- a/wordpress-plugin/openpanel/readme.txt +++ b/wordpress-plugin/openpanel/readme.txt @@ -1,14 +1,14 @@ === OpenPanel === Contributors: openpanel, airano -Tags: analytics, web analytics, privacy-friendly, tracking, proxy, self-hosted +Tags: analytics, privacy-friendly, tracking, self-hosted, proxy Requires at least: 5.8 -Tested up to: 6.8 +Tested up to: 6.9 Requires PHP: 7.4 Stable tag: 1.1.1 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html -OpenPanel WordPress plugin - Privacy-friendly analytics with ad-blocker resistance. Supports both OpenPanel Cloud and Self-Hosted instances. Inline tracking scripts and proxy API calls through your domain. +Privacy-friendly analytics with ad-blocker resistance. Supports OpenPanel Cloud and Self-Hosted instances. == Description == diff --git a/wordpress-plugin/seo-api-bridge.zip b/wordpress-plugin/seo-api-bridge.zip index 9d13a25..346125c 100644 Binary files a/wordpress-plugin/seo-api-bridge.zip and b/wordpress-plugin/seo-api-bridge.zip differ diff --git a/wordpress-plugin/seo-api-bridge/README.md b/wordpress-plugin/seo-api-bridge/README.md index de8aa78..8322d81 100644 --- a/wordpress-plugin/seo-api-bridge/README.md +++ b/wordpress-plugin/seo-api-bridge/README.md @@ -2,7 +2,7 @@ **Version:** 1.3.0 **Requires:** WordPress 5.0+, PHP 7.4+ -**License:** MIT +**License:** GPLv2 or later ## Description @@ -421,7 +421,7 @@ For issues related to: ## License -MIT License - See LICENSE file for details +GPLv2 or later - See [LICENSE](https://www.gnu.org/licenses/gpl-2.0.html) for details ## Credits diff --git a/wordpress-plugin/seo-api-bridge/readme.txt b/wordpress-plugin/seo-api-bridge/readme.txt index 79dcca9..eb90263 100644 --- a/wordpress-plugin/seo-api-bridge/readme.txt +++ b/wordpress-plugin/seo-api-bridge/readme.txt @@ -2,11 +2,11 @@ Contributors: airano Tags: seo, rest-api, rank-math, yoast, mcp Requires at least: 5.0 -Tested up to: 6.7 +Tested up to: 6.9 Requires PHP: 7.4 Stable tag: 1.3.0 -License: MIT -License URI: https://opensource.org/licenses/MIT +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html Exposes Rank Math SEO and Yoast SEO meta fields via WordPress REST API for use with MCP servers and AI agents. diff --git a/wordpress-plugin/seo-api-bridge/seo-api-bridge.php b/wordpress-plugin/seo-api-bridge/seo-api-bridge.php index e013146..7670e93 100644 --- a/wordpress-plugin/seo-api-bridge/seo-api-bridge.php +++ b/wordpress-plugin/seo-api-bridge/seo-api-bridge.php @@ -6,7 +6,7 @@ * Version: 1.3.0 * Author: MCP Hub * Author URI: https://github.com/airano-ir - * License: MIT + * License: GPL-2.0-or-later * Requires at least: 5.0 * Requires PHP: 7.4 * Text Domain: seo-api-bridge @@ -690,8 +690,8 @@ class SEO_API_Bridge { $supported_types = implode(', ', $this->supported_post_types); echo '
'; - echo '

SEO API Bridge v' . self::VERSION . ': Successfully registered meta fields for ' . implode(' and ', $active_plugins) . '.

'; - echo '

Supported post types: ' . $supported_types . '

'; + echo '

SEO API Bridge v' . esc_html( self::VERSION ) . ': ' . esc_html( sprintf( 'Successfully registered meta fields for %s.', implode( ' and ', $active_plugins ) ) ) . '

'; + echo '

Supported post types: ' . esc_html( $supported_types ) . '

'; if ($woocommerce_active) { echo '

WooCommerce: Detected and supported. Product SEO fields are available via REST API.

';