Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a16a260f1 | ||
|
|
f29dc299b2 | ||
|
|
5d97b31c12 | ||
|
|
72cfdad5e3 | ||
|
|
5ce70f120e | ||
|
|
cafcd64eff |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**The AI-native management hub for WordPress, WooCommerce, and self-hosted services.**
|
**The AI-native management hub for WordPress, WooCommerce, and self-hosted services.**
|
||||||
|
|
||||||
**Version 3.0.1** — 596 tools across 9 plugins. Connect your sites, stores, repos, and databases — manage them all through Claude, ChatGPT, Cursor, or any MCP client.
|
596 tools across 9 plugins. Connect your sites, stores, repos, and databases — manage them all through Claude, ChatGPT, Cursor, or any MCP client.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
Connect your sites, stores, repos, and databases — manage them all through Claude, ChatGPT, Cursor, or any MCP client.
|
Connect your sites, stores, repos, and databases — manage them all through Claude, ChatGPT, Cursor, or any MCP client.
|
||||||
|
|
||||||
[](https://github.com/airano-ir/mcphub/releases)
|
[](https://github.com/airano-ir/mcphub/releases)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
[](https://pypi.org/project/mcphub-server/)
|
[](https://pypi.org/project/mcphub-server/)
|
||||||
|
|||||||
@@ -1986,7 +1986,7 @@ def get_registered_plugins() -> list:
|
|||||||
|
|
||||||
|
|
||||||
def _get_project_version() -> str:
|
def _get_project_version() -> str:
|
||||||
"""Read version from pyproject.toml."""
|
"""Read version from pyproject.toml, falling back to package metadata."""
|
||||||
try:
|
try:
|
||||||
toml_path = os.path.join(os.path.dirname(os.path.dirname(TEMPLATES_DIR)), "pyproject.toml")
|
toml_path = os.path.join(os.path.dirname(os.path.dirname(TEMPLATES_DIR)), "pyproject.toml")
|
||||||
with open(toml_path) as f:
|
with open(toml_path) as f:
|
||||||
@@ -1995,7 +1995,12 @@ def _get_project_version() -> str:
|
|||||||
return line.split("=")[1].strip().strip('"').strip("'")
|
return line.split("=")[1].strip().strip('"').strip("'")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return "3.0.1"
|
try:
|
||||||
|
from importlib.metadata import version
|
||||||
|
|
||||||
|
return version("mcphub-server")
|
||||||
|
except Exception:
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
def get_about_info() -> dict:
|
def get_about_info() -> dict:
|
||||||
|
|||||||
@@ -411,9 +411,9 @@ class HealthMonitor:
|
|||||||
# Perform health check via plugin instance
|
# Perform health check via plugin instance
|
||||||
health_result = await plugin.health_check()
|
health_result = await plugin.health_check()
|
||||||
elif self.site_manager:
|
elif self.site_manager:
|
||||||
# Fallback: site exists in SiteManager but not ProjectManager
|
# Site exists in SiteManager but not legacy ProjectManager
|
||||||
# (e.g. WooCommerce uses CONSUMER_KEY, not USERNAME/APP_PASSWORD)
|
# Create a temporary plugin instance for a proper health check
|
||||||
health_result = await self._basic_site_health_check(project_id)
|
health_result = await self._site_manager_health_check(project_id)
|
||||||
else:
|
else:
|
||||||
return ProjectHealthStatus(
|
return ProjectHealthStatus(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@@ -503,27 +503,58 @@ class HealthMonitor:
|
|||||||
alerts=[f"CRITICAL: Health check failed - {error_msg}"],
|
alerts=[f"CRITICAL: Health check failed - {error_msg}"],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _basic_site_health_check(self, project_id: str) -> dict[str, Any]:
|
def _find_site_info(self, project_id: str) -> dict[str, Any] | None:
|
||||||
"""Basic HTTP health check for sites not in ProjectManager."""
|
"""Find site info from SiteManager by full_id."""
|
||||||
import aiohttp
|
if not self.site_manager:
|
||||||
|
return None
|
||||||
|
for info in self.site_manager.list_all_sites():
|
||||||
|
if info["full_id"] == project_id:
|
||||||
|
return info
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _site_manager_health_check(self, project_id: str) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Health check for sites managed by SiteManager (not in legacy ProjectManager).
|
||||||
|
|
||||||
|
Creates a temporary plugin instance and calls its health_check() method,
|
||||||
|
falling back to a basic HTTP check if plugin instantiation fails.
|
||||||
|
"""
|
||||||
if not self.site_manager:
|
if not self.site_manager:
|
||||||
return {"healthy": False, "message": "SiteManager not available"}
|
return {"healthy": False, "message": "SiteManager not available"}
|
||||||
|
|
||||||
# Parse plugin_type and site_id from full_id (e.g. "woocommerce_site1")
|
# Look up site info by full_id (handles multi-word plugin types like wordpress_advanced)
|
||||||
parts = project_id.split("_", 1)
|
site_info = self._find_site_info(project_id)
|
||||||
if len(parts) < 2:
|
if not site_info:
|
||||||
return {"healthy": False, "message": f"Invalid project_id format: {project_id}"}
|
return {"healthy": False, "message": f"Site not found: {project_id}"}
|
||||||
|
|
||||||
plugin_type = parts[0]
|
plugin_type = site_info["plugin_type"]
|
||||||
site_id = parts[1]
|
site_id = site_info["site_id"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = self.site_manager.get_site_config(plugin_type, site_id)
|
config = self.site_manager.get_site_config(plugin_type, site_id)
|
||||||
except (KeyError, ValueError):
|
except (KeyError, ValueError):
|
||||||
return {"healthy": False, "message": f"Site not found: {project_id}"}
|
return {"healthy": False, "message": f"Site config not found: {project_id}"}
|
||||||
|
|
||||||
|
# Try to create a temporary plugin instance for a proper health check
|
||||||
|
try:
|
||||||
|
from plugins import registry as plugin_registry
|
||||||
|
|
||||||
|
config_dict = config.to_dict()
|
||||||
|
plugin_instance = plugin_registry.create_instance(plugin_type, site_id, config_dict)
|
||||||
|
return await plugin_instance.health_check()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(
|
||||||
|
f"Could not create plugin instance for {project_id}, "
|
||||||
|
f"falling back to basic HTTP check: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fallback: basic HTTP check if plugin instantiation fails
|
||||||
|
return await self._basic_http_health_check(config.url, project_id)
|
||||||
|
|
||||||
|
async def _basic_http_health_check(self, url: str | None, project_id: str) -> dict[str, Any]:
|
||||||
|
"""Basic HTTP health check as a last-resort fallback."""
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
url = config.url
|
|
||||||
if not url:
|
if not url:
|
||||||
return {"healthy": False, "message": "No URL configured for site"}
|
return {"healthy": False, "message": "No URL configured for site"}
|
||||||
|
|
||||||
|
|||||||
@@ -501,21 +501,14 @@ class SiteManager:
|
|||||||
>>> suffix = manager.get_effective_path_suffix('wordpress_site1')
|
>>> suffix = manager.get_effective_path_suffix('wordpress_site1')
|
||||||
>>> print(suffix) # 'myblog' or 'wordpress_site1'
|
>>> print(suffix) # 'myblog' or 'wordpress_site1'
|
||||||
"""
|
"""
|
||||||
# Parse full_id to get plugin_type and site_id
|
# Look up by full_id from registered sites (handles multi-word plugin types)
|
||||||
parts = full_id.split("_", 1)
|
for info in self.list_all_sites():
|
||||||
if len(parts) != 2:
|
if info["full_id"] == full_id:
|
||||||
return full_id
|
config = self.sites[info["plugin_type"]].get(info["site_id"])
|
||||||
|
if config and config.alias and config.alias != config.site_id:
|
||||||
plugin_type, site_id = parts
|
|
||||||
|
|
||||||
# Try to get config
|
|
||||||
try:
|
|
||||||
config = self.get_site_config(plugin_type, site_id)
|
|
||||||
# If alias exists and is different from site_id, use alias
|
|
||||||
if config.alias and config.alias != config.site_id:
|
|
||||||
return config.alias
|
return config.alias
|
||||||
return full_id
|
return full_id
|
||||||
except ValueError:
|
|
||||||
return full_id
|
return full_id
|
||||||
|
|
||||||
def get_alias_conflicts(self) -> dict[str, list[str]]:
|
def get_alias_conflicts(self) -> dict[str, list[str]]:
|
||||||
|
|||||||
@@ -139,7 +139,7 @@
|
|||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<p class="text-center text-gray-500 text-sm mt-6">
|
<p class="text-center text-gray-500 text-sm mt-6">
|
||||||
MCP Hub v{{ version|default('3.0.1') }}
|
MCP Hub v{{ version }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "mcphub-server"
|
name = "mcphub-server"
|
||||||
version = "3.0.1"
|
version = "3.0.4"
|
||||||
description = "AI-native management hub for WordPress, WooCommerce, and self-hosted services via Model Context Protocol (MCP)"
|
description = "AI-native management hub for WordPress, WooCommerce, and self-hosted services via Model Context Protocol (MCP)"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "MCP Hub", email = "contact@mcphub.dev"}
|
{name = "MCP Hub", email = "contact@mcphub.dev"}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ import warnings
|
|||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from dotenv import find_dotenv, load_dotenv
|
||||||
|
|
||||||
|
load_dotenv(find_dotenv(usecwd=True)) # Always search from CWD (needed for PyPI mcphub.exe)
|
||||||
|
|
||||||
# Suppress noisy deprecation warning from websockets (transitive dependency)
|
# Suppress noisy deprecation warning from websockets (transitive dependency)
|
||||||
warnings.filterwarnings("ignore", category=DeprecationWarning, module="websockets")
|
warnings.filterwarnings("ignore", category=DeprecationWarning, module="websockets")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user