From cb6bcd813621073c3414ad89e4890d7b84e0c582 Mon Sep 17 00:00:00 2001 From: airano Date: Wed, 18 Feb 2026 18:29:15 +0330 Subject: [PATCH] fix(qa): resolve A.1-A.7 code bugs and B.1-B.8 documentation issues Code fixes: - Rename "Coolify Admin" to "MCP Hub Admin" in endpoints - Enrich list_projects with alias and endpoint URL - Downgrade OAuth InvalidTokenError log from WARNING to DEBUG - Add 7 missing health tools to /system/mcp endpoint (now 24 tools) - Remove "Phase 7.2" from health monitor log - Fix WP Advanced health check to pass with REST API only - Downgrade duplicate alias warnings to INFO Documentation fixes: - Document SSE transport and Bearer auth requirement - Recommend plugin-specific endpoints to save tokens - Document plugin vs project endpoint differences - WordPress plugin requirements for SEO/WP-CLI tools - Docker socket mounting guide for WP-CLI - Mark MASTER_API_KEY as recommended (auto-generates temp key) - Env var naming convention with prefix table Co-Authored-By: Claude Opus 4.6 --- DOCKER_README.md | 17 +++- core/endpoints/config.py | 2 +- core/health.py | 4 +- core/oauth/token_manager.py | 2 +- core/site_registry.py | 8 +- docs/getting-started.md | 67 ++++++++++++++- plugins/wordpress_advanced/plugin.py | 2 +- server.py | 122 ++++++++++++++++++++++++--- 8 files changed, 201 insertions(+), 23 deletions(-) diff --git a/DOCKER_README.md b/DOCKER_README.md index d6d58d8..92228ec 100644 --- a/DOCKER_README.md +++ b/DOCKER_README.md @@ -56,6 +56,16 @@ In Claude Desktop's `claude_desktop_config.json`: } ``` +## Authentication + +MCP Hub uses **Bearer token** authentication. Include the `Authorization` header in all requests: + +``` +Authorization: Bearer YOUR_MASTER_API_KEY +``` + +This applies to both MCP clients and API calls. Query parameter auth is not supported. + ## Using Docker Compose ```yaml @@ -69,6 +79,8 @@ services: volumes: - mcphub-data:/app/data - mcphub-logs:/app/logs + # Optional: mount Docker socket for WP-CLI tools + # - /var/run/docker.sock:/var/run/docker.sock:ro restart: unless-stopped volumes: @@ -76,6 +88,9 @@ volumes: mcphub-logs: ``` +> **WP-CLI tools** (cache flush, database export, plugin updates via CLI) require Docker socket access. +> Add `WORDPRESS_SITE1_CONTAINER=your-wp-container-name` to your `.env` and uncomment the Docker socket volume above. + ```bash docker compose up -d ``` @@ -92,7 +107,7 @@ docker compose up -d | Variable | Required | Description | |----------|----------|-------------| -| `MASTER_API_KEY` | **Yes** | API key for authentication | +| `MASTER_API_KEY` | Recommended | API key for authentication. If omitted, a temporary key is auto-generated and printed to logs | | `WORDPRESS_SITE1_URL` | For WP | WordPress site URL | | `WORDPRESS_SITE1_USERNAME` | For WP | WordPress admin username | | `WORDPRESS_SITE1_APP_PASSWORD` | For WP | WordPress Application Password | diff --git a/core/endpoints/config.py b/core/endpoints/config.py index e929a5c..b22db17 100644 --- a/core/endpoints/config.py +++ b/core/endpoints/config.py @@ -99,7 +99,7 @@ ENDPOINT_CONFIGS = { # Mounted at "/" → /mcp (FastMCP adds /mcp automatically) EndpointType.ADMIN: EndpointConfig( path="/", - name="Coolify Admin", + name="MCP Hub Admin", description="Full administrative access to all tools and plugins", endpoint_type=EndpointType.ADMIN, plugin_types=[], # Empty = all plugins diff --git a/core/health.py b/core/health.py index 3c1d4fa..9296049 100644 --- a/core/health.py +++ b/core/health.py @@ -1,5 +1,5 @@ """ -Enhanced Health Monitoring System for MCP Server (Phase 7.2) +Enhanced Health Monitoring System for MCP Server This module provides comprehensive health monitoring capabilities including: - Response time tracking @@ -172,7 +172,7 @@ class HealthMonitor: # Request rate tracking (for requests per minute) self.request_timestamps: deque = deque(maxlen=1000) - logger.info("HealthMonitor initialized (Phase 7.2)") + logger.info("HealthMonitor initialized") def _setup_default_thresholds(self): """Setup default alert thresholds.""" diff --git a/core/oauth/token_manager.py b/core/oauth/token_manager.py index 39ecc70..3bc1690 100644 --- a/core/oauth/token_manager.py +++ b/core/oauth/token_manager.py @@ -139,7 +139,7 @@ class TokenManager: logger.warning("Expired access token") raise except jwt.InvalidTokenError as e: - logger.warning(f"Invalid access token: {e}") + logger.debug(f"Invalid access token: {e}") raise def generate_refresh_token(self, client_id: str, access_token: str) -> str: diff --git a/core/site_registry.py b/core/site_registry.py index e2f537c..0acaa67 100644 --- a/core/site_registry.py +++ b/core/site_registry.py @@ -94,15 +94,13 @@ class SiteRegistry: # Log alias conflicts if any if self.alias_conflicts: - self.logger.warning("=" * 50) - self.logger.warning("DUPLICATE ALIAS CONFLICTS DETECTED:") + self.logger.info("Duplicate alias conflicts detected:") for alias, full_ids in self.alias_conflicts.items(): winner = self.aliases.get(alias) losers = [fid for fid in full_ids if fid != winner] - self.logger.warning( + self.logger.info( f" Alias '{alias}': {winner} (winner), {losers} (using full_id)" ) - self.logger.warning("=" * 50) # Reserved words that should NOT be interpreted as site IDs RESERVED_SITE_WORDS = { @@ -206,7 +204,7 @@ class SiteRegistry: if alias not in self.alias_conflicts: self.alias_conflicts[alias] = [existing_full_id] self.alias_conflicts[alias].append(full_id) - self.logger.warning( + self.logger.info( f"Duplicate alias '{alias}': {full_id} conflicts with {existing_full_id}. " f"{full_id} will use full_id for endpoint path." ) diff --git a/docs/getting-started.md b/docs/getting-started.md index f2b37dd..6f72371 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -129,7 +129,7 @@ Edit the `.env` file with your credentials: ```bash # ============================================ -# Required +# Authentication (recommended — auto-generates temp key if omitted) # ============================================ MASTER_API_KEY=your-secure-key-here @@ -171,6 +171,52 @@ RATE_LIMIT_PER_HOUR=1000 RATE_LIMIT_PER_DAY=10000 ``` +### WordPress Plugin Requirements + +Some WordPress tools require additional plugins on your WordPress site: + +| MCP Tool Category | WordPress Plugin Required | +|-------------------|--------------------------| +| SEO tools (`get_post_seo`, `update_post_seo`) | **Rank Math** or **Yoast SEO** | +| WP-CLI tools (`wp_cache_flush`, `wp_db_export`, etc.) | Docker socket access + `CONTAINER` env var | +| WooCommerce tools | **WooCommerce** 3.0+ (separate `WOOCOMMERCE_` config) | + +### Docker Socket for WP-CLI Tools + +WP-CLI tools (cache management, database export, plugin updates via CLI) require Docker socket access: + +1. Add the container name to your `.env`: + ```bash + WORDPRESS_SITE1_CONTAINER=your-wp-container-name + ``` + +2. Mount the Docker socket in `docker-compose.yaml`: + ```yaml + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + ``` + +Without Docker socket, WP-CLI tools will return a "not available" message but all REST API tools work normally. + +### Environment Variable Naming Convention + +All site configuration follows the pattern: `{PLUGIN_PREFIX}_{SITE_ID}_{CONFIG_KEY}` + +| Plugin | Prefix | Example | +|--------|--------|---------| +| WordPress | `WORDPRESS_` | `WORDPRESS_SITE1_URL` | +| WooCommerce | `WOOCOMMERCE_` | `WOOCOMMERCE_STORE1_URL` | +| WordPress Advanced | `WORDPRESS_ADVANCED_` | `WORDPRESS_ADVANCED_SITE1_URL` | +| Gitea | `GITEA_` | `GITEA_REPO1_URL` | +| n8n | `N8N_` | `N8N_INSTANCE1_URL` | +| Supabase | `SUPABASE_` | `SUPABASE_PROJECT1_URL` | +| OpenPanel | `OPENPANEL_` | `OPENPANEL_INSTANCE1_URL` | +| Appwrite | `APPWRITE_` | `APPWRITE_PROJECT1_URL` | +| Directus | `DIRECTUS_` | `DIRECTUS_INSTANCE1_URL` | + +- `SITE_ID` can be any alphanumeric identifier (e.g., `SITE1`, `PROD`, `MYBLOG`) +- Add `_ALIAS` for a friendly name used in tool calls (e.g., `WORDPRESS_SITE1_ALIAS=myblog`) + ### Configuration Tips - **Site Aliases**: Use friendly names like `myblog`, `mystore`, or `mygitea` @@ -240,6 +286,8 @@ docker compose logs -f mcphub ## Connect Your AI Client +MCP Hub uses **SSE (Server-Sent Events)** transport over HTTP. All requests require **Bearer token** authentication via the `Authorization` header. Query parameter auth is not supported. + ### Claude Desktop Add to `claude_desktop_config.json`: @@ -343,7 +391,7 @@ The `site` parameter accepts either a **site_id** (e.g., `site1`) or an **alias* ### Multi-Endpoint Architecture -Use specific endpoints to limit tool access: +Use specific endpoints to limit tool access and save tokens: ``` /mcp → All 596 tools (Master API Key) @@ -351,9 +399,24 @@ Use specific endpoints to limit tool access: /wordpress/mcp → WordPress tools (67 tools) /woocommerce/mcp → WooCommerce tools (28 tools) /gitea/mcp → Gitea tools (56 tools) +/n8n/mcp → n8n tools (56 tools) +/supabase/mcp → Supabase tools (70 tools) +/openpanel/mcp → OpenPanel tools (73 tools) +/appwrite/mcp → Appwrite tools (100 tools) +/directus/mcp → Directus tools (100 tools) /project/{alias}/mcp → Per-project (auto-injects site) ``` +> **Recommendation**: Use plugin-specific endpoints (e.g., `/wordpress/mcp`) instead of `/mcp` when possible. This reduces the number of tools your AI client loads, saving context tokens and improving response quality. + +**Plugin endpoint vs Project endpoint:** + +| Feature | Plugin endpoint (`/wordpress/mcp`) | Project endpoint (`/project/myblog/mcp`) | +|---------|-----------------------------------|----------------------------------------| +| Tools loaded | All tools for that plugin type | Same tools, but `site` parameter auto-injected | +| Site selection | Must pass `site` parameter manually | Site is auto-selected (no `site` param needed) | +| Best for | Managing multiple sites of same type | Dedicated access to a single site | + --- ## Docker Deployment diff --git a/plugins/wordpress_advanced/plugin.py b/plugins/wordpress_advanced/plugin.py index c9b5696..703ea2d 100644 --- a/plugins/wordpress_advanced/plugin.py +++ b/plugins/wordpress_advanced/plugin.py @@ -127,7 +127,7 @@ class WordPressAdvancedPlugin(BasePlugin): rest_api_available = False return { - "healthy": wp_cli_available, # Only WP-CLI is critical for wordpress_advanced + "healthy": wp_cli_available or rest_api_available, "wp_cli_available": wp_cli_available, "rest_api_available": rest_api_available, "features": { diff --git a/server.py b/server.py index cd469c8..791580d 100644 --- a/server.py +++ b/server.py @@ -1183,6 +1183,19 @@ async def _list_projects_impl() -> str: """Internal implementation for listing projects.""" try: projects = project_manager.list_projects() + + # Enrich with alias and endpoint info from SiteManager + all_sites = site_manager.list_all_sites() + site_lookup = {s["full_id"]: s for s in all_sites} + + for project in projects: + full_id = project.get("id", "") + site_info = site_lookup.get(full_id, {}) + alias = site_info.get("alias") + path_suffix = alias if alias and alias != site_info.get("site_id") else full_id + project["alias"] = alias + project["endpoint"] = f"/project/{path_suffix}/mcp" + result = {"total": len(projects), "projects": projects} import json @@ -2827,7 +2840,7 @@ async def oauth_get_client_info(client_id: str) -> dict: # === PHASE X.3: SYSTEM TOOLS === -# Internal implementations for Phase X.3 system tools +# Internal implementations for system tools async def _get_endpoints_impl() -> dict: """Internal implementation for listing endpoints.""" try: @@ -2835,7 +2848,7 @@ async def _get_endpoints_impl() -> dict: endpoints = [ { "path": "/mcp", - "name": "Coolify Admin", + "name": "MCP Hub Admin", "description": "Full administrative access to all tools", "require_master_key": True, "plugin_types": ["all"], @@ -2846,7 +2859,7 @@ async def _get_endpoints_impl() -> dict: "description": "System management tools (API keys, OAuth, health, rate limiting)", "require_master_key": True, "plugin_types": ["system"], - "tool_count": 16, + "tool_count": 24, }, { "path": "/wordpress/mcp", @@ -3333,13 +3346,14 @@ async def manage_api_keys_rotate(project_id: str) -> dict: def create_system_mcp(): """ - Create System-only MCP instance (Phase X.3). + Create System-only MCP instance. - Contains only 16 system management tools: - - API Key Management (6) - - OAuth Management (3) - - Rate Limiting (3) - - Health & Status (4) + Contains 24 system management tools: + - API Key Management (6): create, list, get_info, revoke, delete, rotate + - OAuth Management (4): register_client, list_clients, revoke_client, get_client_info + - Rate Limiting (3): get_stats, reset, set_config + - Health & Monitoring (7): check_all, get_project, get_metrics, get_uptime, get_project_metrics, export + - Status & Discovery (4): list_projects, get_endpoints, get_system_info, get_audit_log """ from fastmcp import FastMCP @@ -3349,7 +3363,9 @@ Available tools: • API Key Management: create, list, get_info, revoke, delete, rotate • OAuth Management: register_client, list_clients, revoke_client, get_client_info • Rate Limiting: get_stats, reset, set_config -• Health & Status: list_projects, get_endpoints, get_system_info, get_audit_log +• Health & Monitoring: check_all_projects_health, get_project_health, get_system_metrics, + get_system_uptime, get_project_metrics, export_health_metrics, get_project_info +• Status & Discovery: list_projects, get_endpoints, get_system_info, get_audit_log Use list_projects() to see all configured sites across all plugin types. Use get_endpoints() to see all available MCP endpoints.""" @@ -3460,6 +3476,92 @@ Use get_endpoints() to see all available MCP endpoints.""" requests_per_minute, requests_per_hour, requests_per_day ) + # Health & Monitoring tools (7) + @system_mcp.tool() + async def get_project_info(project_id: str) -> str: + """Get detailed information about a specific project.""" + try: + info = project_manager.get_project_info(project_id) + if info is None: + return f"Project '{project_id}' not found. Use list_projects to see available projects." + import json + + return json.dumps(info, indent=2) + except Exception as e: + logger.error(f"Error getting project info: {e}", exc_info=True) + return f"Error: {str(e)}" + + @system_mcp.tool() + async def check_all_projects_health() -> str: + """Check health status of all projects with enhanced metrics.""" + try: + health_data = await health_monitor.check_all_projects_health(include_metrics=True) + import json + + return json.dumps(health_data, indent=2) + except Exception as e: + logger.error(f"Error checking health: {e}", exc_info=True) + return f"Error: {str(e)}" + + @system_mcp.tool() + async def get_project_health(project_id: str) -> str: + """Get detailed health information for a specific project.""" + try: + status = await health_monitor.check_project_health(project_id, include_metrics=True) + import json + + return json.dumps(status.to_dict(), indent=2) + except Exception as e: + logger.error(f"Error getting project health: {e}", exc_info=True) + return f"Error: {str(e)}" + + @system_mcp.tool() + async def get_system_metrics() -> str: + """Get overall MCP server metrics and statistics.""" + try: + metrics = health_monitor.get_system_metrics() + import json + + return json.dumps(metrics.to_dict(), indent=2) + except Exception as e: + logger.error(f"Error getting system metrics: {e}", exc_info=True) + return f"Error: {str(e)}" + + @system_mcp.tool() + async def get_system_uptime() -> str: + """Get MCP server uptime information.""" + try: + uptime = health_monitor.get_uptime() + import json + + return json.dumps(uptime, indent=2) + except Exception as e: + logger.error(f"Error getting uptime: {e}", exc_info=True) + return f"Error: {str(e)}" + + @system_mcp.tool() + async def get_project_metrics(project_id: str, hours: int = 1) -> str: + """Get historical metrics for a specific project.""" + try: + hours = min(hours, 24) + metrics = health_monitor.get_project_metrics(project_id, hours=hours) + import json + + return json.dumps(metrics, indent=2) + except Exception as e: + logger.error(f"Error getting project metrics: {e}", exc_info=True) + return f"Error: {str(e)}" + + @system_mcp.tool() + async def export_health_metrics(output_path: str = "logs/metrics_export.json") -> str: + """Export all health metrics to a JSON file.""" + try: + exported_path = health_monitor.export_metrics(output_path=output_path, format="json") + return f"Metrics exported successfully to: {exported_path}" + except Exception as e: + logger.error(f"Error exporting metrics: {e}", exc_info=True) + return f"Error: {str(e)}" + logger.info("Created System endpoint with 24 tools") return system_mcp